/* $Id: Pop3Client.c,v 1.23 2004/12/12 00:01:53 bluehal Exp $ */ /* Author : Scott Holden ( scotth@thezone.net ) Modified : Yong-iL Joh ( tolkien@mizi.com ) Modified : Jorge García ( Jorge.Garcia@uv.es ) Modified ; Mark Hurley ( debian4tux@telocity.com ) Modified : Neil Spring ( nspring@cs.washington.edu ) * * Pop3 Email checker. * * Last Updated : Tue Nov 13 13:45:23 PST 2001 * */ #ifdef HAVE_CONFIG_H #include #endif #include "Client.h" #include "charutil.h" #include "regulo.h" #include "MessageList.h" #include #include "tlsComm.h" #include "passwordMgr.h" #ifdef USE_DMALLOC #include #endif extern int Relax; /* temp */ static void ask_user_for_password( /*@notnull@ */ Pop3 pc, int bFlushCache); #define PCU (pc->u).pop_imap #define POP_DM(pc, lvl, args...) DM(pc, lvl, "pop3: " args) #ifdef HAVE_GCRYPT_H static struct connection_state *authenticate_md5( /*@notnull@ */ Pop3 pc, struct connection_state * scs, char *unused); static struct connection_state *authenticate_apop( /*@notnull@ */ Pop3 pc, struct connection_state * scs, char *apop_str); #endif static struct connection_state *authenticate_plaintext( /*@notnull@ */ Pop3 pc, struct connection_state * scs, char *unused); void pop3_cacheHeaders( /*@notnull@ */ Pop3 pc); extern void imap_releaseHeaders(Pop3 pc __attribute__ ((unused)), struct msglst *h); extern struct connection_state *state_for_pcu(Pop3 pc); static struct authentication_method { const char *name; /* callback returns the connection state pointer if successful, NULL if failed */ struct connection_state *(*auth_callback) (Pop3 pc, struct connection_state * scs, char *apop_str); } auth_methods[] = { { #ifdef HAVE_GCRYPT_H "cram-md5", authenticate_md5}, { "apop", authenticate_apop}, { #endif "plaintext", authenticate_plaintext}, { NULL, NULL} }; /*@null@*/ struct connection_state *pop3Login(Pop3 pc) { int fd; char buf[BUF_SIZE]; char apop_str[BUF_SIZE]; char *ptr1, *ptr2; struct authentication_method *a; struct connection_state *scs; char *connection_name; apop_str[0] = '\0'; /* if defined, server supports apop */ if ((fd = sock_connect(PCU.serverName, PCU.serverPort)) == -1) { POP_DM(pc, DEBUG_ERROR, "Not Connected To Server '%s:%d'\n", PCU.serverName, PCU.serverPort); return NULL; } connection_name = malloc(strlen(PCU.serverName) + 20); sprintf(connection_name, "%s:%d", PCU.serverName, PCU.serverPort); if (PCU.dossl != 0) { scs = initialize_gnutls(fd, connection_name, pc, PCU.serverName); if (scs == NULL) { POP_DM(pc, DEBUG_ERROR, "Failed to initialize TLS\n"); return NULL; } } else { scs = initialize_unencrypted(fd, connection_name, pc); } tlscomm_gets(buf, BUF_SIZE, scs); POP_DM(pc, DEBUG_INFO, "%s", buf); /* Detect APOP, copy challenge into apop_str */ for (ptr1 = buf + strlen(buf), ptr2 = NULL; ptr1 > buf; --ptr1) { if (*ptr1 == '>') { ptr2 = ptr1; } else if (*ptr1 == '<') { if (ptr2) { *(ptr2 + 1) = 0; strncpy(apop_str, ptr1, BUF_SIZE); } break; } } /* 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)) /* did it work? */ if ((a->auth_callback(pc, scs, apop_str)) != NULL) return (scs); } /* if authentication worked, we won't get here */ POP_DM(pc, DEBUG_ERROR, "All Pop3 authentication methods failed for '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); tlscomm_printf(scs, "QUIT\r\n"); tlscomm_close(scs); return NULL; } int pop3CheckMail( /*@notnull@ */ Pop3 pc) { struct connection_state *scs; int read; char buf[BUF_SIZE]; scs = pop3Login(pc); if (scs == NULL) return -1; tlscomm_printf(scs, "STAT\r\n"); if( ! tlscomm_expect(scs, "+", buf, BUF_SIZE) ) { POP_DM(pc, DEBUG_ERROR, "Error Receiving Stats '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); POP_DM(pc, DEBUG_INFO, "It said: %s\n", buf); return -1; } else { sscanf(buf, "+OK %d", &(pc->TotalMsgs)); } /* - Updated - Mark Hurley - debian4tux@telocity.com * In compliance with RFC 1725 * which removed the LAST command, any servers * which follow this spec will return: * -ERR unimplimented * We will leave it here for those servers which haven't * caught up with the spec. */ tlscomm_printf(scs, "LAST\r\n"); tlscomm_gets(buf, BUF_SIZE, scs); if (buf[0] != '+') { /* it is not an error to receive this according to RFC 1725 */ /* no error should be returned */ pc->UnreadMsgs = pc->TotalMsgs; // there's also a LIST command... not sure how to make use of it. */ } else { sscanf(buf, "+OK %d", &read); pc->UnreadMsgs = pc->TotalMsgs - read; } tlscomm_printf(scs, "QUIT\r\n"); tlscomm_close(scs); return 0; } struct msglst *pop_getHeaders( /*@notnull@ */ Pop3 pc) { if (pc->headerCache == NULL) pop3_cacheHeaders(pc); if (pc->headerCache != NULL) pc->headerCache->in_use = 1; return pc->headerCache; } int pop3Create(Pop3 pc, const char *str) { /* POP3 format: pop3:user:password@server[:port] */ /* new POP3 format: pop3:user password server [port] */ /* If 'str' line is badly formatted, wmbiff won't display the mailbox. */ int i; int matchedchars; /* ([^: ]+) user ([^@]+) or ([^ ]+) password ([^: ]+) server ([: ][0-9]+)? optional port ' *' gobbles trailing whitespace before authentication types. use separate regexes for old and new types to permit use of '@' in passwords */ const char *regexes[] = { "pop3s?:([^: ]{1,32}):([^@]{0,32})@([A-Za-z1-9][-A-Za-z0-9_.]+)(:[0-9]+)?( *([CcAaPp][-A-Za-z5 ]*))?$", "pop3s?:([^: ]{1,32}) ([^ ]{1,32}) ([A-Za-z1-9][-A-Za-z0-9_.]+)( [0-9]+)?( *([CcAaPp][-A-Za-z5 ]*))?$", // "pop3:([^: ]{1,32}) ([^ ]{1,32}) ([^: ]+)( [0-9]+)? *", // "pop3:([^: ]{1,32}):([^@]{0,32})@([^: ]+)(:[0-9]+)? *", NULL }; struct regulo regulos[] = { {1, PCU.userName, regulo_strcpy}, {2, PCU.password, regulo_strcpy}, {3, PCU.serverName, regulo_strcpy}, {4, &PCU.serverPort, regulo_atoi}, {6, PCU.authList, regulo_strcpy_tolower}, {0, NULL, NULL} }; if (Relax) { regexes[0] = "pop3:([^: ]{1,32}):([^@]{0,32})@([^/: ]+)(:[0-9]+)?( *(.*))?$"; regexes[1] = "pop3:([^: ]{1,32}) ([^ ]{1,32}) ([^/: ]+)( [0-9]+)?( *(.*))?$"; } if (strncmp("pop3s:", str, 6) == 0) { #ifdef HAVE_GNUTLS_GNUTLS_H PCU.dossl = 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) ? 995 : 110; PCU.authList[0] = '\0'; for (matchedchars = 0, i = 0; regexes[i] != NULL && matchedchars <= 0; i++) { matchedchars = regulo_match(regexes[i], str, regulos); } /* failed to match either regex */ if (matchedchars <= 0) { pc->label[0] = '\0'; POP_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", str, matchedchars); return -1; } // grab_authList(str + matchedchars, PCU.authList); PCU.password_len = strlen(PCU.password); if (PCU.password[0] == '\0') { PCU.interactive_password = 1; } else { // ENFROB(PCU.password); } POP_DM(pc, DEBUG_INFO, "userName= '%s'\n", PCU.userName); POP_DM(pc, DEBUG_INFO, "password is %ld chars long\n", strlen(PCU.password)); POP_DM(pc, DEBUG_INFO, "serverName= '%s'\n", PCU.serverName); POP_DM(pc, DEBUG_INFO, "serverPort= '%d'\n", PCU.serverPort); POP_DM(pc, DEBUG_INFO, "authList= '%s'\n", PCU.authList); pc->checkMail = pop3CheckMail; pc->getHeaders = pop_getHeaders; pc->TotalMsgs = 0; pc->UnreadMsgs = 0; pc->OldMsgs = -1; pc->OldUnreadMsgs = -1; return 0; } #ifdef HAVE_GCRYPT_H static struct connection_state *authenticate_md5(Pop3 pc, struct connection_state * scs, char *apop_str __attribute__ ((unused))) { char buf[BUF_SIZE]; char buf2[BUF_SIZE]; unsigned char *md5; gcry_md_hd_t gmh; gcry_error_t rc; /* See if MD5 is supported */ tlscomm_printf(scs, "AUTH CRAM-MD5\r\n"); tlscomm_gets(buf, BUF_SIZE, scs); POP_DM(pc, DEBUG_INFO, "%s", buf); if (buf[0] != '+' || buf[1] != ' ') { /* nope, not supported. */ return NULL; } Decode_Base64(buf + 2, buf2); POP_DM(pc, DEBUG_INFO, "CRAM-MD5 challenge: %s\n", buf2); strcpy(buf, PCU.userName); strcat(buf, " "); rc = gcry_md_open(&gmh, GCRY_MD_MD5, GCRY_MD_FLAG_HMAC); if (rc != 0) { POP_DM(pc, DEBUG_ERROR, "unable to initialize gcrypt md5.\n"); return NULL; } gcry_md_setkey(gmh, PCU.password, strlen(PCU.password)); gcry_md_write(gmh, (unsigned char *) buf2, strlen(buf2)); gcry_md_final(gmh); md5 = gcry_md_read(gmh, 0); /* hmac_md5(buf2, strlen(buf2), PCU.password, strlen(PCU.password), md5); */ Bin2Hex(md5, 16, buf2); gcry_md_close(gmh); strcat(buf, buf2); POP_DM(pc, DEBUG_INFO, "CRAM-MD5 response: %s\n", buf); Encode_Base64(buf, buf2); tlscomm_printf(scs, "%s\r\n", buf2); tlscomm_gets(buf, BUF_SIZE, scs); if (!strncmp(buf, "+OK", 3)) return scs; /* AUTH successful */ else { POP_DM(pc, DEBUG_ERROR, "CRAM-MD5 AUTH failed for user '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); fprintf(stderr, "It said %s", buf); return NULL; } } static struct connection_state *authenticate_apop(Pop3 pc, struct connection_state * scs, char *apop_str) { gcry_md_hd_t gmh; gcry_error_t rc; char buf[BUF_SIZE]; unsigned char *md5; if (apop_str[0] == '\0') { /* server doesn't support apop. */ return (NULL); } POP_DM(pc, DEBUG_INFO, "APOP challenge: %s\n", apop_str); strcat(apop_str, PCU.password); rc = gcry_md_open(&gmh, GCRY_MD_MD5, 0); if (rc != 0) { POP_DM(pc, DEBUG_ERROR, "unable to initialize gcrypt md5.\n"); return NULL; } gcry_md_write(gmh, (unsigned char *) apop_str, strlen(apop_str)); gcry_md_final(gmh); md5 = gcry_md_read(gmh, 0); Bin2Hex(md5, 16, buf); gcry_md_close(gmh); POP_DM(pc, DEBUG_INFO, "APOP response: %s %s\n", PCU.userName, buf); tlscomm_printf(scs, "APOP %s %s\r\n", PCU.userName, buf); tlscomm_gets(buf, BUF_SIZE, scs); if (!strncmp(buf, "+OK", 3)) return scs; /* AUTH successful */ else { POP_DM(pc, DEBUG_ERROR, "APOP AUTH failed for user '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); POP_DM(pc, DEBUG_INFO, "It said %s", buf); return NULL; } } #endif /* HAVE_GCRYPT_H */ /*@null@*/ static struct connection_state *authenticate_plaintext( /*@notnull@ */ Pop3 pc, struct connection_state * scs, char *apop_str __attribute__ ((unused))) { char buf[BUF_SIZE]; tlscomm_printf(scs, "USER %s\r\n", PCU.userName); if (tlscomm_gets(buf, BUF_SIZE, scs) == 0) { POP_DM(pc, DEBUG_ERROR, "Error reading from server authenticating '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); return NULL; } if (buf[0] != '+') { POP_DM(pc, DEBUG_ERROR, "Failed user name when authenticating '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); /* deb #128863 might be easier if we printed: */ POP_DM(pc, DEBUG_ERROR, "The server's error message was: %s\n", buf); return NULL; }; tlscomm_printf(scs, "PASS %s\r\n", PCU.password); if (tlscomm_gets(buf, BUF_SIZE, scs) == 0) { POP_DM(pc, DEBUG_ERROR, "Error reading from server (2) authenticating '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); return NULL; } if (strncmp(buf, "-ERR [AUTH] Password required", 20) == 0) { if (PCU.interactive_password) { PCU.password[0] = '\0'; ask_user_for_password(pc, 1); /* 1=overwrite the cache */ tlscomm_printf(scs, "PASS %s\r\n", PCU.password); if (tlscomm_gets(buf, BUF_SIZE, scs) == 0) { POP_DM(pc, DEBUG_ERROR, "Error reading from server (2) authenticating '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); return NULL; } } } if (buf[0] != '+') { POP_DM(pc, DEBUG_ERROR, "Failed password when authenticating '%s@%s:%d'\n", PCU.userName, PCU.serverName, PCU.serverPort); POP_DM(pc, DEBUG_ERROR, "The server's error message was: %s\n", buf); return NULL; }; return scs; } void pop3_cacheHeaders( /*@notnull@ */ Pop3 pc) { char buf[BUF_SIZE]; struct connection_state *scs; int i; if (pc->headerCache != NULL) { /* decrement the reference count, and free our version */ imap_releaseHeaders(pc, pc->headerCache); pc->headerCache = NULL; } POP_DM(pc, DEBUG_INFO, "working headers\n"); /* login the server */ scs = pop3Login(pc); if (scs == NULL) return; /* pc->UnreadMsgs = pc->TotalMsgs - read; */ pc->headerCache = NULL; for (i = pc->TotalMsgs - pc->UnreadMsgs + 1; i <= pc->TotalMsgs; ++i) { struct msglst *m; m = malloc(sizeof(struct msglst)); m->subj[0] = '\0'; m->from[0] = '\0'; POP_DM(pc, DEBUG_INFO, "search: %s", buf); tlscomm_printf(scs, "TOP %i 0\r\n", i); while (tlscomm_gets(buf, 256, scs) && buf[0] != '.') { if (!strncasecmp(buf, "From: ", 6)) { /* manage the from in heads */ strncpy(m->from, buf + 6, FROM_LEN - 1); m->from[FROM_LEN - 1] = '\0'; } else if (!strncasecmp(buf, "Subject: ", 9)) { /* manage subject */ strncpy(m->subj, buf + 9, SUBJ_LEN - 1); m->subj[SUBJ_LEN - 1] = '\0'; } if (!m->subj[0]) { strncpy(m->subj, "[NO SUBJECT]", 14); } if (!m->from[0]) { strncpy(m->from, "[ANONYMOUS]", 14); } } m->next = pc->headerCache; pc->headerCache = m; pc->headerCache->in_use = 0; } tlscomm_printf(scs, "QUIT\r\n"); tlscomm_close(scs); } /* vim:set ts=4: */ 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; POP_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); } } } }