1 /* rewrite of the IMAP code by Neil Spring
2 * (nspring@cs.washington.edu) to support gnutls and
3 * persistent connections to servers. */
5 /* Originally written by Yong-iL Joh (tolkien@mizi.com),
6 * modified by Jorge Garcia (Jorge.Garcia@uv.es), and
7 * modified by Jay Francis (jtf@u880.org) to support
17 #include "passwordMgr.h"
19 #include "MessageList.h"
21 #include <sys/types.h>
34 #define PCU (pc->u).pop_imap
38 #define IMAP_DM(pc, lvl, args...) DM(pc, lvl, "imap4: " args)
41 #define DEFROB(x) memfrob(x, x ## _len)
42 #define ENFROB(x) memfrob(x, x ## _len)
48 /* this array maps server:port pairs to file descriptors, so
49 that when more than one mailbox is queried from a server,
50 we only use one socket. It's limited in size by the
51 number of different mailboxes displayed. */
53 static struct fdmap_struct
{
54 char *user_server_port
; /* tuple, in string form */
55 /*@owned@ */ struct connection_state
*cs
;
58 static void ask_user_for_password( /*@notnull@ */ Pop3 pc
,
61 /* authentication callbacks */
63 static int authenticate_md5( /*@notnull@ */ Pop3 pc
,
64 struct connection_state
*scs
,
65 const char *capabilities
);
67 static int authenticate_plaintext( /*@notnull@ */ Pop3 pc
,
68 struct connection_state
*scs
,
69 const char *capabilities
);
71 /* the auth_methods array maps authentication identifiers
72 to the callback that will attempt to authenticate */
73 static struct imap_authentication_method
{
75 /* callback returns 1 if successful, 0 if failed */
76 int (*auth_callback
) ( /*@notnull@ */ Pop3 pc
,
77 struct connection_state
* scs
,
78 const char *capabilities
);
82 "cram-md5", authenticate_md5
}, {
84 "plaintext", authenticate_plaintext
}, {
89 /* recover a socket from the connection cache */
92 static struct connection_state
*state_for_pcu(Pop3 pc
)
95 struct connection_state
*retval
= NULL
;
98 malloc(strlen(PCU
.userName
) + strlen(PCU
.serverName
) + 22);
99 sprintf(connection_id
, "%s|%s|%d", PCU
.userName
, PCU
.serverName
,
101 for (i
= 0; i
< FDMAP_SIZE
; i
++)
102 if (fdmap
[i
].user_server_port
!= NULL
&&
103 (strcmp(connection_id
, fdmap
[i
].user_server_port
) == 0)) {
104 retval
= fdmap
[i
].cs
;
110 /* bind to the connection cache */
111 static void bind_state_to_pcu(Pop3 pc
,
112 /*@owned@ */ struct connection_state
*scs
)
120 malloc(strlen(PCU
.userName
) + strlen(PCU
.serverName
) + 22);
121 sprintf(connection_id
, "%s|%s|%d", PCU
.userName
, PCU
.serverName
,
123 for (i
= 0; i
< FDMAP_SIZE
&& fdmap
[i
].cs
!= NULL
; i
++);
124 if (i
== FDMAP_SIZE
) {
125 /* should never happen */
126 IMAP_DM(pc
, DEBUG_ERROR
,
127 "Tried to open too many IMAP connections. Sorry!\n");
130 fdmap
[i
].user_server_port
= connection_id
;
134 /* remove from the connection cache */
138 struct connection_state
*unbind(
139 /*@returned@*/ struct connection_state
143 struct connection_state
*retval
= NULL
;
146 for (i
= 0; i
< FDMAP_SIZE
&& fdmap
[i
].cs
!= scs
; i
++);
147 if (i
< FDMAP_SIZE
) {
148 free(fdmap
[i
].user_server_port
);
149 fdmap
[i
].user_server_port
= NULL
;
150 retval
= fdmap
[i
].cs
;
156 /* creates a connection to the server, if a matching one doesn't exist. */
157 /* *always* returns null, just declared this wasy to match other protocols. */
159 FILE *imap_open(Pop3 pc
)
161 static int complained_already
; /* we have to succeed once before
162 complaining again about failure */
163 struct connection_state
*scs
;
164 struct imap_authentication_method
*a
;
165 char *connection_name
;
167 char capabilities
[BUF_SIZE
];
171 if (state_for_pcu(pc
) != NULL
) {
172 /* don't need to open. */
176 /* got this far; we're going to create a connection_state
177 structure, although it might be a blacklist entry */
178 connection_name
= malloc(strlen(PCU
.serverName
) + 20);
179 sprintf(connection_name
, "%s:%d", PCU
.serverName
, PCU
.serverPort
);
183 /* no cached connection */
184 sd
= sock_connect((const char *) PCU
.serverName
, PCU
.serverPort
);
186 if (complained_already
== 0) {
187 IMAP_DM(pc
, DEBUG_ERROR
, "Couldn't connect to %s:%d: %s\n",
188 PCU
.serverName
, PCU
.serverPort
,
189 errno
? strerror(errno
) : "");
190 complained_already
= 1;
192 if (errno
== ETIMEDOUT
) {
193 /* temporarily bump the interval, in a crude way:
194 fast forward time so that the mailbox isn't
195 checked for a while. */
196 pc
->prevtime
= time(0) + 60 * 5; /* now + 60 seconds per min * 5 minutes */
197 /* TCP's retry (how much time has elapsed while
198 the connect times out) is around 3 minutes;
199 here we just try to allow checking local
200 mailboxes more often while remote things are
201 unavailable or disconnected. */
206 /* build the connection using STARTTLS */
207 if (PCU
.dossl
!= 0 && (PCU
.serverPort
== 143)) {
208 /* setup an unencrypted binding long enough to invoke STARTTLS */
209 scs
= initialize_unencrypted(sd
, connection_name
, pc
);
212 tlscomm_printf(scs
, "a000 CAPABILITY\r\n");
213 if (tlscomm_expect(scs
, "* CAPABILITY", capabilities
, BUF_SIZE
) ==
215 goto communication_failure
;
217 if (!strstr(capabilities
, "STARTTLS")) {
218 IMAP_DM(pc
, DEBUG_ERROR
,
219 "server doesn't support ssl imap on port 143.");
220 goto communication_failure
;
224 IMAP_DM(pc
, DEBUG_INFO
, "Negotiating TLS within IMAP");
225 tlscomm_printf(scs
, "a001 STARTTLS\r\n");
227 if (tlscomm_expect(scs
, "a001 ", buf
, BUF_SIZE
) == 0)
228 goto communication_failure
;
230 if (strstr(buf
, "a001 OK") == 0) {
231 /* we didn't see the success message in the response */
232 IMAP_DM(pc
, DEBUG_ERROR
, "couldn't negotiate tls. :(\n");
233 goto communication_failure
;
236 /* we don't need the unencrypted state anymore */
237 /* note that communication_failure will close the
238 socket and free via tls_close() */
239 free(scs
); /* fall through will scs = initialize_gnutls(sd); */
242 /* either we've negotiated ssl from starttls, or
243 we're starting an encrypted connection now */
244 if (PCU
.dossl
!= 0) {
245 scs
= initialize_gnutls(sd
, connection_name
, pc
, PCU
.serverName
);
247 IMAP_DM(pc
, DEBUG_ERROR
, "Failed to initialize TLS\n");
251 scs
= initialize_unencrypted(sd
, connection_name
, pc
);
254 /* authenticate; first find out how */
255 /* note that capabilities may have changed since last
256 time we may have asked, if we called STARTTLS, my
257 server will allow plain password login within an
258 encrypted session. */
259 tlscomm_printf(scs
, "a000 CAPABILITY\r\n");
260 if (tlscomm_expect(scs
, "* CAPABILITY", capabilities
, BUF_SIZE
) == 0) {
261 IMAP_DM(pc
, DEBUG_ERROR
, "unable to query capability string");
262 goto communication_failure
;
265 /* try each authentication method in turn. */
266 for (a
= auth_methods
; a
->name
!= NULL
; a
++) {
267 /* was it specified or did the user leave it up to us? */
268 if (PCU
.authList
[0] == '\0'
269 || strstr(PCU
.authList
, a
->name
) != NULL
)
270 /* try the authentication method */
271 if ((a
->auth_callback(pc
, scs
, capabilities
)) != 0) {
272 /* store this well setup connection in the cache */
273 bind_state_to_pcu(pc
, scs
);
274 complained_already
= 0;
279 /* if authentication worked, we won't get here */
280 IMAP_DM(pc
, DEBUG_ERROR
,
281 "All authentication methods failed for '%s@%s:%d'\n",
282 PCU
.userName
, PCU
.serverName
, PCU
.serverPort
);
283 communication_failure
:
284 tlscomm_printf(scs
, "a002 LOGOUT\r\n");
290 void imap_cacheHeaders( /*@notnull@ */ Pop3 pc
);
292 int imap_checkmail( /*@notnull@ */ Pop3 pc
)
294 /* recover connection state from the cache */
295 struct connection_state
*scs
= state_for_pcu(pc
);
297 static int command_id
;
299 /* if it's not in the cache, try to open */
301 IMAP_DM(pc
, DEBUG_INFO
, "Need new connection to %s@%s\n",
302 PCU
.userName
, PCU
.serverName
);
303 (void) imap_open(pc
);
304 scs
= state_for_pcu(pc
);
310 if (tlscomm_is_blacklisted(scs
) != 0) {
311 /* unresponsive server, don't bother. */
315 /* if we've got it by now, try the status query */
317 tlscomm_printf(scs
, "a%03d STATUS %s (MESSAGES UNSEEN)\r\n",
318 command_id
% 1000, pc
->path
);
319 if (tlscomm_expect(scs
, "* STATUS", buf
, 127) != 0) {
320 /* a valid response? */
321 // doesn't support spaces: (void) sscanf(buf, "* STATUS %*s (MESSAGES %d UNSEEN %d)",
323 msg
= strstr(buf
, "(MESSAGES");
325 (void) sscanf(msg
, "(MESSAGES %d UNSEEN %d)",
326 &(pc
->TotalMsgs
), &(pc
->UnreadMsgs
));
327 /* update the cached headers if evidence that change
328 has occurred; not necessarily complete. */
329 if (pc
->UnreadMsgs
!= pc
->OldUnreadMsgs
||
330 pc
->TotalMsgs
!= pc
->OldMsgs
) {
331 if (PCU
.wantCacheHeaders
) {
332 imap_cacheHeaders(pc
);
336 /* something went wrong. bail. */
337 tlscomm_close(unbind(scs
));
344 imap_releaseHeaders(Pop3 pc
__attribute__ ((unused
)), struct msglst
*h
)
347 /* allow the list to be released next time around */
348 if (h
->in_use
<= 0) {
349 /* free the old one */
351 struct msglst
*n
= h
->next
;
360 void imap_cacheHeaders( /*@notnull@ */ Pop3 pc
)
362 struct connection_state
*scs
= state_for_pcu(pc
);
367 (void) imap_open(pc
);
368 scs
= state_for_pcu(pc
);
373 if (tlscomm_is_blacklisted(scs
) != 0) {
377 if (pc
->headerCache
!= NULL
) {
378 /* decrement the reference count, and free our version */
379 imap_releaseHeaders(pc
, pc
->headerCache
);
380 pc
->headerCache
= NULL
;
383 IMAP_DM(pc
, DEBUG_INFO
, "working headers\n");
385 tlscomm_printf(scs
, "a004 EXAMINE %s\r\n", pc
->path
);
386 if (tlscomm_expect(scs
, "a004 OK", buf
, 127) == 0) {
387 tlscomm_close(unbind(scs
));
390 IMAP_DM(pc
, DEBUG_INFO
, "examine ok\n");
392 /* if we've got it by now, try the status query */
393 tlscomm_printf(scs
, "a005 SEARCH UNSEEN\r\n");
394 if (tlscomm_expect(scs
, "* SEARCH", buf
, 127) == 0) {
395 tlscomm_close(unbind(scs
));
398 IMAP_DM(pc
, DEBUG_INFO
, "search: %s", buf
);
400 return; /* search turned up nothing */
401 msgid
= strtok(buf
+ 9, " \r\n");
402 pc
->headerCache
= NULL
;
403 /* the isdigit cruft is to deal with EOL */
404 if (msgid
!= NULL
&& isdigit(msgid
[0]))
406 struct msglst
*m
= malloc(sizeof(struct msglst
));
408 int fetch_command_done
= FALSE
;
409 tlscomm_printf(scs
, "a04 FETCH %s (FLAGS "
410 "BODY[HEADER.FIELDS (FROM SUBJECT)])\r\n",
412 if (tlscomm_expect(scs
, "* ", hdrbuf
, 127)) {
415 while (m
->subj
[0] == '\0' || m
->from
[0] == '\0') {
416 if (tlscomm_expect(scs
, "", hdrbuf
, 127)) {
417 if (strncasecmp(hdrbuf
, "Subject:", 8) == 0) {
418 strncpy(m
->subj
, hdrbuf
+ 9, SUBJ_LEN
- 1);
419 m
->subj
[SUBJ_LEN
- 1] = '\0';
420 } else if (strncasecmp(hdrbuf
, "From: ", 5) == 0) {
421 strncpy(m
->from
, hdrbuf
+ 6, FROM_LEN
- 1);
422 m
->from
[FROM_LEN
- 1] = '\0';
423 } else if (strncasecmp
424 (hdrbuf
, "a04 OK FETCH", 5) == 0) {
425 /* server says we're done getting this header, which
426 may occur if the message has no subject */
427 if (m
->from
[0] == '\0') {
428 strcpy(m
->from
, " ");
430 if (m
->subj
[0] == '\0') {
431 strcpy(m
->subj
, "(no subject)");
433 fetch_command_done
= TRUE
;
436 IMAP_DM(pc
, DEBUG_ERROR
,
437 "timedout looking for headers.: %s",
439 strcpy(m
->from
, "wmbiff");
440 strcpy(m
->subj
, "failure");
443 IMAP_DM(pc
, DEBUG_INFO
, "From: '%s' Subj: '%s'\n",
445 m
->next
= pc
->headerCache
;
447 pc
->headerCache
->in_use
= 0; /* initialize that it isn't locked */
449 IMAP_DM(pc
, DEBUG_ERROR
, "error fetching: %s", hdrbuf
);
451 if (!fetch_command_done
) {
452 tlscomm_expect(scs
, "a04 OK", hdrbuf
, 127);
455 while ((msgid
= strtok(NULL
, " \r\n")) != NULL
456 && isdigit(msgid
[0]));
458 tlscomm_printf(scs
, "a06 CLOSE\r\n"); /* return to polling state */
459 /* may be unneeded tlscomm_expect(scs, "a06 OK CLOSE\r\n" ); see if it worked? */
460 IMAP_DM(pc
, DEBUG_INFO
, "worked headers\n");
463 /* a client is asking for the headers, hand em a reference, increase the
464 one-bit reference counter */
465 struct msglst
*imap_getHeaders( /*@notnull@ */ Pop3 pc
)
467 if (pc
->headerCache
== NULL
)
468 imap_cacheHeaders(pc
);
469 if (pc
->headerCache
!= NULL
)
470 pc
->headerCache
->in_use
= 1;
471 return pc
->headerCache
;
474 /* parse the config line to setup the Pop3 structure */
475 int imap4Create( /*@notnull@ */ Pop3 pc
, const char *const str
)
479 /* special characters aren't allowed in hostnames, rfc 1034 */
480 const char *regexes
[] = {
481 // type : username : password @ hostname (/ name)?(:port)?
482 ".*imaps?:([^: ]{1,32}):([^@]{0,32})@([A-Za-z1-9][-A-Za-z0-9_.]+)(/(\"[^\"]+\")|([^:@ ]+))?(:[0-9]+)?( *([CcAaPp][-A-Za-z5 ]*))?$",
483 ".*imaps?:([^: ]{1,32}) ([^ ]{1,32}) ([A-Za-z1-9][-A-Za-z0-9_.]+)(/(\"[^\"]+\")|([^: ]+))?( [0-9]+)?( *([CcAaPp][-A-Za-z5 ]*))?$",
488 struct regulo regulos
[] = {
489 {1, PCU
.userName
, regulo_strcpy
},
490 {2, PCU
.password
, regulo_strcpy
},
491 {3, PCU
.serverName
, regulo_strcpy
},
492 {4, pc
->path
, regulo_strcpy_skip1
},
493 {7, &PCU
.serverPort
, regulo_atoi
},
494 {9, PCU
.authList
, regulo_strcpy_tolower
},
500 ".*imaps?:([^: ]{1,32}):([^@]{0,32})@([^/: ]+)(/(\"[^\"]+\")|([^:@ ]+))?(:[0-9]+)?( *(.*))?$";
502 ".*imaps?:([^: ]{1,32}) ([^ ]{1,32}) ([^/: ]+)(/(\"[^\"]+\")|([^: ]+))?( [0-9]+)?( *(.*))?$";
506 /* IMAP4 format: imap:user:password@server/mailbox[:port] */
507 /* If 'str' line is badly formatted, wmbiff won't display the mailbox. */
508 if (strncmp("sslimap:", str
, 8) == 0 || strncmp("imaps:", str
, 6) == 0) {
509 #ifdef HAVE_GNUTLS_GNUTLS_H
510 static int haveBeenWarned
;
512 if (!haveBeenWarned
) {
513 printf("wmbiff uses gnutls for TLS/SSL encryption support:\n"
514 " If you distribute software that uses gnutls, don't forget\n"
515 " to warn the users of your software that gnutls is at a\n"
516 " testing phase and may be totally insecure.\n"
517 "\nConsider yourself warned.\n");
521 printf("This copy of wmbiff was not compiled with gnutls;\n"
522 "imaps is unavailable. Exiting to protect your\n"
523 "passwords and privacy.\n");
531 PCU
.serverPort
= (PCU
.dossl
!= 0) ? 993 : 143;
532 PCU
.authList
[0] = '\0';
534 /* argh, str and pc->path are aliases, so we can't just write the default
535 value into the string we're about to parse. */
536 unaliased_str
= strdup(str
);
537 strcpy(pc
->path
, "INBOX");
539 for (matchedchars
= 0, i
= 0;
540 regexes
[i
] != NULL
&& matchedchars
<= 0; i
++) {
541 matchedchars
= regulo_match(regexes
[i
], unaliased_str
, regulos
);
544 /* failed to match either regex */
545 if (matchedchars
<= 0) {
547 IMAP_DM(pc
, DEBUG_ERROR
, "Couldn't parse line %s (%d)\n"
548 " If this used to work, run wmbiff with the -relax option, and\n"
549 " send mail to wmbiff-devel@lists.sourceforge.net with the hostname\n"
550 " of your mail server.\n", unaliased_str
, matchedchars
);
554 PCU
.password_len
= strlen(PCU
.password
);
555 if (PCU
.password
[0] == '\0') {
556 PCU
.interactive_password
= 1;
558 ENFROB(PCU
.password
);
561 // grab_authList(unaliased_str + matchedchars, PCU.authList);
565 IMAP_DM(pc
, DEBUG_INFO
, "userName= '%s'\n", PCU
.userName
);
566 IMAP_DM(pc
, DEBUG_INFO
, "password is %d characters long\n",
567 (int) PCU
.password_len
);
568 IMAP_DM(pc
, DEBUG_INFO
, "serverName= '%s'\n", PCU
.serverName
);
569 IMAP_DM(pc
, DEBUG_INFO
, "serverPath= '%s'\n", pc
->path
);
570 IMAP_DM(pc
, DEBUG_INFO
, "serverPort= '%d'\n", PCU
.serverPort
);
571 IMAP_DM(pc
, DEBUG_INFO
, "authList= '%s'\n", PCU
.authList
);
573 if (strcmp(pc
->action
, "msglst") == 0 ||
574 strcmp(pc
->fetchcmd
, "msglst") == 0 ||
575 strcmp(pc
->button2
, "msglst") == 0) {
576 PCU
.wantCacheHeaders
= 1;
578 PCU
.wantCacheHeaders
= 0;
580 pc
->checkMail
= imap_checkmail
;
581 pc
->getHeaders
= imap_getHeaders
;
582 pc
->releaseHeaders
= imap_releaseHeaders
;
586 pc
->OldUnreadMsgs
= -1;
590 static int authenticate_plaintext( /*@notnull@ */ Pop3 pc
,
591 struct connection_state
*scs
,
592 const char *capabilities
)
595 /* is login prohibited? */
596 /* "An IMAP client which complies with [rfc2525, section 3.2]
597 * MUST NOT issue the LOGIN command if this capability is present.
599 if (strstr(capabilities
, "LOGINDISABLED")) {
600 IMAP_DM(pc
, DEBUG_ERROR
,
601 "Plaintext auth prohibited by server: (LOGINDISABLED).\n");
602 goto plaintext_failed
;
605 ask_user_for_password(pc
, 0);
608 DEFROB(PCU
.password
);
609 tlscomm_printf(scs
, "a001 LOGIN %s \"%s\"\r\n", PCU
.userName
,
611 ENFROB(PCU
.password
);
612 if (tlscomm_expect(scs
, "a001 ", buf
, BUF_SIZE
) == 0) {
613 IMAP_DM(pc
, DEBUG_ERROR
,
614 "Did not get a response to the LOGIN command.\n");
615 goto plaintext_failed
;
619 IMAP_DM(pc
, DEBUG_ERROR
, "IMAP Login failed: %s\n", buf
);
620 /* if we're prompting the user, ask again, else fail */
621 if (PCU
.interactive_password
) {
622 PCU
.password
[0] = '\0';
623 ask_user_for_password(pc
, 1); /* 1=overwrite the cache */
625 goto plaintext_failed
;
639 authenticate_md5(Pop3 pc
,
640 struct connection_state
*scs
, const char *capabilities
)
648 if (!strstr(capabilities
, "AUTH=CRAM-MD5")) {
649 /* server doesn't support cram-md5. */
653 tlscomm_printf(scs
, "a007 AUTHENTICATE CRAM-MD5\r\n");
654 if (tlscomm_expect(scs
, "+ ", buf
, BUF_SIZE
) == 0)
657 Decode_Base64(buf
+ 2, buf2
);
658 IMAP_DM(pc
, DEBUG_INFO
, "CRAM-MD5 challenge: %s\n", buf2
);
660 strcpy(buf
, PCU
.userName
);
662 ask_user_for_password(pc
, 0);
663 rc
= gcry_md_open(&gmh
, GCRY_MD_MD5
, GCRY_MD_FLAG_HMAC
);
665 IMAP_DM(pc
, DEBUG_INFO
, "unable to initialize gcrypt md5\n");
668 DEFROB(PCU
.password
);
669 gcry_md_setkey(gmh
, PCU
.password
, strlen(PCU
.password
));
670 ENFROB(PCU
.password
);
671 gcry_md_write(gmh
, (unsigned char *) buf2
, strlen(buf2
));
673 md5
= gcry_md_read(gmh
, 0);
674 Bin2Hex(md5
, 16, buf2
);
678 IMAP_DM(pc
, DEBUG_INFO
, "CRAM-MD5 response: %s\n", buf
);
679 Encode_Base64(buf
, buf2
);
681 tlscomm_printf(scs
, "%s\r\n", buf2
);
682 if (tlscomm_expect(scs
, "a007 ", buf
, BUF_SIZE
) == 0)
685 if (!strncmp(buf
, "a007 OK", 7))
686 return 1; /* AUTH successful */
688 IMAP_DM(pc
, DEBUG_ERROR
,
689 "CRAM-MD5 AUTH failed for user '%s@%s:%d'\n",
690 PCU
.userName
, PCU
.serverName
, PCU
.serverPort
);
691 IMAP_DM(pc
, DEBUG_INFO
, "It said %s", buf
);
695 IMAP_DM(pc
, DEBUG_ERROR
,
696 "tlscomm_expect failed during cram-md5 auth: %s", buf
);
697 IMAP_DM(pc
, DEBUG_ERROR
, "failed to authenticate using cram-md5.");
702 static void ask_user_for_password( /*@notnull@ */ Pop3 pc
, int bFlushCache
)
704 /* see if we already have a password, as provided in the config file, or
705 already requested from the user. */
706 if (PCU
.interactive_password
) {
707 if (strlen(PCU
.password
) == 0) {
708 /* we need to grab the password from the user. */
710 IMAP_DM(pc
, DEBUG_INFO
, "asking for password %d\n",
713 passwordFor(PCU
.userName
, PCU
.serverName
, pc
, bFlushCache
);
714 if (password
!= NULL
) {
715 if (strlen(password
) + 1 > BUF_SMALL
) {
716 DMA(DEBUG_ERROR
, "Password is too long.\n");
717 memset(PCU
.password
, 0, BUF_SMALL
- 1);
719 strncpy(PCU
.password
, password
, BUF_SMALL
- 1);
720 PCU
.password_len
= strlen(PCU
.password
);
723 ENFROB(PCU
.password
);