X-Git-Url: https://diplodocus.org/git/nmh/blobdiff_plain/dd740c4e3e407d8dba7884bb599c1f5a588b05e9..303e8387acecca26329e939f228f78ca805b7a15:/mts/smtp/smtp.c?ds=sidebyside diff --git a/mts/smtp/smtp.c b/mts/smtp/smtp.c index 0261d6d6..79c1e79b 100644 --- a/mts/smtp/smtp.c +++ b/mts/smtp/smtp.c @@ -10,6 +10,8 @@ #include "smtp.h" #include #include +#include +#include #ifdef CYRUS_SASL #include @@ -75,9 +77,7 @@ #define SM_DOT 600 /* see above */ #define SM_QUIT 30 #define SM_CLOS 10 -#ifdef CYRUS_SASL #define SM_AUTH 45 -#endif /* CYRUS_SASL */ static int sm_addrs = 0; static int sm_alarmed = 0; @@ -89,13 +89,15 @@ 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 int maxoutbuf; /* Maximum crypto output buffer */ static char *sasl_outbuffer; /* SASL output buffer for encryption */ @@ -151,9 +153,9 @@ static char *EHLOkeys[MAXEHLO + 1]; /* * static prototypes */ -static int smtp_init (char *, char *, char *, int, int, int, int, int, int, - char *, char *, int); -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 *); @@ -167,11 +169,13 @@ static int sm_rrecord (char *, int *); static int sm_rerror (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 /* @@ -183,28 +187,26 @@ static int sm_auth_sasl(char *, int, char *, char *); int sm_init (char *client, char *server, char *port, int watch, int verbose, - int debug, int queued, int sasl, int saslssf, - char *saslmech, char *user, int tls) + 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, queued, sasl, saslssf, saslmech, - user, tls); + debug, sasl, saslssf, saslmech, user, + oauth_svc, tls); else return sendmail_init (client, server, watch, verbose, - debug, queued, sasl, saslssf, saslmech, - user); + debug, sasl, saslssf, saslmech, user); } static int smtp_init (char *client, char *server, char *port, int watch, int verbose, - int debug, int queued, - int sasl, int saslssf, char *saslmech, char *user, int tls) + int debug, + int sasl, int saslssf, char *saslmech, char *user, + const char *oauth_svc, int tls) { int result, sd1, sd2; -#ifdef CYRUS_SASL - char *server_mechs; -#else /* CYRUS_SASL */ +#ifndef CYRUS_SASL NMH_UNUSED (sasl); NMH_UNUSED (saslssf); NMH_UNUSED (saslmech); @@ -364,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"); @@ -376,7 +379,10 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, saslmech, server_mechs); } - if (sm_auth_sasl(user, saslssf, 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; @@ -384,19 +390,29 @@ 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 (queued && EHLOset ("XQUE")) - smtalk (SM_HELO, "QUED"); return RP_OK; } int sendmail_init (char *client, char *server, int watch, int verbose, - int debug, int queued, - int sasl, int saslssf, char *saslmech, char *user) + int debug, int sasl, int saslssf, char *saslmech, char *user) { unsigned int i, result, vecp; int pdi[2], pdo[2]; @@ -472,7 +488,7 @@ sendmail_init (char *client, char *server, int watch, int verbose, vecp = 0; vec[vecp++] = r1bindex (sendmail, '/'); vec[vecp++] = "-bs"; - vec[vecp++] = watch ? "-odi" : queued ? "-odq" : "-odb"; + vec[vecp++] = watch ? "-odi" : "-odb"; vec[vecp++] = "-oem"; vec[vecp++] = "-om"; if (verbose) @@ -804,17 +820,26 @@ 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, 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 @@ -911,10 +936,13 @@ sm_auth_sasl(char *user, int saslssf, 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; } @@ -941,8 +969,11 @@ sm_auth_sasl(char *user, int saslssf, 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 @@ -953,12 +984,18 @@ sm_auth_sasl(char *user, int saslssf, 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; } } @@ -970,21 +1007,29 @@ sm_auth_sasl(char *user, int saslssf, 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 */ @@ -1059,7 +1104,9 @@ sm_get_user(void *context, int id, const char **result, unsigned *len) * That's used when those values really don't matter, and only * with legacy/.netrc, i.e., with a credentials profile entry. */ - nmh_get_credentials (creds->host, creds->user, 1, creds); + if (nmh_get_credentials (creds->host, creds->user, 1, creds) != OK) { + return SASL_BADPARAM; + } } *result = creds->user; @@ -1086,9 +1133,11 @@ sm_get_pass(sasl_conn_t *conn, void *context, int id, * 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 used. + * switch to send(1)/post(8) wasn't used. */ - nmh_get_credentials (creds->host, creds->user, 0, creds); + if (nmh_get_credentials (creds->host, creds->user, 0, creds) != OK) { + return SASL_BADPARAM; + } } len = strlen (creds->password); @@ -1104,6 +1153,53 @@ sm_get_pass(sasl_conn_t *conn, void *context, int id, } #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, ...) { @@ -1124,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) { + char *decoded_buffer = + prepare_for_display (buffer, &next_line_encoded); + if (sasl_ssf) printf("(sasl-encrypted) "); if (tls_active) printf("(tls-encrypted) "); - printf ("=> %s\n", buffer); + printf ("=> %s\n", decoded_buffer); + free (decoded_buffer); fflush (stdout); } @@ -1145,6 +1255,8 @@ smtalk (int time, char *fmt, ...) result = smhear (); alarm (0); + free(buffer); + return result; } @@ -1183,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; @@ -1232,7 +1344,9 @@ sm_fwrite(char *buffer, int len) } } else #endif /* TLS_SUPPORT */ - fwrite(buffer, sizeof(*buffer), len, sm_wfp); + if ((int) fwrite(buffer, sizeof(*buffer), len, sm_wfp) < len) { + advise ("sm_fwrite", "fwrite"); + } #ifdef CYRUS_SASL } else { while (len >= maxoutbuf - sasl_outbuflen) { @@ -1248,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) { @@ -1357,7 +1474,7 @@ tls_negotiate(void) #endif /* TLS_SUPPORT */ /* - * Convenience functions to replace occurences of fputs() and fputc() + * Convenience functions to replace occurrences of fputs() and fputc() */ static int @@ -1395,7 +1512,9 @@ 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 */ @@ -1427,7 +1546,7 @@ smhear (void) 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; @@ -1457,11 +1576,15 @@ again: ; for (more = FALSE; sm_rrecord ((char *) (bp = (unsigned char *) buffer), &bc) != NOTOK ; ) { if (sm_debug) { + char *decoded_buffer = + prepare_for_display (buffer, &next_line_encoded); + if (sasl_ssf > 0) printf("(sasl-decrypted) "); if (tls_active) printf("(tls-decrypted) "); - printf ("<= %s\n", buffer); + printf ("<= %s\n", decoded_buffer); + free (decoded_buffer); fflush (stdout); } @@ -1803,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); + } +}