X-Git-Url: https://diplodocus.org/git/nmh/blobdiff_plain/352fe458a57061db81240c19fa4b356c7448463b..303e8387acecca26329e939f228f78ca805b7a15:/mts/smtp/smtp.c?ds=sidebyside diff --git a/mts/smtp/smtp.c b/mts/smtp/smtp.c index ab70c246..79c1e79b 100644 --- a/mts/smtp/smtp.c +++ b/mts/smtp/smtp.c @@ -1,8 +1,6 @@ /* * smtp.c -- nmh SMTP interface * - * $Id$ - * * This code is Copyright (c) 2002, by the authors of nmh. See the * COPYRIGHT file in the root directory of the nmh distribution for * complete copyright information. @@ -11,19 +9,31 @@ #include #include "smtp.h" #include -#include #include +#include +#include #ifdef CYRUS_SASL #include #include +# if SASL_VERSION_FULL < 0x020125 + /* Cyrus SASL 2.1.25 introduced the sasl_callback_ft prototype, + which has an explicit void parameter list, according to best + practice. So we need to cast to avoid compile warnings. + Provide this prototype for earlier versions. */ + typedef int (*sasl_callback_ft)(); +# endif /* SASL_VERSION_FULL < 0x020125 */ #include #include #include #include -#include #endif /* CYRUS_SASL */ +#ifdef TLS_SUPPORT +#include +#include +#endif /* TLS_SUPPORT */ + /* * This module implements an interface to SendMail very similar * to the MMDF mm_(3) routines. The sm_() routines herein talk @@ -79,56 +89,73 @@ static int sm_verbose = 0; static FILE *sm_rfp = NULL; static FILE *sm_wfp = NULL; +static int next_line_encoded = 0; + #ifdef CYRUS_SASL /* * Some globals needed by SASL */ static sasl_conn_t *conn = NULL; /* SASL connection state */ -static int sasl_complete = 0; /* Has authentication succeded? */ +static int sasl_complete = 0; /* Has authentication succeeded? */ static sasl_ssf_t sasl_ssf; /* Our security strength factor */ -static char *sasl_pw_context[2]; /* Context to pass into sm_get_pass */ static int maxoutbuf; /* Maximum crypto output buffer */ static char *sasl_outbuffer; /* SASL output buffer for encryption */ static int sasl_outbuflen; /* Current length of data in outbuf */ -static char *sasl_inbuffer; /* SASL input buffer for encryption */ -static char *sasl_inptr; /* Pointer to current inbuf position */ -static int sasl_inbuflen; /* Current length of data in inbuf */ static int sm_get_user(void *, int, const char **, unsigned *); static int sm_get_pass(sasl_conn_t *, void *, int, sasl_secret_t **); -static int sm_fgetc(FILE *); static sasl_callback_t callbacks[] = { - { SASL_CB_USER, sm_get_user, NULL }, + { SASL_CB_USER, (sasl_callback_ft) sm_get_user, NULL }, #define SM_SASL_N_CB_USER 0 - { SASL_CB_PASS, sm_get_pass, NULL }, -#define SM_SASL_N_CB_PASS 1 - { SASL_CB_AUTHNAME, sm_get_user, NULL }, -#define SM_SASL_N_CB_AUTHNAME 2 + { SASL_CB_AUTHNAME, (sasl_callback_ft) sm_get_user, NULL }, +#define SM_SASL_N_CB_AUTHNAME 1 + { SASL_CB_PASS, (sasl_callback_ft) sm_get_pass, NULL }, +#define SM_SASL_N_CB_PASS 2 { SASL_CB_LIST_END, NULL, NULL }, }; -#define SASL_MAXRECVBUF 65536 #else /* CYRUS_SASL */ -#define sm_fgetc fgetc +int sasl_ssf = 0; #endif /* CYRUS_SASL */ +#ifdef TLS_SUPPORT +static SSL_CTX *sslctx = NULL; +static SSL *ssl = NULL; +static BIO *sbior = NULL; +static BIO *sbiow = NULL; +static BIO *io = NULL; + +static int tls_negotiate(void); +#endif /* TLS_SUPPORT */ + +#if defined(CYRUS_SASL) || defined(TLS_SUPPORT) +#define SASL_MAXRECVBUF 65536 +static int sm_fgetc(FILE *); +static char *sasl_inbuffer; /* SASL input buffer for encryption */ +static char *sasl_inptr; /* Pointer to current inbuf position */ +static int sasl_inbuflen; /* Current length of data in inbuf */ +#else +#define sm_fgetc fgetc +#endif + +static int tls_active = 0; + static char *sm_noreply = "No reply text given"; static char *sm_moreply = "; "; - -struct smtp sm_reply; /* global... */ +static struct smtp sm_reply; #define MAXEHLO 20 static int doingEHLO; -char *EHLOkeys[MAXEHLO + 1]; +static char *EHLOkeys[MAXEHLO + 1]; /* * static prototypes */ -static int smtp_init (char *, char *, char *, int, int, int, int, int, int, - char *, char *); -static int sendmail_init (char *, char *, int, int, int, int, int, int, +static int smtp_init (char *, char *, char *, int, int, int, int, int, + char *, char *, const char *, int); +static int sendmail_init (char *, char *, int, int, int, int, int, char *, char *); static int rclient (char *, char *); @@ -140,44 +167,51 @@ static int sm_werror (void); static int smhear (void); static int sm_rrecord (char *, int *); static int sm_rerror (int); -static RETSIGTYPE alrmser (int); +static void alrmser (int); static char *EHLOset (char *); +static char *prepare_for_display (const char *, int *); static int sm_fwrite(char *, int); static int sm_fputs(char *); static int sm_fputc(int); static void sm_fflush(void); static int sm_fgets(char *, int, FILE *); +static int sm_auth_xoauth2(const char *, const char *, int); #ifdef CYRUS_SASL /* * Function prototypes needed for SASL */ -static int sm_auth_sasl(char *, char *, char *); +static int sm_auth_sasl(char *, int, char *, char *); #endif /* CYRUS_SASL */ int sm_init (char *client, char *server, char *port, int watch, int verbose, - int debug, int onex, int queued, int sasl, char *saslmech, - char *user) + int debug, int sasl, int saslssf, char *saslmech, char *user, + const char *oauth_svc, int tls) { if (sm_mts == MTS_SMTP) return smtp_init (client, server, port, watch, verbose, - debug, onex, queued, sasl, saslmech, user); + debug, sasl, saslssf, saslmech, user, + oauth_svc, tls); else return sendmail_init (client, server, watch, verbose, - debug, onex, queued, sasl, saslmech, user); + debug, sasl, saslssf, saslmech, user); } static int smtp_init (char *client, char *server, char *port, int watch, int verbose, - int debug, int onex, int queued, - int sasl, char *saslmech, char *user) + int debug, + int sasl, int saslssf, char *saslmech, char *user, + const char *oauth_svc, int tls) { -#ifdef CYRUS_SASL - char *server_mechs; -#endif /* CYRUS_SASL */ int result, sd1, sd2; +#ifndef CYRUS_SASL + NMH_UNUSED (sasl); + NMH_UNUSED (saslssf); + NMH_UNUSED (saslmech); + NMH_UNUSED (user); +#endif /* CYRUS_SASL */ if (watch) verbose = TRUE; @@ -192,21 +226,23 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, if (clientname) { client = clientname; } else { - client = LocalName(); /* no clientname -> LocalName */ + client = LocalName(1); /* no clientname -> LocalName */ } } -#ifdef ZMAILER + /* + * Last-ditch check just in case client still isn't set to anything + */ + if (client == NULL || *client == '\0') client = "localhost"; -#endif -#ifdef CYRUS_SASL +#if defined(CYRUS_SASL) || defined(TLS_SUPPORT) sasl_inbuffer = malloc(SASL_MAXRECVBUF); if (!sasl_inbuffer) return sm_ierror("Unable to allocate %d bytes for read buffer", SASL_MAXRECVBUF); -#endif /* CYRUS_SASL */ +#endif /* CYRUS_SASL || TLS_SUPPORT */ if ((sd1 = rclient (server, port)) == NOTOK) return RP_BHST; @@ -227,6 +263,27 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, return sm_ierror ("unable to fdopen"); } + tls_active = 0; + +#ifdef TLS_SUPPORT + /* + * If tls == 2, that means that the user requested "initial" TLS, + * which happens right after the connection has opened. Do that + * negotiation now + */ + + if (tls == 2) { + result = tls_negotiate(); + + /* + * Note: if tls_negotiate() fails it will call sm_end() for us, + * which closes the connection. + */ + if (result != RP_OK) + return result; + } +#endif /* TLS_SUPPORT */ + sm_alarmed = 0; alarm (SM_OPEN); result = smhear (); @@ -244,19 +301,61 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, /* * Give EHLO or HELO command */ - if (client && *client) { + + doingEHLO = 1; + result = smtalk (SM_HELO, "EHLO %s", client); + doingEHLO = 0; + + if (result >= 500 && result <= 599) + result = smtalk (SM_HELO, "HELO %s", client); + + if (result != 250) { + sm_end (NOTOK); + return RP_RPLY; + } + +#ifdef TLS_SUPPORT + /* + * If the user requested TLS support, then try to do the STARTTLS command + * as part of the initial dialog. Assuming this works, we then need to + * restart the EHLO dialog after TLS negotiation is complete. + */ + + if (tls == 1) { + if (! EHLOset("STARTTLS")) { + sm_end(NOTOK); + return sm_ierror("SMTP server does not support TLS"); + } + + result = smtalk(SM_HELO, "STARTTLS"); + + if (result != 220) { + sm_end(NOTOK); + return RP_RPLY; + } + + /* + * Okay, the other side should be waiting for us to start TLS + * negotiation. Oblige them. + */ + + result = tls_negotiate(); + + if (result != RP_OK) + return result; + doingEHLO = 1; result = smtalk (SM_HELO, "EHLO %s", client); doingEHLO = 0; - if (result >= 500 && result <= 599) - result = smtalk (SM_HELO, "HELO %s", client); - if (result != 250) { sm_end (NOTOK); return RP_RPLY; } } +#else /* TLS_SUPPORT */ + NMH_UNUSED (tls); +#endif /* TLS_SUPPORT */ #ifdef CYRUS_SASL /* @@ -267,6 +366,7 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, */ if (sasl) { + char *server_mechs; if (! (server_mechs = EHLOset("AUTH"))) { sm_end(NOTOK); return sm_ierror("SMTP server does not support SASL"); @@ -279,7 +379,10 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, saslmech, server_mechs); } - if (sm_auth_sasl(user, saslmech ? saslmech : server_mechs, + /* Don't call sm_auth_sasl() for XAUTH2 with -sasl. Instead, call + sm_auth_xoauth2() below. */ + if (oauth_svc == NULL && + sm_auth_sasl(user, saslssf, saslmech ? saslmech : server_mechs, server) != RP_OK) { sm_end(NOTOK); return NOTOK; @@ -287,28 +390,42 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, } #endif /* CYRUS_SASL */ + if (oauth_svc != NULL) { + char *server_mechs; + if ((server_mechs = EHLOset("AUTH")) == NULL + || stringdex("XOAUTH2", server_mechs) == -1) { + sm_end(NOTOK); + return sm_ierror("SMTP server does not support SASL XOAUTH2"); + } + if (sm_auth_xoauth2(user, oauth_svc, debug) != RP_OK) { + sm_end(NOTOK); + return NOTOK; + } + } + send_options: ; if (watch && EHLOset ("XVRB")) smtalk (SM_HELO, "VERB on"); - if (onex && EHLOset ("XONE")) - smtalk (SM_HELO, "ONEX"); - if (queued && EHLOset ("XQUE")) - smtalk (SM_HELO, "QUED"); return RP_OK; } int sendmail_init (char *client, char *server, int watch, int verbose, - int debug, int onex, int queued, - int sasl, char *saslmech, char *user) + int debug, int sasl, int saslssf, char *saslmech, char *user) { + unsigned int i, result, vecp; + int pdi[2], pdo[2]; + char *vec[15]; #ifdef CYRUS_SASL char *server_mechs; +#else /* CYRUS_SASL */ + NMH_UNUSED (server); + NMH_UNUSED (sasl); + NMH_UNUSED (saslssf); + NMH_UNUSED (saslmech); + NMH_UNUSED (user); #endif /* CYRUS_SASL */ - int i, result, vecp; - int pdi[2], pdo[2]; - char *vec[15]; if (watch) verbose = TRUE; @@ -322,20 +439,22 @@ sendmail_init (char *client, char *server, int watch, int verbose, if (clientname) client = clientname; else - client = LocalName(); /* no clientname -> LocalName */ + client = LocalName(1); /* no clientname -> LocalName */ } -#ifdef ZMAILER + /* + * Last-ditch check just in case client still isn't set to anything + */ + if (client == NULL || *client == '\0') client = "localhost"; -#endif -#ifdef CYRUS_SASL +#if defined(CYRUS_SASL) || defined(TLS_SUPPORT) sasl_inbuffer = malloc(SASL_MAXRECVBUF); if (!sasl_inbuffer) return sm_ierror("Unable to allocate %d bytes for read buffer", SASL_MAXRECVBUF); -#endif /* CYRUS_SASL */ +#endif /* CYRUS_SASL || TLS_SUPPORT */ if (pipe (pdi) == NOTOK) return sm_ierror ("no pipes"); @@ -369,19 +488,13 @@ sendmail_init (char *client, char *server, int watch, int verbose, vecp = 0; vec[vecp++] = r1bindex (sendmail, '/'); vec[vecp++] = "-bs"; -#ifndef ZMAILER - vec[vecp++] = watch ? "-odi" : queued ? "-odq" : "-odb"; + vec[vecp++] = watch ? "-odi" : "-odb"; vec[vecp++] = "-oem"; vec[vecp++] = "-om"; -# ifndef RAND if (verbose) vec[vecp++] = "-ov"; -# endif /* not RAND */ -#endif /* not ZMAILER */ vec[vecp++] = NULL; - setgid (getegid ()); - setuid (geteuid ()); execvp (sendmail, vec); fprintf (stderr, "unable to exec "); perror (sendmail); @@ -413,22 +526,20 @@ sendmail_init (char *client, char *server, int watch, int verbose, return RP_RPLY; } - if (client && *client) { - doingEHLO = 1; - result = smtalk (SM_HELO, "EHLO %s", client); - doingEHLO = 0; + doingEHLO = 1; + result = smtalk (SM_HELO, "EHLO %s", client); + doingEHLO = 0; - if (500 <= result && result <= 599) - result = smtalk (SM_HELO, "HELO %s", client); + if (500 <= result && result <= 599) + result = smtalk (SM_HELO, "HELO %s", client); - switch (result) { - case 250: - break; + switch (result) { + case 250: + break; - default: - sm_end (NOTOK); - return RP_RPLY; - } + default: + sm_end (NOTOK); + return RP_RPLY; } #ifdef CYRUS_SASL @@ -452,7 +563,7 @@ sendmail_init (char *client, char *server, int watch, int verbose, saslmech, server_mechs); } - if (sm_auth_sasl(user, saslmech ? saslmech : server_mechs, + if (sm_auth_sasl(user, saslssf, saslmech ? saslmech : server_mechs, server) != RP_OK) { sm_end(NOTOK); return NOTOK; @@ -460,10 +571,6 @@ sendmail_init (char *client, char *server, int watch, int verbose, } #endif /* CYRUS_SASL */ -#ifndef ZMAILER - if (onex) - smtalk (SM_HELO, "ONEX"); -#endif if (watch) smtalk (SM_HELO, "VERB on"); @@ -486,33 +593,9 @@ rclient (char *server, char *service) } int -sm_winit (int mode, char *from) +sm_winit (char *from) { - char *smtpcom = NULL; - - switch (mode) { - case S_MAIL: - smtpcom = "MAIL"; - break; - - case S_SEND: - smtpcom = "SEND"; - break; - - case S_SOML: - smtpcom = "SOML"; - break; - - case S_SAML: - smtpcom = "SAML"; - break; - - default: - /* Hopefully, we do not get here. */ - break; - } - - switch (smtalk (SM_MAIL, "%s FROM:<%s>", smtpcom, from)) { + switch (smtalk (SM_MAIL, "MAIL FROM:<%s>", from)) { case 250: sm_addrs = 0; return RP_OK; @@ -639,7 +722,7 @@ sm_end (int type) int status; struct smtp sm_note; - if (sm_mts == MTS_SENDMAIL) { + if (sm_mts == MTS_SENDMAIL_SMTP) { switch (sm_child) { case NOTOK: case OK: @@ -668,9 +751,19 @@ sm_end (int type) if (sm_mts == MTS_SMTP) smtalk (SM_QUIT, "QUIT"); else { + /* The SIGPIPE block replaces old calls to discard (). + We're not sure what the discard () calls were for, + maybe to prevent deadlock on old systems. In any + case, blocking SIGPIPE should be harmless. + Because the file handles are closed below, leave it + blocked. */ + sigset_t set, oset; + sigemptyset (&set); + sigaddset (&set, SIGPIPE); + sigprocmask (SIG_BLOCK, &set, &oset); + kill (sm_child, SIGKILL); - discard (sm_rfp); - discard (sm_wfp); + sm_child = NOTOK; } if (type == NOTOK) { sm_reply.code = sm_note.code; @@ -680,6 +773,13 @@ sm_end (int type) break; } +#ifdef TLS_SUPPORT + if (tls_active) { + BIO_ssl_shutdown(io); + BIO_free_all(io); + } +#endif /* TLS_SUPPORT */ + if (sm_rfp != NULL) { alarm (SM_CLOS); fclose (sm_rfp); @@ -703,9 +803,11 @@ sm_end (int type) if (sasl_inbuffer) free(sasl_inbuffer); #endif /* CYRUS_SASL */ - } else { + } else if (sm_child != NOTOK) { status = pidwait (sm_child, OK); sm_child = NOTOK; + } else { + status = OK; } sm_rfp = sm_wfp = NULL; @@ -718,27 +820,31 @@ sm_end (int type) * completes successfully, then authentication is successful and we've * (optionally) negotiated a security layer. */ + +#define CHECKB64SIZE(insize, outbuf, outsize) \ + { size_t wantout = (((insize + 2) / 3) * 4) + 32; \ + if (wantout > outsize) { \ + outbuf = mh_xrealloc(outbuf, outsize = wantout); \ + } \ + } + static int -sm_auth_sasl(char *user, char *mechlist, char *inhost) +sm_auth_sasl(char *user, int saslssf, char *mechlist, char *inhost) { int result, status; unsigned int buflen, outlen; - char *buf, outbuf[BUFSIZ], host[NI_MAXHOST]; + char *buf, *outbuf = NULL, host[NI_MAXHOST]; const char *chosen_mech; sasl_security_properties_t secprops; sasl_ssf_t *ssf; int *outbufmax; + struct nmh_creds creds = { 0, 0, 0 }; + size_t outbufsize = 0; /* * Initialize the callback contexts */ - if (user == NULL) - user = getusername(); - - callbacks[SM_SASL_N_CB_USER].context = user; - callbacks[SM_SASL_N_CB_AUTHNAME].context = user; - /* * This is a _bit_ of a hack ... but if the hostname wasn't supplied * to us on the command line, then call getpeername and do a @@ -769,10 +875,13 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) strncpy(host, inhost, sizeof(host) - 1); } - sasl_pw_context[0] = host; - sasl_pw_context[1] = user; - - callbacks[SM_SASL_N_CB_PASS].context = sasl_pw_context; + /* It's OK to copy the addresses here. The callbacks that use + them will only be called before this function returns. */ + creds.host = host; + creds.user = user; + callbacks[SM_SASL_N_CB_USER].context = &creds; + callbacks[SM_SASL_N_CB_AUTHNAME].context = &creds; + callbacks[SM_SASL_N_CB_PASS].context = &creds; result = sasl_client_init(callbacks); @@ -791,12 +900,14 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) } /* - * Initialize the security properties + * Initialize the security properties. But if TLS is active, then + * don't negotiate encryption here. */ memset(&secprops, 0, sizeof(secprops)); secprops.maxbufsize = SASL_MAXRECVBUF; - secprops.max_ssf = UINT_MAX; + secprops.max_ssf = + tls_active ? 0 : (saslssf != -1 ? (unsigned int) saslssf : UINT_MAX); result = sasl_setprop(conn, SASL_SEC_PROPS, &secprops); @@ -825,10 +936,13 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) */ if (buflen) { - status = sasl_encode64(buf, buflen, outbuf, sizeof(outbuf), NULL); + CHECKB64SIZE(buflen, outbuf, outbufsize); + status = sasl_encode64(buf, buflen, outbuf, outbufsize, NULL); if (status != SASL_OK) { sm_ierror("SASL base64 encode failed: %s", sasl_errstring(status, NULL, NULL)); + if (outbuf) + free(outbuf); return NOTOK; } @@ -855,8 +969,11 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) if (status == 235) break; - else if (status < 300 || status > 399) + else if (status < 300 || status > 399) { + if (outbuf) + free(outbuf); return RP_BHST; + } /* * Special case; a zero-length response from the SMTP server @@ -867,13 +984,18 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) if (strcmp("=", sm_reply.text) == 0) { outlen = 0; } else { + if (sm_reply.length > (int) outbufsize) { + outbuf = mh_xrealloc(outbuf, outbufsize = sm_reply.length); + } + result = sasl_decode64(sm_reply.text, sm_reply.length, - outbuf, sizeof(outbuf), &outlen); - + outbuf, outbufsize, &outlen); if (result != SASL_OK) { smtalk(SM_AUTH, "*"); sm_ierror("SASL base64 decode failed: %s", sasl_errstring(result, NULL, NULL)); + if (outbuf) + free(outbuf); return NOTOK; } } @@ -885,21 +1007,29 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) smtalk(SM_AUTH, "*"); sm_ierror("SASL client negotiation failed: %s", sasl_errstring(result, NULL, NULL)); + if (outbuf) + free(outbuf); return NOTOK; } - status = sasl_encode64(buf, buflen, outbuf, sizeof(outbuf), NULL); + CHECKB64SIZE(buflen, outbuf, outbufsize); + status = sasl_encode64(buf, buflen, outbuf, outbufsize, NULL); if (status != SASL_OK) { smtalk(SM_AUTH, "*"); sm_ierror("SASL base64 encode failed: %s", sasl_errstring(status, NULL, NULL)); + if (outbuf) + free(outbuf); return NOTOK; } status = smtalk(SM_AUTH, outbuf); } + if (outbuf) + free(outbuf); + /* * Make sure that we got the correct response */ @@ -961,14 +1091,27 @@ sm_auth_sasl(char *user, char *mechlist, char *inhost) static int sm_get_user(void *context, int id, const char **result, unsigned *len) { - char *user = (char *) context; + nmh_creds_t creds = (nmh_creds_t) context; if (! result || ((id != SASL_CB_USER) && (id != SASL_CB_AUTHNAME))) return SASL_BADPARAM; - *result = user; + if (creds->user == NULL) { + /* + * Pass the 1 third argument to nmh_get_credentials() so + * that a default user if the -user switch to send(1)/post(8) + * wasn't used, and so that a default password will be supplied. + * That's used when those values really don't matter, and only + * with legacy/.netrc, i.e., with a credentials profile entry. + */ + if (nmh_get_credentials (creds->host, creds->user, 1, creds) != OK) { + return SASL_BADPARAM; + } + } + + *result = creds->user; if (len) - *len = strlen(user); + *len = strlen(creds->user); return SASL_OK; } @@ -977,32 +1120,86 @@ static int sm_get_pass(sasl_conn_t *conn, void *context, int id, sasl_secret_t **psecret) { - char **pw_context = (char **) context; - char *pass = NULL; + nmh_creds_t creds = (nmh_creds_t) context; int len; + NMH_UNUSED (conn); + if (! psecret || id != SASL_CB_PASS) return SASL_BADPARAM; - ruserpass(pw_context[0], &(pw_context[1]), &pass); - - len = strlen(pass); + if (creds->password == NULL) { + /* + * Pass the 0 third argument to nmh_get_credentials() so + * that the default password isn't used. With legacy/.netrc + * credentials support, we'll only get here if the -user + * switch to send(1)/post(8) wasn't used. + */ + if (nmh_get_credentials (creds->host, creds->user, 0, creds) != OK) { + return SASL_BADPARAM; + } + } - *psecret = (sasl_secret_t *) malloc(sizeof(sasl_secret_t) + len); + len = strlen (creds->password); - if (! *psecret) { - free(pass); + if (! (*psecret = (sasl_secret_t *) malloc(sizeof(sasl_secret_t) + len))) { return SASL_NOMEM; } (*psecret)->len = len; - strcpy((char *) (*psecret)->data, pass); -/* free(pass); */ + strcpy((char *) (*psecret)->data, creds->password); return SASL_OK; } #endif /* CYRUS_SASL */ +/* https://developers.google.com/gmail/xoauth2_protocol */ +static int +sm_auth_xoauth2(const char *user, const char *oauth_svc, int snoop) +{ + const char *xoauth_client_res; + int status; + +#ifdef OAUTH_SUPPORT + xoauth_client_res = mh_oauth_do_xoauth(user, oauth_svc, + snoop ? stderr : NULL); + + if (xoauth_client_res == NULL) { + return sm_ierror("Internal error: mh_oauth_do_xoauth() returned NULL"); + } +#else + NMH_UNUSED(user); + NMH_UNUSED(snoop); + adios(NULL, "sendfrom built without OAUTH_SUPPORT, " + "so oauth_svc %s is not supported", oauth_svc); +#endif /* OAUTH_SUPPORT */ + + status = smtalk(SM_AUTH, "AUTH XOAUTH2 %s", xoauth_client_res); + if (status == 235) { + /* It worked! */ + return RP_OK; + } + + /* + * Status is 334 and sm_reply.text contains base64-encoded JSON. As far as + * epg can tell, no matter the error, the JSON is always the same: + * {"status":"400","schemes":"Bearer","scope":"https://mail.google.com/"} + * I tried these errors: + * - garbage token + * - expired token + * - wrong scope + * - wrong username + */ + /* Then we're supposed to send an empty response ("\r\n"). */ + smtalk(SM_AUTH, ""); + /* + * And now we always get this, again, no matter the error: + * 535-5.7.8 Username and Password not accepted. Learn more at + * 535 5.7.8 http://support.google.com/mail/bin/answer.py?answer=14257 + */ + return RP_BHST; +} + static int sm_ierror (char *fmt, ...) { @@ -1023,18 +1220,32 @@ smtalk (int time, char *fmt, ...) { va_list ap; int result; - char buffer[BUFSIZ]; + char *buffer; + size_t bufsize = BUFSIZ; + + buffer = mh_xmalloc(bufsize); va_start(ap, fmt); - vsnprintf (buffer, sizeof(buffer), fmt, ap); + result = vsnprintf (buffer, bufsize, fmt, ap); va_end(ap); + if (result > (int) bufsize) { + buffer = mh_xrealloc(buffer, bufsize = result + 1); + va_start(ap, fmt); + vsnprintf (buffer, bufsize, fmt, ap); + va_end(ap); + } + if (sm_debug) { -#ifdef CYRUS_SASL + char *decoded_buffer = + prepare_for_display (buffer, &next_line_encoded); + if (sasl_ssf) - printf("(encrypted) "); -#endif /* CYRUS_SASL */ - printf ("=> %s\n", buffer); + printf("(sasl-encrypted) "); + if (tls_active) + printf("(tls-encrypted) "); + printf ("=> %s\n", decoded_buffer); + free (decoded_buffer); fflush (stdout); } @@ -1044,6 +1255,8 @@ smtalk (int time, char *fmt, ...) result = smhear (); alarm (0); + free(buffer); + return result; } @@ -1082,7 +1295,7 @@ sm_wstream (char *buffer, int len) return (ferror (sm_wfp) ? sm_werror () : OK); } - for (bp = buffer; len > 0; bp++, len--) { + for (bp = buffer; bp && len > 0; bp++, len--) { switch (*bp) { case '\n': sm_nl = TRUE; @@ -1116,11 +1329,26 @@ sm_fwrite(char *buffer, int len) const char *output; unsigned int outputlen; - if (sasl_complete == 0 || sasl_ssf == 0) + if (sasl_complete == 0 || sasl_ssf == 0) { #endif /* CYRUS_SASL */ - fwrite(buffer, sizeof(*buffer), len, sm_wfp); +#ifdef TLS_SUPPORT + if (tls_active) { + int ret; + + ret = BIO_write(io, buffer, len); + + if (ret <= 0) { + sm_ierror("TLS error during write: %s", + ERR_error_string(ERR_get_error(), NULL)); + return NOTOK; + } + } else +#endif /* TLS_SUPPORT */ + if ((int) fwrite(buffer, sizeof(*buffer), len, sm_wfp) < len) { + advise ("sm_fwrite", "fwrite"); + } #ifdef CYRUS_SASL - else { + } else { while (len >= maxoutbuf - sasl_outbuflen) { memcpy(sasl_outbuffer + sasl_outbuflen, buffer, maxoutbuf - sasl_outbuflen); @@ -1134,7 +1362,10 @@ sm_fwrite(char *buffer, int len) return NOTOK; } - fwrite(output, sizeof(*output), outputlen, sm_wfp); + if (fwrite(output, sizeof(*output), outputlen, sm_wfp) < + outputlen) { + advise ("sm_fwrite", "fwrite"); + } } if (len > 0) { @@ -1146,8 +1377,104 @@ sm_fwrite(char *buffer, int len) return ferror(sm_wfp) ? NOTOK : RP_OK; } +#ifdef TLS_SUPPORT +/* + * Negotiate Transport Layer Security + */ + +static int +tls_negotiate(void) +{ + BIO *ssl_bio; + + if (! sslctx) { + const SSL_METHOD *method; + + SSL_library_init(); + SSL_load_error_strings(); + + method = TLSv1_client_method(); /* Not sure about this */ + + /* Older ssl takes a non-const arg. */ + sslctx = SSL_CTX_new((SSL_METHOD *) method); + + if (! sslctx) { + sm_end(NOTOK); + return sm_ierror("Unable to initialize OpenSSL context: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + } + + ssl = SSL_new(sslctx); + + if (! ssl) { + sm_end(NOTOK); + return sm_ierror("Unable to create SSL connection: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + + sbior = BIO_new_socket(fileno(sm_rfp), BIO_NOCLOSE); + sbiow = BIO_new_socket(fileno(sm_wfp), BIO_NOCLOSE); + + if (sbior == NULL || sbiow == NULL) { + sm_end(NOTOK); + return sm_ierror("Unable to create BIO endpoints: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + + SSL_set_bio(ssl, sbior, sbiow); + SSL_set_connect_state(ssl); + + /* + * Set up a BIO to handle buffering for us + */ + + io = BIO_new(BIO_f_buffer()); + + if (! io) { + sm_end(NOTOK); + return sm_ierror("Unable to create a buffer BIO: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + + ssl_bio = BIO_new(BIO_f_ssl()); + + if (! ssl_bio) { + sm_end(NOTOK); + return sm_ierror("Unable to create a SSL BIO: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + + BIO_set_ssl(ssl_bio, ssl, BIO_CLOSE); + BIO_push(io, ssl_bio); + + /* + * Try doing the handshake now + */ + + if (BIO_do_handshake(io) < 1) { + sm_end(NOTOK); + return sm_ierror("Unable to negotiate SSL connection: %s", + ERR_error_string(ERR_get_error(), NULL)); + } + + if (sm_debug) { + const SSL_CIPHER *cipher = SSL_get_current_cipher(ssl); + printf("SSL negotiation successful: %s(%d) %s\n", + SSL_CIPHER_get_name(cipher), + SSL_CIPHER_get_bits(cipher, NULL), + SSL_CIPHER_get_version(cipher)); + + } + + tls_active = 1; + + return RP_OK; +} +#endif /* TLS_SUPPORT */ + /* - * Convenience functions to replace occurences of fputs() and fputc() + * Convenience functions to replace occurrences of fputs() and fputc() */ static int @@ -1185,11 +1512,19 @@ sm_fflush(void) return; } - fwrite(output, sizeof(*output), outputlen, sm_wfp); + if (fwrite(output, sizeof(*output), outputlen, sm_wfp) < outputlen) { + advise ("sm_fflush", "fwrite"); + } sasl_outbuflen = 0; } #endif /* CYRUS_SASL */ +#ifdef TLS_SUPPORT + if (tls_active) { + (void) BIO_flush(io); + } +#endif /* TLS_SUPPORT */ + fflush(sm_wfp); } @@ -1208,10 +1543,10 @@ sm_werror (void) static int smhear (void) { - int i, code, cont, bc, rc, more; + int i, code, cont, bc = 0, rc, more; unsigned char *bp; char *rp; - char **ehlo = NULL, buffer[BUFSIZ]; + char **ehlo = EHLOkeys, buffer[BUFSIZ]; if (doingEHLO) { static int at_least_once = 0; @@ -1241,11 +1576,15 @@ again: ; for (more = FALSE; sm_rrecord ((char *) (bp = (unsigned char *) buffer), &bc) != NOTOK ; ) { if (sm_debug) { -#ifdef CYRUS_SASL + char *decoded_buffer = + prepare_for_display (buffer, &next_line_encoded); + if (sasl_ssf > 0) - printf("(decrypted) "); -#endif /* CYRUS_SASL */ - printf ("<= %s\n", buffer); + printf("(sasl-decrypted) "); + if (tls_active) + printf("(tls-decrypted) "); + printf ("<= %s\n", decoded_buffer); + free (decoded_buffer); fflush (stdout); } @@ -1337,7 +1676,7 @@ sm_rrecord (char *buffer, int *len) buffer[*len = 0] = 0; if ((retval = sm_fgets (buffer, BUFSIZ, sm_rfp)) != RP_OK) - return retval; + return sm_rerror (retval); *len = strlen (buffer); /* *len should be >0 except on EOF, but check for safety's sake */ if (*len == 0) @@ -1382,9 +1721,9 @@ sm_fgets(char *buffer, int size, FILE *f) } -#ifdef CYRUS_SASL +#if defined(CYRUS_SASL) || defined(TLS_SUPPORT) /* - * Read from the network, but do SASL encryption + * Read from the network, but do SASL or TLS encryption */ static int @@ -1409,6 +1748,28 @@ sm_fgetc(FILE *f) while (retbufsize == 0) { +#ifdef TLS_SUPPORT + if (tls_active) { + cc = SSL_read(ssl, tmpbuf, sizeof(tmpbuf)); + + if (cc == 0) { + result = SSL_get_error(ssl, cc); + + if (result != SSL_ERROR_ZERO_RETURN) { + sm_ierror("TLS peer aborted connection"); + } + + return EOF; + } + + if (cc < 0) { + sm_ierror("SSL_read failed: %s", + ERR_error_string(ERR_get_error(), NULL)); + return -2; + } + } else +#endif /* TLS_SUPPORT */ + cc = read(fileno(f), tmpbuf, sizeof(tmpbuf)); if (cc == 0) @@ -1424,6 +1785,7 @@ sm_fgetc(FILE *f) * encryption working */ +#ifdef CYRUS_SASL if (sasl_complete == 0 || sasl_ssf == 0) { retbuf = tmpbuf; retbufsize = cc; @@ -1437,6 +1799,10 @@ sm_fgetc(FILE *f) return -2; } } +#else /* ! CYRUS_SASL */ + retbuf = tmpbuf; + retbufsize = cc; +#endif /* CYRUS_SASL */ } if (retbufsize > SASL_MAXRECVBUF) { @@ -1451,7 +1817,7 @@ sm_fgetc(FILE *f) return (int) sasl_inbuffer[0]; } -#endif /* CYRUS_SASL */ +#endif /* CYRUS_SASL || TLS_SUPPORT */ static int sm_rerror (int rc) @@ -1473,9 +1839,11 @@ sm_rerror (int rc) } -static RETSIGTYPE +static void alrmser (int i) { + NMH_UNUSED (i); + #ifndef RELIABLE_SIGNALS SIGNAL (SIGALRM, alrmser); #endif @@ -1558,3 +1926,69 @@ EHLOset (char *s) return 0; } + + +/* + * Detects, using heuristics, if an SMTP server or client response string + * contains a base64-encoded portion. If it does, decodes it and replaces + * any non-printable characters with a hex representation. Caller is + * responsible for free'ing return value. If the decode fails, a copy of + * the input string is returned. + */ +static +char * +prepare_for_display (const char *string, int *next_line_encoded) { + const char *start = NULL; + const char *decoded; + size_t decoded_len; + int prefix_len = -1; + + if (strncmp (string, "AUTH ", 5) == 0) { + /* AUTH line: the mechanism isn't encoded. If there's an initial + response, it must be base64 encoded.. */ + char *mechanism = strchr (string + 5, ' '); + + if (mechanism != NULL) { + prefix_len = (int) (mechanism - string + 1); + } /* else no space following the mechanism, so no initial response */ + *next_line_encoded = 0; + } else if (strncmp (string, "334 ", 4) == 0) { + /* 334 is the server's request for user or password. */ + prefix_len = 4; + /* The next (client response) line must be base64 encoded. */ + *next_line_encoded = 1; + } else if (*next_line_encoded) { + /* "next" line now refers to this line, which is a base64-encoded + client response. */ + prefix_len = 0; + *next_line_encoded = 0; + } else { + *next_line_encoded = 0; + } + + /* Don't attempt to decoded unencoded initial response ('=') or cancel + response ('*'). */ + if (prefix_len > -1 && + string[prefix_len] != '=' && string[prefix_len] != '*') { + start = string + prefix_len; + } + + if (start && decodeBase64 (start, &decoded, &decoded_len, 1, NULL) == OK) { + char *hexified; + char *prefix = mh_xmalloc(prefix_len + 1); + char *display_string; + + /* prefix is the beginning portion, which isn't base64 encoded. */ + snprintf (prefix, prefix_len + 1, "%*s", prefix_len, string); + hexify ((const unsigned char *) decoded, decoded_len, &hexified); + /* Wrap the decoded portion in "b64<>". */ + display_string = concat (prefix, "b64<", hexified, ">", NULL); + free (hexified); + free (prefix); + free ((char *) decoded); + + return display_string; + } else { + return getcpy (string); + } +}