/* rewrite of the IMAP code by Neil Spring * (nspring@cs.washington.edu) to support gnutls and * persistent connections to servers. */ /* Originally written by Yong-iL Joh (tolkien@mizi.com), * modified by Jorge Garcia (Jorge.Garcia@uv.es), and * modified by Jay Francis (jtf@u880.org) to support * CRAM-MD5 */ #ifdef HAVE_CONFIG_H #include #endif #include "Client.h" #include "charutil.h" #include "tlsComm.h" #include "passwordMgr.h" #include "regulo.h" #include "MessageList.h" #include #include #include #include #include #include #include #include #ifdef USE_DMALLOC #include #endif #define PCU (pc->u).pop_imap extern int Relax; #define IMAP_DM(pc, lvl, args...) DM(pc, lvl, "imap4: " args) #ifdef HAVE_MEMFROB #define DEFROB(x) memfrob(x, x ## _len) #define ENFROB(x) memfrob(x, x ## _len) #else #define DEFROB(x) #define ENFROB(x) #endif /* this array maps server:port pairs to file descriptors, so that when more than one mailbox is queried from a server, we only use one socket. It's limited in size by the number of different mailboxes displayed. */ #define FDMAP_SIZE 5 static struct fdmap_struct { char *user_server_port; /* tuple, in string form */ /*@owned@ */ struct connection_state *cs; } fdmap[FDMAP_SIZE]; static void ask_user_for_password( /*@notnull@ */ Pop3 pc, int bFlushCache); /* authentication callbacks */ #ifdef HAVE_GCRYPT_H static int authenticate_md5( /*@notnull@ */ Pop3 pc, struct connection_state *scs, const char *capabilities); #endif static int authenticate_plaintext( /*@notnull@ */ Pop3 pc, struct connection_state *scs, const char *capabilities); /* the auth_methods array maps authentication identifiers to the callback that will attempt to authenticate */ static struct imap_authentication_method { const char *name; /* callback returns 1 if successful, 0 if failed */ int (*auth_callback) ( /*@notnull@ */ Pop3 pc, struct connection_state * scs, const char *capabilities); } auth_methods[] = { { #ifdef HAVE_GCRYPT_H "cram-md5", authenticate_md5}, { #endif "plaintext", authenticate_plaintext}, { NULL, NULL} }; /* recover a socket from the connection cache */ /*@null@*/ /*@dependent@*/ static struct connection_state *state_for_pcu(Pop3 pc) { char *connection_id; struct connection_state *retval = NULL; int i; connection_id = malloc(strlen(PCU.userName) + strlen(PCU.serverName) + 22); sprintf(connection_id, "%s|%s|%d", PCU.userName, PCU.serverName, PCU.serverPort); for (i = 0; i < FDMAP_SIZE; i++) if (fdmap[i].user_server_port != NULL && (strcmp(connection_id, fdmap[i].user_server_port) == 0)) { retval = fdmap[i].cs; } free(connection_id); return (retval); } /* bind to the connection cache */ static void bind_state_to_pcu(Pop3 pc, /*@owned@ */ struct connection_state *scs) { char *connection_id; int i; if (scs == NULL) { abort(); } connection_id = malloc(strlen(PCU.userName) + strlen(PCU.serverName) + 22); sprintf(connection_id, "%s|%s|%d", PCU.userName, PCU.serverName, PCU.serverPort); for (i = 0; i < FDMAP_SIZE && fdmap[i].cs != NULL; i++); if (i == FDMAP_SIZE) { /* should never happen */ IMAP_DM(pc, DEBUG_ERROR, "Tried to open too many IMAP connections. Sorry!\n"); exit(EXIT_FAILURE); } fdmap[i].user_server_port = connection_id; fdmap[i].cs = scs; } /* remove from the connection cache */ static /*@owned@*/ /*@null@*/ struct connection_state *unbind( /*@returned@*/ struct connection_state *scs) { int i; struct connection_state *retval = NULL; assert(scs != NULL); for (i = 0; i < FDMAP_SIZE && fdmap[i].cs != scs; i++); if (i < FDMAP_SIZE) { free(fdmap[i].user_server_port); fdmap[i].user_server_port = NULL; retval = fdmap[i].cs; fdmap[i].cs = NULL; } return (retval); } /* creates a connection to the server, if a matching one doesn't exist. */ /* *always* returns null, just declared this wasy to match other protocols. */ /*@null@*/ FILE *imap_open(Pop3 pc) { static int complained_already; /* we have to succeed once before complaining again about failure */ struct connection_state *scs; struct imap_authentication_method *a; char *connection_name; int sd; char capabilities[BUF_SIZE]; char buf[BUF_SIZE]; if (state_for_pcu(pc) != NULL) { /* don't need to open. */ return NULL; } /* got this far; we're going to create a connection_state structure, although it might be a blacklist entry */ connection_name = malloc(strlen(PCU.serverName) + 20); sprintf(connection_name, "%s:%d", PCU.serverName, PCU.serverPort); assert(pc != NULL); /* no cached connection */ sd = sock_connect((const char *) PCU.serverName, PCU.serverPort); if (sd == -1) { if (complained_already == 0) { IMAP_DM(pc, DEBUG_ERROR, "Couldn't connect to %s:%d: %s\n", PCU.serverName, PCU.serverPort, errno ? strerror(errno) : ""); complained_already = 1; } if (errno == ETIMEDOUT) { /* temporarily bump the interval, in a crude way: fast forward time so that the mailbox isn't checked for a while. */ pc->prevtime = time(0) + 60 * 5; /* now + 60 seconds per min * 5 minutes */ /* TCP's retry (how much time has elapsed while the connect times out) is around 3 minutes; here we just try to allow checking local mailboxes more often while remote things are unavailable or disconnected. */ } return NULL; } /* build the connection using STARTTLS */ if (PCU.dossl != 0 && (PCU.serverPort == 143)) { /* setup an unencrypted binding long enough to invoke STARTTLS */ scs = initialize_unencrypted(sd, connection_name, pc); /* can we? */ tlscomm_printf(scs, "a000 CAPABILITY\r\n"); if (tlscomm_expect(scs, "* CAPABILITY", capabilities, BUF_SIZE) == 0) goto communication_failure; if (!strstr(capabilities, "STARTTLS")) { IMAP_DM(pc, DEBUG_ERROR, "server doesn't support ssl imap on port 143."); goto communication_failure; } /* we sure can! */ IMAP_DM(pc, DEBUG_INFO, "Negotiating TLS within IMAP"); tlscomm_printf(scs, "a001 STARTTLS\r\n"); if (tlscomm_expect(scs, "a001 ", buf, BUF_SIZE) == 0) goto communication_failure; if (strstr(buf, "a001 OK") == 0) { /* we didn't see the success message in the response */ IMAP_DM(pc, DEBUG_ERROR, "couldn't negotiate tls. :(\n"); goto communication_failure; } /* we don't need the unencrypted state anymore */ /* note that communication_failure will close the socket and free via tls_close() */ free(scs); /* fall through will scs = initialize_gnutls(sd); */ } /* either we've negotiated ssl from starttls, or we're starting an encrypted connection now */ if (PCU.dossl != 0) { scs = initialize_gnutls(sd, connection_name, pc, PCU.serverName); if (scs == NULL) { IMAP_DM(pc, DEBUG_ERROR, "Failed to initialize TLS\n"); return NULL; } } else { scs = initialize_unencrypted(sd, connection_name, pc); } /* authenticate; first find out how */ /* note that capabilities may have changed since last time we may have asked, if we called STARTTLS, my server will allow plain password login within an encrypted session. */ tlscomm_printf(scs, "a000 CAPABILITY\r\n"); if (tlscomm_expect(scs, "* CAPABILITY", capabilities, BUF_SIZE) == 0) { IMAP_DM(pc, DEBUG_ERROR, "unable to query capability string"); goto communication_failure; } /* try each authentication method in turn. */ for (a = auth_methods; a->name != NULL; a++) { /* was it specified or did the user leave it up to us? */ if (PCU.authList[0] == '\0' || strstr(PCU.authList, a->name) != NULL) /* try the authentication method */ if ((a->auth_callback(pc, scs, capabilities)) != 0) { /* store this well setup connection in the cache */ bind_state_to_pcu(pc, scs); complained_already = 0; return NULL; } } /* if authentication worked, we won't get here */ IMAP_DM(pc, DEBUG_ERROR, "All authentication methods failed for '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); communication_failure: tlscomm_printf(scs, "a002 LOGOUT\r\n"); tlscomm_close(scs); return NULL; } void imap_cacheHeaders( /*@notnull@ */ Pop3 pc); int imap_checkmail( /*@notnull@ */ Pop3 pc) { /* recover connection state from the cache */ struct connection_state *scs = state_for_pcu(pc); char buf[BUF_SIZE]; static int command_id; /* if it's not in the cache, try to open */ if (scs == NULL) { IMAP_DM(pc, DEBUG_INFO, "Need new connection to %s@%s\n", PCU.userName, PCU.serverName); (void) imap_open(pc); scs = state_for_pcu(pc); } if (scs == NULL) { return -1; } if (tlscomm_is_blacklisted(scs) != 0) { /* unresponsive server, don't bother. */ return -1; } /* if we've got it by now, try the status query */ command_id++; tlscomm_printf(scs, "a%03d STATUS %s (MESSAGES UNSEEN)\r\n", command_id % 1000, pc->path); if (tlscomm_expect(scs, "* STATUS", buf, 127) != 0) { /* a valid response? */ // doesn't support spaces: (void) sscanf(buf, "* STATUS %*s (MESSAGES %d UNSEEN %d)", const char *msg; msg = strstr(buf, "(MESSAGES"); if (msg != NULL) (void) sscanf(msg, "(MESSAGES %d UNSEEN %d)", &(pc->TotalMsgs), &(pc->UnreadMsgs)); /* update the cached headers if evidence that change has occurred; not necessarily complete. */ if (pc->UnreadMsgs != pc->OldUnreadMsgs || pc->TotalMsgs != pc->OldMsgs) { if (PCU.wantCacheHeaders) { imap_cacheHeaders(pc); } } } else { /* something went wrong. bail. */ tlscomm_close(unbind(scs)); return -1; } return 0; } void imap_releaseHeaders(Pop3 pc __attribute__ ((unused)), struct msglst *h) { assert(h != NULL); /* allow the list to be released next time around */ if (h->in_use <= 0) { /* free the old one */ while (h != NULL) { struct msglst *n = h->next; free(h); h = n; } } else { h->in_use--; } } void imap_cacheHeaders( /*@notnull@ */ Pop3 pc) { struct connection_state *scs = state_for_pcu(pc); char *msgid; char buf[BUF_SIZE]; if (scs == NULL) { (void) imap_open(pc); scs = state_for_pcu(pc); } if (scs == NULL) { return; } if (tlscomm_is_blacklisted(scs) != 0) { return; } if (pc->headerCache != NULL) { /* decrement the reference count, and free our version */ imap_releaseHeaders(pc, pc->headerCache); pc->headerCache = NULL; } IMAP_DM(pc, DEBUG_INFO, "working headers\n"); tlscomm_printf(scs, "a004 EXAMINE %s\r\n", pc->path); if (tlscomm_expect(scs, "a004 OK", buf, 127) == 0) { tlscomm_close(unbind(scs)); return; } IMAP_DM(pc, DEBUG_INFO, "examine ok\n"); /* if we've got it by now, try the status query */ tlscomm_printf(scs, "a005 SEARCH UNSEEN\r\n"); if (tlscomm_expect(scs, "* SEARCH", buf, 127) == 0) { tlscomm_close(unbind(scs)); return; } IMAP_DM(pc, DEBUG_INFO, "search: %s", buf); if (strlen(buf) < 9) return; /* search turned up nothing */ msgid = strtok(buf + 9, " \r\n"); pc->headerCache = NULL; /* the isdigit cruft is to deal with EOL */ if (msgid != NULL && isdigit(msgid[0])) do { struct msglst *m = malloc(sizeof(struct msglst)); char hdrbuf[128]; int fetch_command_done = FALSE; tlscomm_printf(scs, "a04 FETCH %s (FLAGS " "BODY[HEADER.FIELDS (FROM SUBJECT)])\r\n", msgid); if (tlscomm_expect(scs, "* ", hdrbuf, 127)) { m->subj[0] = '\0'; m->from[0] = '\0'; while (m->subj[0] == '\0' || m->from[0] == '\0') { if (tlscomm_expect(scs, "", hdrbuf, 127)) { if (strncasecmp(hdrbuf, "Subject:", 8) == 0) { strncpy(m->subj, hdrbuf + 9, SUBJ_LEN - 1); m->subj[SUBJ_LEN - 1] = '\0'; } else if (strncasecmp(hdrbuf, "From: ", 5) == 0) { strncpy(m->from, hdrbuf + 6, FROM_LEN - 1); m->from[FROM_LEN - 1] = '\0'; } else if (strncasecmp (hdrbuf, "a04 OK FETCH", 5) == 0) { /* server says we're done getting this header, which may occur if the message has no subject */ if (m->from[0] == '\0') { strcpy(m->from, " "); } if (m->subj[0] == '\0') { strcpy(m->subj, "(no subject)"); } fetch_command_done = TRUE; } } else { IMAP_DM(pc, DEBUG_ERROR, "timedout looking for headers.: %s", hdrbuf); strcpy(m->from, "wmbiff"); strcpy(m->subj, "failure"); } } IMAP_DM(pc, DEBUG_INFO, "From: '%s' Subj: '%s'\n", m->from, m->subj); m->next = pc->headerCache; pc->headerCache = m; pc->headerCache->in_use = 0; /* initialize that it isn't locked */ } else { IMAP_DM(pc, DEBUG_ERROR, "error fetching: %s", hdrbuf); } if (!fetch_command_done) { tlscomm_expect(scs, "a04 OK", hdrbuf, 127); } } while ((msgid = strtok(NULL, " \r\n")) != NULL && isdigit(msgid[0])); tlscomm_printf(scs, "a06 CLOSE\r\n"); /* return to polling state */ /* may be unneeded tlscomm_expect(scs, "a06 OK CLOSE\r\n" ); see if it worked? */ IMAP_DM(pc, DEBUG_INFO, "worked headers\n"); } /* a client is asking for the headers, hand em a reference, increase the one-bit reference counter */ struct msglst *imap_getHeaders( /*@notnull@ */ Pop3 pc) { if (pc->headerCache == NULL) imap_cacheHeaders(pc); if (pc->headerCache != NULL) pc->headerCache->in_use = 1; return pc->headerCache; } /* parse the config line to setup the Pop3 structure */ int imap4Create( /*@notnull@ */ Pop3 pc, const char *const str) { int i; int matchedchars; /* special characters aren't allowed in hostnames, rfc 1034 */ const char *regexes[] = { // type : username : password @ hostname (/ name)?(:port)? ".*imaps?:([^: ]{1,32}):([^@]{0,32})@([A-Za-z1-9][-A-Za-z0-9_.]+)(/(\"[^\"]+\")|([^:@ ]+))?(:[0-9]+)?( *([CcAaPp][-A-Za-z5 ]*))?$", ".*imaps?:([^: ]{1,32}) ([^ ]{1,32}) ([A-Za-z1-9][-A-Za-z0-9_.]+)(/(\"[^\"]+\")|([^: ]+))?( [0-9]+)?( *([CcAaPp][-A-Za-z5 ]*))?$", NULL }; char *unaliased_str; struct regulo regulos[] = { {1, PCU.userName, regulo_strcpy}, {2, PCU.password, regulo_strcpy}, {3, PCU.serverName, regulo_strcpy}, {4, pc->path, regulo_strcpy_skip1}, {7, &PCU.serverPort, regulo_atoi}, {9, PCU.authList, regulo_strcpy_tolower}, {0, NULL, NULL} }; if (Relax) { regexes[0] = ".*imaps?:([^: ]{1,32}):([^@]{0,32})@([^/: ]+)(/(\"[^\"]+\")|([^:@ ]+))?(:[0-9]+)?( *(.*))?$"; regexes[1] = ".*imaps?:([^: ]{1,32}) ([^ ]{1,32}) ([^/: ]+)(/(\"[^\"]+\")|([^: ]+))?( [0-9]+)?( *(.*))?$"; } /* IMAP4 format: imap:user:password@server/mailbox[:port] */ /* If 'str' line is badly formatted, wmbiff won't display the mailbox. */ if (strncmp("sslimap:", str, 8) == 0 || strncmp("imaps:", str, 6) == 0) { #ifdef HAVE_GNUTLS_GNUTLS_H static int haveBeenWarned; PCU.dossl = 1; if (!haveBeenWarned) { printf("wmbiff uses gnutls for TLS/SSL encryption support:\n" " If you distribute software that uses gnutls, don't forget\n" " to warn the users of your software that gnutls is at a\n" " testing phase and may be totally insecure.\n" "\nConsider yourself warned.\n"); haveBeenWarned = 1; } #else printf("This copy of wmbiff was not compiled with gnutls;\n" "imaps is unavailable. Exiting to protect your\n" "passwords and privacy.\n"); exit(EXIT_FAILURE); #endif } else { PCU.dossl = 0; } /* defaults */ PCU.serverPort = (PCU.dossl != 0) ? 993 : 143; PCU.authList[0] = '\0'; /* argh, str and pc->path are aliases, so we can't just write the default value into the string we're about to parse. */ unaliased_str = strdup(str); strcpy(pc->path, "INBOX"); for (matchedchars = 0, i = 0; regexes[i] != NULL && matchedchars <= 0; i++) { matchedchars = regulo_match(regexes[i], unaliased_str, regulos); } /* failed to match either regex */ if (matchedchars <= 0) { pc->label[0] = '\0'; IMAP_DM(pc, DEBUG_ERROR, "Couldn't parse line %s (%d)\n" " If this used to work, run wmbiff with the -relax option, and\n" " send mail to wmbiff-devel@lists.sourceforge.net with the hostname\n" " of your mail server.\n", unaliased_str, matchedchars); return -1; } PCU.password_len = strlen(PCU.password); if (PCU.password[0] == '\0') { PCU.interactive_password = 1; } else { ENFROB(PCU.password); } // grab_authList(unaliased_str + matchedchars, PCU.authList); free(unaliased_str); IMAP_DM(pc, DEBUG_INFO, "userName= '%s'\n", PCU.userName); IMAP_DM(pc, DEBUG_INFO, "password is %d characters long\n", (int) PCU.password_len); IMAP_DM(pc, DEBUG_INFO, "serverName= '%s'\n", PCU.serverName); IMAP_DM(pc, DEBUG_INFO, "serverPath= '%s'\n", pc->path); IMAP_DM(pc, DEBUG_INFO, "serverPort= '%d'\n", PCU.serverPort); IMAP_DM(pc, DEBUG_INFO, "authList= '%s'\n", PCU.authList); if (strcmp(pc->action, "msglst") == 0 || strcmp(pc->fetchcmd, "msglst") == 0 || strcmp(pc->button2, "msglst") == 0) { PCU.wantCacheHeaders = 1; } else { PCU.wantCacheHeaders = 0; } pc->checkMail = imap_checkmail; pc->getHeaders = imap_getHeaders; pc->releaseHeaders = imap_releaseHeaders; pc->TotalMsgs = 0; pc->UnreadMsgs = 0; pc->OldMsgs = -1; pc->OldUnreadMsgs = -1; return 0; } static int authenticate_plaintext( /*@notnull@ */ Pop3 pc, struct connection_state *scs, const char *capabilities) { char buf[BUF_SIZE]; /* is login prohibited? */ /* "An IMAP client which complies with [rfc2525, section 3.2] * MUST NOT issue the LOGIN command if this capability is present. */ if (strstr(capabilities, "LOGINDISABLED")) { IMAP_DM(pc, DEBUG_ERROR, "Plaintext auth prohibited by server: (LOGINDISABLED).\n"); goto plaintext_failed; } ask_user_for_password(pc, 0); do { /* login */ DEFROB(PCU.password); tlscomm_printf(scs, "a001 LOGIN %s \"%s\"\r\n", PCU.userName, PCU.password); ENFROB(PCU.password); if (tlscomm_expect(scs, "a001 ", buf, BUF_SIZE) == 0) { IMAP_DM(pc, DEBUG_ERROR, "Did not get a response to the LOGIN command.\n"); goto plaintext_failed; } if (buf[5] != 'O') { IMAP_DM(pc, DEBUG_ERROR, "IMAP Login failed: %s\n", buf); /* if we're prompting the user, ask again, else fail */ if (PCU.interactive_password) { PCU.password[0] = '\0'; ask_user_for_password(pc, 1); /* 1=overwrite the cache */ } else { goto plaintext_failed; } } else { return (1); } } while (1); plaintext_failed: return (0); } #ifdef HAVE_GCRYPT_H static int authenticate_md5(Pop3 pc, struct connection_state *scs, const char *capabilities) { char buf[BUF_SIZE]; char buf2[BUF_SIZE]; unsigned char *md5; gcry_md_hd_t gmh; gcry_error_t rc; if (!strstr(capabilities, "AUTH=CRAM-MD5")) { /* server doesn't support cram-md5. */ return 0; } tlscomm_printf(scs, "a007 AUTHENTICATE CRAM-MD5\r\n"); if (tlscomm_expect(scs, "+ ", buf, BUF_SIZE) == 0) goto expect_failure; Decode_Base64(buf + 2, buf2); IMAP_DM(pc, DEBUG_INFO, "CRAM-MD5 challenge: %s\n", buf2); strcpy(buf, PCU.userName); strcat(buf, " "); ask_user_for_password(pc, 0); rc = gcry_md_open(&gmh, GCRY_MD_MD5, GCRY_MD_FLAG_HMAC); if (rc != 0) { IMAP_DM(pc, DEBUG_INFO, "unable to initialize gcrypt md5\n"); return 0; } DEFROB(PCU.password); gcry_md_setkey(gmh, PCU.password, strlen(PCU.password)); ENFROB(PCU.password); gcry_md_write(gmh, (unsigned char *) buf2, strlen(buf2)); gcry_md_final(gmh); md5 = gcry_md_read(gmh, 0); Bin2Hex(md5, 16, buf2); gcry_md_close(gmh); strcat(buf, buf2); IMAP_DM(pc, DEBUG_INFO, "CRAM-MD5 response: %s\n", buf); Encode_Base64(buf, buf2); tlscomm_printf(scs, "%s\r\n", buf2); if (tlscomm_expect(scs, "a007 ", buf, BUF_SIZE) == 0) goto expect_failure; if (!strncmp(buf, "a007 OK", 7)) return 1; /* AUTH successful */ IMAP_DM(pc, DEBUG_ERROR, "CRAM-MD5 AUTH failed for user '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); IMAP_DM(pc, DEBUG_INFO, "It said %s", buf); return 0; expect_failure: IMAP_DM(pc, DEBUG_ERROR, "tlscomm_expect failed during cram-md5 auth: %s", buf); IMAP_DM(pc, DEBUG_ERROR, "failed to authenticate using cram-md5."); return 0; } #endif static void ask_user_for_password( /*@notnull@ */ Pop3 pc, int bFlushCache) { /* see if we already have a password, as provided in the config file, or already requested from the user. */ if (PCU.interactive_password) { if (strlen(PCU.password) == 0) { /* we need to grab the password from the user. */ char *password; IMAP_DM(pc, DEBUG_INFO, "asking for password %d\n", bFlushCache); password = passwordFor(PCU.userName, PCU.serverName, pc, bFlushCache); if (password != NULL) { if (strlen(password) + 1 > BUF_SMALL) { DMA(DEBUG_ERROR, "Password is too long.\n"); memset(PCU.password, 0, BUF_SMALL - 1); } else { strncpy(PCU.password, password, BUF_SMALL - 1); PCU.password_len = strlen(PCU.password); } free(password); ENFROB(PCU.password); } } } } /* vim:set ts=4: */ /* * Local Variables: * tab-width: 4 * c-indent-level: 4 * c-basic-offset: 4 * End: */