diff options
Diffstat (limited to 'contrib/sendmail/src/usersmtp.c')
-rw-r--r-- | contrib/sendmail/src/usersmtp.c | 3322 |
1 files changed, 3322 insertions, 0 deletions
diff --git a/contrib/sendmail/src/usersmtp.c b/contrib/sendmail/src/usersmtp.c new file mode 100644 index 0000000..23278b0 --- /dev/null +++ b/contrib/sendmail/src/usersmtp.c @@ -0,0 +1,3322 @@ +/* + * Copyright (c) 1998-2006, 2008, 2009 Sendmail, Inc. and its suppliers. + * All rights reserved. + * Copyright (c) 1983, 1995-1997 Eric P. Allman. All rights reserved. + * Copyright (c) 1988, 1993 + * The Regents of the University of California. All rights reserved. + * + * By using this file, you agree to the terms and conditions set + * forth in the LICENSE file which can be found at the top level of + * the sendmail distribution. + * + */ + +#include <sendmail.h> + +SM_RCSID("@(#)$Id: usersmtp.c,v 8.473 2009/06/17 17:26:51 ca Exp $") + +#include <sysexits.h> + + +static void esmtp_check __P((char *, bool, MAILER *, MCI *, ENVELOPE *)); +static void helo_options __P((char *, bool, MAILER *, MCI *, ENVELOPE *)); +static int smtprcptstat __P((ADDRESS *, MAILER *, MCI *, ENVELOPE *)); + +#if SASL +extern void *sm_sasl_malloc __P((unsigned long)); +extern void sm_sasl_free __P((void *)); +#endif /* SASL */ + +/* +** USERSMTP -- run SMTP protocol from the user end. +** +** This protocol is described in RFC821. +*/ + +#define REPLYCLASS(r) (((r) / 10) % 10) /* second digit of reply code */ +#define SMTPCLOSING 421 /* "Service Shutting Down" */ + +#define ENHSCN(e, d) ((e) == NULL ? (d) : (e)) + +#define ENHSCN_RPOOL(e, d, rpool) \ + ((e) == NULL ? (d) : sm_rpool_strdup_x(rpool, e)) + +static char SmtpMsgBuffer[MAXLINE]; /* buffer for commands */ +static char SmtpReplyBuffer[MAXLINE]; /* buffer for replies */ +static bool SmtpNeedIntro; /* need "while talking" in transcript */ +/* +** SMTPINIT -- initialize SMTP. +** +** Opens the connection and sends the initial protocol. +** +** Parameters: +** m -- mailer to create connection to. +** mci -- the mailer connection info. +** e -- the envelope. +** onlyhelo -- send only helo command? +** +** Returns: +** none. +** +** Side Effects: +** creates connection and sends initial protocol. +*/ + +void +smtpinit(m, mci, e, onlyhelo) + MAILER *m; + register MCI *mci; + ENVELOPE *e; + bool onlyhelo; +{ + register int r; + int state; + register char *p; + register char *hn; + char *enhsc; + + enhsc = NULL; + if (tTd(18, 1)) + { + sm_dprintf("smtpinit "); + mci_dump(sm_debug_file(), mci, false); + } + + /* + ** Open the connection to the mailer. + */ + + SmtpError[0] = '\0'; + SmtpMsgBuffer[0] = '\0'; + CurHostName = mci->mci_host; /* XXX UGLY XXX */ + if (CurHostName == NULL) + CurHostName = MyHostName; + SmtpNeedIntro = true; + state = mci->mci_state; + switch (state) + { + case MCIS_MAIL: + case MCIS_RCPT: + case MCIS_DATA: + /* need to clear old information */ + smtprset(m, mci, e); + /* FALLTHROUGH */ + + case MCIS_OPEN: + if (!onlyhelo) + return; + break; + + case MCIS_ERROR: + case MCIS_QUITING: + case MCIS_SSD: + /* shouldn't happen */ + smtpquit(m, mci, e); + /* FALLTHROUGH */ + + case MCIS_CLOSED: + syserr("451 4.4.0 smtpinit: state CLOSED (was %d)", state); + return; + + case MCIS_OPENING: + break; + } + if (onlyhelo) + goto helo; + + mci->mci_state = MCIS_OPENING; + clrsessenvelope(e); + + /* + ** Get the greeting message. + ** This should appear spontaneously. Give it five minutes to + ** happen. + */ + + SmtpPhase = mci->mci_phase = "client greeting"; + sm_setproctitle(true, e, "%s %s: %s", + qid_printname(e), CurHostName, mci->mci_phase); + r = reply(m, mci, e, TimeOuts.to_initial, esmtp_check, NULL, + XS_DEFAULT); + if (r < 0) + goto tempfail1; + if (REPLYTYPE(r) == 4) + goto tempfail2; + if (REPLYTYPE(r) != 2) + goto unavailable; + + /* + ** Send the HELO command. + ** My mother taught me to always introduce myself. + */ + +helo: + if (bitnset(M_ESMTP, m->m_flags) || bitnset(M_LMTP, m->m_flags)) + mci->mci_flags |= MCIF_ESMTP; + hn = mci->mci_heloname ? mci->mci_heloname : MyHostName; + +tryhelo: +#if _FFR_IGNORE_EXT_ON_HELO + mci->mci_flags &= ~MCIF_HELO; +#endif /* _FFR_IGNORE_EXT_ON_HELO */ + if (bitnset(M_LMTP, m->m_flags)) + { + smtpmessage("LHLO %s", m, mci, hn); + SmtpPhase = mci->mci_phase = "client LHLO"; + } + else if (bitset(MCIF_ESMTP, mci->mci_flags) && + !bitnset(M_FSMTP, m->m_flags)) + { + smtpmessage("EHLO %s", m, mci, hn); + SmtpPhase = mci->mci_phase = "client EHLO"; + } + else + { + smtpmessage("HELO %s", m, mci, hn); + SmtpPhase = mci->mci_phase = "client HELO"; +#if _FFR_IGNORE_EXT_ON_HELO + mci->mci_flags |= MCIF_HELO; +#endif /* _FFR_IGNORE_EXT_ON_HELO */ + } + sm_setproctitle(true, e, "%s %s: %s", qid_printname(e), + CurHostName, mci->mci_phase); + r = reply(m, mci, e, + bitnset(M_LMTP, m->m_flags) ? TimeOuts.to_lhlo + : TimeOuts.to_helo, + helo_options, NULL, XS_DEFAULT); + if (r < 0) + goto tempfail1; + else if (REPLYTYPE(r) == 5) + { + if (bitset(MCIF_ESMTP, mci->mci_flags) && + !bitnset(M_LMTP, m->m_flags)) + { + /* try old SMTP instead */ + mci->mci_flags &= ~MCIF_ESMTP; + goto tryhelo; + } + goto unavailable; + } + else if (REPLYTYPE(r) != 2) + goto tempfail2; + + /* + ** Check to see if we actually ended up talking to ourself. + ** This means we didn't know about an alias or MX, or we managed + ** to connect to an echo server. + */ + + p = strchr(&SmtpReplyBuffer[4], ' '); + if (p != NULL) + *p = '\0'; + if (!bitnset(M_NOLOOPCHECK, m->m_flags) && + !bitnset(M_LMTP, m->m_flags) && + sm_strcasecmp(&SmtpReplyBuffer[4], MyHostName) == 0) + { + syserr("553 5.3.5 %s config error: mail loops back to me (MX problem?)", + CurHostName); + mci_setstat(mci, EX_CONFIG, "5.3.5", + "553 5.3.5 system config error"); + mci->mci_errno = 0; + smtpquit(m, mci, e); + return; + } + + /* + ** If this is expected to be another sendmail, send some internal + ** commands. + ** If we're running as MSP, "propagate" -v flag if possible. + */ + + if ((UseMSP && Verbose && bitset(MCIF_VERB, mci->mci_flags)) +# if !_FFR_DEPRECATE_MAILER_FLAG_I + || bitnset(M_INTERNAL, m->m_flags) +# endif /* !_FFR_DEPRECATE_MAILER_FLAG_I */ + ) + { + /* tell it to be verbose */ + smtpmessage("VERB", m, mci); + r = reply(m, mci, e, TimeOuts.to_miscshort, NULL, &enhsc, + XS_DEFAULT); + if (r < 0) + goto tempfail1; + } + + if (mci->mci_state != MCIS_CLOSED) + { + mci->mci_state = MCIS_OPEN; + return; + } + + /* got a 421 error code during startup */ + + tempfail1: + mci_setstat(mci, EX_TEMPFAIL, ENHSCN(enhsc, "4.4.2"), NULL); + if (mci->mci_state != MCIS_CLOSED) + smtpquit(m, mci, e); + return; + + tempfail2: + /* XXX should use code from other end iff ENHANCEDSTATUSCODES */ + mci_setstat(mci, EX_TEMPFAIL, ENHSCN(enhsc, "4.5.0"), + SmtpReplyBuffer); + if (mci->mci_state != MCIS_CLOSED) + smtpquit(m, mci, e); + return; + + unavailable: + mci_setstat(mci, EX_UNAVAILABLE, "5.5.0", SmtpReplyBuffer); + smtpquit(m, mci, e); + return; +} +/* +** ESMTP_CHECK -- check to see if this implementation likes ESMTP protocol +** +** Parameters: +** line -- the response line. +** firstline -- set if this is the first line of the reply. +** m -- the mailer. +** mci -- the mailer connection info. +** e -- the envelope. +** +** Returns: +** none. +*/ + +static void +esmtp_check(line, firstline, m, mci, e) + char *line; + bool firstline; + MAILER *m; + register MCI *mci; + ENVELOPE *e; +{ + if (strstr(line, "ESMTP") != NULL) + mci->mci_flags |= MCIF_ESMTP; + + /* + ** Dirty hack below. Quoting the author: + ** This was a response to people who wanted SMTP transmission to be + ** just-send-8 by default. Essentially, you could put this tag into + ** your greeting message to behave as though the F=8 flag was set on + ** the mailer. + */ + + if (strstr(line, "8BIT-OK") != NULL) + mci->mci_flags |= MCIF_8BITOK; +} + +#if SASL +/* specify prototype so compiler can check calls */ +static char *str_union __P((char *, char *, SM_RPOOL_T *)); + +/* +** STR_UNION -- create the union of two lists +** +** Parameters: +** s1, s2 -- lists of items (separated by single blanks). +** rpool -- resource pool from which result is allocated. +** +** Returns: +** the union of both lists. +*/ + +static char * +str_union(s1, s2, rpool) + char *s1, *s2; + SM_RPOOL_T *rpool; +{ + char *hr, *h1, *h, *res; + int l1, l2, rl; + + if (s1 == NULL || *s1 == '\0') + return s2; + if (s2 == NULL || *s2 == '\0') + return s1; + l1 = strlen(s1); + l2 = strlen(s2); + rl = l1 + l2; + res = (char *) sm_rpool_malloc(rpool, rl + 2); + if (res == NULL) + { + if (l1 > l2) + return s1; + return s2; + } + (void) sm_strlcpy(res, s1, rl); + hr = res + l1; + h1 = s2; + h = s2; + + /* walk through s2 */ + while (h != NULL && *h1 != '\0') + { + /* is there something after the current word? */ + if ((h = strchr(h1, ' ')) != NULL) + *h = '\0'; + l1 = strlen(h1); + + /* does the current word appear in s1 ? */ + if (iteminlist(h1, s1, " ") == NULL) + { + /* add space as delimiter */ + *hr++ = ' '; + + /* copy the item */ + memcpy(hr, h1, l1); + + /* advance pointer in result list */ + hr += l1; + *hr = '\0'; + } + if (h != NULL) + { + /* there are more items */ + *h = ' '; + h1 = h + 1; + } + } + return res; +} +#endif /* SASL */ + +/* +** HELO_OPTIONS -- process the options on a HELO line. +** +** Parameters: +** line -- the response line. +** firstline -- set if this is the first line of the reply. +** m -- the mailer. +** mci -- the mailer connection info. +** e -- the envelope (unused). +** +** Returns: +** none. +*/ + +static void +helo_options(line, firstline, m, mci, e) + char *line; + bool firstline; + MAILER *m; + register MCI *mci; + ENVELOPE *e; +{ + register char *p; +#if _FFR_IGNORE_EXT_ON_HELO + static bool logged = false; +#endif /* _FFR_IGNORE_EXT_ON_HELO */ + + if (firstline) + { +#if SASL + mci->mci_saslcap = NULL; +#endif /* SASL */ +#if _FFR_IGNORE_EXT_ON_HELO + logged = false; +#endif /* _FFR_IGNORE_EXT_ON_HELO */ + return; + } +#if _FFR_IGNORE_EXT_ON_HELO + else if (bitset(MCIF_HELO, mci->mci_flags)) + { + if (LogLevel > 8 && !logged) + { + sm_syslog(LOG_WARNING, NOQID, + "server=%s [%s] returned extensions despite HELO command", + macvalue(macid("{server_name}"), e), + macvalue(macid("{server_addr}"), e)); + logged = true; + } + return; + } +#endif /* _FFR_IGNORE_EXT_ON_HELO */ + + if (strlen(line) < 5) + return; + line += 4; + p = strpbrk(line, " ="); + if (p != NULL) + *p++ = '\0'; + if (sm_strcasecmp(line, "size") == 0) + { + mci->mci_flags |= MCIF_SIZE; + if (p != NULL) + mci->mci_maxsize = atol(p); + } + else if (sm_strcasecmp(line, "8bitmime") == 0) + { + mci->mci_flags |= MCIF_8BITMIME; + mci->mci_flags &= ~MCIF_7BIT; + } + else if (sm_strcasecmp(line, "expn") == 0) + mci->mci_flags |= MCIF_EXPN; + else if (sm_strcasecmp(line, "dsn") == 0) + mci->mci_flags |= MCIF_DSN; + else if (sm_strcasecmp(line, "enhancedstatuscodes") == 0) + mci->mci_flags |= MCIF_ENHSTAT; + else if (sm_strcasecmp(line, "pipelining") == 0) + mci->mci_flags |= MCIF_PIPELINED; + else if (sm_strcasecmp(line, "verb") == 0) + mci->mci_flags |= MCIF_VERB; +#if STARTTLS + else if (sm_strcasecmp(line, "starttls") == 0) + mci->mci_flags |= MCIF_TLS; +#endif /* STARTTLS */ + else if (sm_strcasecmp(line, "deliverby") == 0) + { + mci->mci_flags |= MCIF_DLVR_BY; + if (p != NULL) + mci->mci_min_by = atol(p); + } +#if SASL + else if (sm_strcasecmp(line, "auth") == 0) + { + if (p != NULL && *p != '\0') + { + if (mci->mci_saslcap != NULL) + { + /* + ** Create the union with previous auth + ** offerings because we recognize "auth " + ** and "auth=" (old format). + */ + + mci->mci_saslcap = str_union(mci->mci_saslcap, + p, mci->mci_rpool); + mci->mci_flags |= MCIF_AUTH; + } + else + { + int l; + + l = strlen(p) + 1; + mci->mci_saslcap = (char *) + sm_rpool_malloc(mci->mci_rpool, l); + if (mci->mci_saslcap != NULL) + { + (void) sm_strlcpy(mci->mci_saslcap, p, + l); + mci->mci_flags |= MCIF_AUTH; + } + } + } + } +#endif /* SASL */ +} +#if SASL + +static int getsimple __P((void *, int, const char **, unsigned *)); +static int getsecret __P((sasl_conn_t *, void *, int, sasl_secret_t **)); +static int saslgetrealm __P((void *, int, const char **, const char **)); +static int readauth __P((char *, bool, SASL_AI_T *m, SM_RPOOL_T *)); +static int getauth __P((MCI *, ENVELOPE *, SASL_AI_T *)); +static char *removemech __P((char *, char *, SM_RPOOL_T *)); +static int attemptauth __P((MAILER *, MCI *, ENVELOPE *, SASL_AI_T *)); + +static sasl_callback_t callbacks[] = +{ + { SASL_CB_GETREALM, &saslgetrealm, NULL }, +#define CB_GETREALM_IDX 0 + { SASL_CB_PASS, &getsecret, NULL }, +#define CB_PASS_IDX 1 + { SASL_CB_USER, &getsimple, NULL }, +#define CB_USER_IDX 2 + { SASL_CB_AUTHNAME, &getsimple, NULL }, +#define CB_AUTHNAME_IDX 3 + { SASL_CB_VERIFYFILE, &safesaslfile, NULL }, +#define CB_SAFESASL_IDX 4 + { SASL_CB_LIST_END, NULL, NULL } +}; + +/* +** INIT_SASL_CLIENT -- initialize client side of Cyrus-SASL +** +** Parameters: +** none. +** +** Returns: +** SASL_OK -- if successful. +** SASL error code -- otherwise. +** +** Side Effects: +** checks/sets sasl_clt_init. +** +** Note: +** Callbacks are ignored if sasl_client_init() has +** been called before (by a library such as libnss_ldap) +*/ + +static bool sasl_clt_init = false; + +static int +init_sasl_client() +{ + int result; + + if (sasl_clt_init) + return SASL_OK; + result = sasl_client_init(callbacks); + + /* should we retry later again or just remember that it failed? */ + if (result == SASL_OK) + sasl_clt_init = true; + return result; +} +/* +** STOP_SASL_CLIENT -- shutdown client side of Cyrus-SASL +** +** Parameters: +** none. +** +** Returns: +** none. +** +** Side Effects: +** checks/sets sasl_clt_init. +*/ + +void +stop_sasl_client() +{ + if (!sasl_clt_init) + return; + sasl_clt_init = false; + sasl_done(); +} +/* +** GETSASLDATA -- process the challenges from the SASL protocol +** +** This gets the relevant sasl response data out of the reply +** from the server. +** +** Parameters: +** line -- the response line. +** firstline -- set if this is the first line of the reply. +** m -- the mailer. +** mci -- the mailer connection info. +** e -- the envelope (unused). +** +** Returns: +** none. +*/ + +static void getsasldata __P((char *, bool, MAILER *, MCI *, ENVELOPE *)); + +static void +getsasldata(line, firstline, m, mci, e) + char *line; + bool firstline; + MAILER *m; + register MCI *mci; + ENVELOPE *e; +{ + int len; + int result; +# if SASL < 20000 + char *out; +# endif /* SASL < 20000 */ + + /* if not a continue we don't care about it */ + len = strlen(line); + if ((len <= 4) || + (line[0] != '3') || + !isascii(line[1]) || !isdigit(line[1]) || + !isascii(line[2]) || !isdigit(line[2])) + { + SM_FREE_CLR(mci->mci_sasl_string); + return; + } + + /* forget about "334 " */ + line += 4; + len -= 4; +# if SASL >= 20000 + /* XXX put this into a macro/function? It's duplicated below */ + if (mci->mci_sasl_string != NULL) + { + if (mci->mci_sasl_string_len <= len) + { + sm_free(mci->mci_sasl_string); /* XXX */ + mci->mci_sasl_string = xalloc(len + 1); + } + } + else + mci->mci_sasl_string = xalloc(len + 1); + + result = sasl_decode64(line, len, mci->mci_sasl_string, len + 1, + (unsigned int *) &mci->mci_sasl_string_len); + if (result != SASL_OK) + { + mci->mci_sasl_string_len = 0; + *mci->mci_sasl_string = '\0'; + } +# else /* SASL >= 20000 */ + out = (char *) sm_rpool_malloc_x(mci->mci_rpool, len + 1); + result = sasl_decode64(line, len, out, (unsigned int *) &len); + if (result != SASL_OK) + { + len = 0; + *out = '\0'; + } + + /* + ** mci_sasl_string is "shared" with Cyrus-SASL library; hence + ** it can't be in an rpool unless we use the same memory + ** management mechanism (with same rpool!) for Cyrus SASL. + */ + + if (mci->mci_sasl_string != NULL) + { + if (mci->mci_sasl_string_len <= len) + { + sm_free(mci->mci_sasl_string); /* XXX */ + mci->mci_sasl_string = xalloc(len + 1); + } + } + else + mci->mci_sasl_string = xalloc(len + 1); + + memcpy(mci->mci_sasl_string, out, len); + mci->mci_sasl_string[len] = '\0'; + mci->mci_sasl_string_len = len; +# endif /* SASL >= 20000 */ + return; +} +/* +** READAUTH -- read auth values from a file +** +** Parameters: +** filename -- name of file to read. +** safe -- if set, this is a safe read. +** sai -- where to store auth_info. +** rpool -- resource pool for sai. +** +** Returns: +** EX_OK -- data succesfully read. +** EX_UNAVAILABLE -- no valid filename. +** EX_TEMPFAIL -- temporary failure. +*/ + +static char *sasl_info_name[] = +{ + "user id", + "authentication id", + "password", + "realm", + "mechlist" +}; +static int +readauth(filename, safe, sai, rpool) + char *filename; + bool safe; + SASL_AI_T *sai; + SM_RPOOL_T *rpool; +{ + SM_FILE_T *f; + long sff; + pid_t pid; + int lc; + char *s; + char buf[MAXLINE]; + + if (filename == NULL || filename[0] == '\0') + return EX_UNAVAILABLE; + +#if !_FFR_ALLOW_SASLINFO + /* + ** make sure we don't use a program that is not + ** accesible to the user who specified a different authinfo file. + ** However, currently we don't pass this info (authinfo file + ** specified by user) around, so we just turn off program access. + */ + + if (filename[0] == '|') + { + auto int fd; + int i; + char *p; + char *argv[MAXPV + 1]; + + i = 0; + for (p = strtok(&filename[1], " \t"); p != NULL; + p = strtok(NULL, " \t")) + { + if (i >= MAXPV) + break; + argv[i++] = p; + } + argv[i] = NULL; + pid = prog_open(argv, &fd, CurEnv); + if (pid < 0) + f = NULL; + else + f = sm_io_open(SmFtStdiofd, SM_TIME_DEFAULT, + (void *) &fd, SM_IO_RDONLY, NULL); + } + else +#endif /* !_FFR_ALLOW_SASLINFO */ + { + pid = -1; + sff = SFF_REGONLY|SFF_SAFEDIRPATH|SFF_NOWLINK + |SFF_NOGWFILES|SFF_NOWWFILES|SFF_NOWRFILES; +# if _FFR_GROUPREADABLEAUTHINFOFILE + if (!bitnset(DBS_GROUPREADABLEAUTHINFOFILE, DontBlameSendmail)) +# endif /* _FFR_GROUPREADABLEAUTHINFOFILE */ + sff |= SFF_NOGRFILES; + if (DontLockReadFiles) + sff |= SFF_NOLOCK; + +#if _FFR_ALLOW_SASLINFO + /* + ** XXX: make sure we don't read or open files that are not + ** accesible to the user who specified a different authinfo + ** file. + */ + + sff |= SFF_MUSTOWN; +#else /* _FFR_ALLOW_SASLINFO */ + if (safe) + sff |= SFF_OPENASROOT; +#endif /* _FFR_ALLOW_SASLINFO */ + + f = safefopen(filename, O_RDONLY, 0, sff); + } + if (f == NULL) + { + if (LogLevel > 5) + sm_syslog(LOG_ERR, NOQID, + "AUTH=client, error: can't open %s: %s", + filename, sm_errstring(errno)); + return EX_TEMPFAIL; + } + + lc = 0; + while (lc <= SASL_MECHLIST && + sm_io_fgets(f, SM_TIME_DEFAULT, buf, sizeof(buf)) != NULL) + { + if (buf[0] != '#') + { + (*sai)[lc] = sm_rpool_strdup_x(rpool, buf); + if ((s = strchr((*sai)[lc], '\n')) != NULL) + *s = '\0'; + lc++; + } + } + + (void) sm_io_close(f, SM_TIME_DEFAULT); + if (pid > 0) + (void) waitfor(pid); + if (lc < SASL_PASSWORD) + { + if (LogLevel > 8) + sm_syslog(LOG_ERR, NOQID, + "AUTH=client, error: can't read %s from %s", + sasl_info_name[lc + 1], filename); + return EX_TEMPFAIL; + } + return EX_OK; +} + +/* +** GETAUTH -- get authinfo from ruleset call +** +** {server_name}, {server_addr} must be set +** +** Parameters: +** mci -- the mailer connection structure. +** e -- the envelope (including the sender to specify). +** sai -- pointer to authinfo (result). +** +** Returns: +** EX_OK -- ruleset was succesfully called, data may not +** be available, sai must be checked. +** EX_UNAVAILABLE -- ruleset unavailable (or failed). +** EX_TEMPFAIL -- temporary failure (from ruleset). +** +** Side Effects: +** Fills in sai if successful. +*/ + +static int +getauth(mci, e, sai) + MCI *mci; + ENVELOPE *e; + SASL_AI_T *sai; +{ + int i, r, l, got, ret; + char **pvp; + char pvpbuf[PSBUFSIZE]; + + r = rscap("authinfo", macvalue(macid("{server_name}"), e), + macvalue(macid("{server_addr}"), e), e, + &pvp, pvpbuf, sizeof(pvpbuf)); + + if (r != EX_OK) + return EX_UNAVAILABLE; + + /* other than expected return value: ok (i.e., no auth) */ + if (pvp == NULL || pvp[0] == NULL || (pvp[0][0] & 0377) != CANONNET) + return EX_OK; + if (pvp[1] != NULL && sm_strncasecmp(pvp[1], "temp", 4) == 0) + return EX_TEMPFAIL; + + /* + ** parse the data, put it into sai + ** format: "TDstring" (including the '"' !) + ** where T is a tag: 'U', ... + ** D is a delimiter: ':' or '=' + */ + + ret = EX_OK; /* default return value */ + i = 0; + got = 0; + while (i < SASL_ENTRIES) + { + if (pvp[i + 1] == NULL) + break; + if (pvp[i + 1][0] != '"') + break; + switch (pvp[i + 1][1]) + { + case 'U': + case 'u': + r = SASL_USER; + break; + case 'I': + case 'i': + r = SASL_AUTHID; + break; + case 'P': + case 'p': + r = SASL_PASSWORD; + break; + case 'R': + case 'r': + r = SASL_DEFREALM; + break; + case 'M': + case 'm': + r = SASL_MECHLIST; + break; + default: + goto fail; + } + l = strlen(pvp[i + 1]); + + /* check syntax */ + if (l <= 3 || pvp[i + 1][l - 1] != '"') + goto fail; + + /* remove closing quote */ + pvp[i + 1][l - 1] = '\0'; + + /* remove "TD and " */ + l -= 4; + (*sai)[r] = (char *) sm_rpool_malloc(mci->mci_rpool, l + 1); + if ((*sai)[r] == NULL) + goto tempfail; + if (pvp[i + 1][2] == ':') + { + /* ':text' (just copy) */ + (void) sm_strlcpy((*sai)[r], pvp[i + 1] + 3, l + 1); + got |= 1 << r; + } + else if (pvp[i + 1][2] == '=') + { + unsigned int len; + + /* '=base64' (decode) */ +# if SASL >= 20000 + ret = sasl_decode64(pvp[i + 1] + 3, + (unsigned int) l, (*sai)[r], + (unsigned int) l + 1, &len); +# else /* SASL >= 20000 */ + ret = sasl_decode64(pvp[i + 1] + 3, + (unsigned int) l, (*sai)[r], &len); +# endif /* SASL >= 20000 */ + if (ret != SASL_OK) + goto fail; + got |= 1 << r; + } + else + goto fail; + if (tTd(95, 5)) + sm_syslog(LOG_DEBUG, NOQID, "getauth %s=%s", + sasl_info_name[r], (*sai)[r]); + ++i; + } + + /* did we get the expected data? */ + /* XXX: EXTERNAL mechanism only requires (and only uses) SASL_USER */ + if (!(bitset(SASL_USER_BIT|SASL_AUTHID_BIT, got) && + bitset(SASL_PASSWORD_BIT, got))) + goto fail; + + /* no authid? copy uid */ + if (!bitset(SASL_AUTHID_BIT, got)) + { + l = strlen((*sai)[SASL_USER]) + 1; + (*sai)[SASL_AUTHID] = (char *) sm_rpool_malloc(mci->mci_rpool, + l + 1); + if ((*sai)[SASL_AUTHID] == NULL) + goto tempfail; + (void) sm_strlcpy((*sai)[SASL_AUTHID], (*sai)[SASL_USER], l); + } + + /* no uid? copy authid */ + if (!bitset(SASL_USER_BIT, got)) + { + l = strlen((*sai)[SASL_AUTHID]) + 1; + (*sai)[SASL_USER] = (char *) sm_rpool_malloc(mci->mci_rpool, + l + 1); + if ((*sai)[SASL_USER] == NULL) + goto tempfail; + (void) sm_strlcpy((*sai)[SASL_USER], (*sai)[SASL_AUTHID], l); + } + return EX_OK; + + tempfail: + ret = EX_TEMPFAIL; + fail: + if (LogLevel > 8) + sm_syslog(LOG_WARNING, NOQID, + "AUTH=client, relay=%.64s [%.16s], authinfo %sfailed", + macvalue(macid("{server_name}"), e), + macvalue(macid("{server_addr}"), e), + ret == EX_TEMPFAIL ? "temp" : ""); + for (i = 0; i <= SASL_MECHLIST; i++) + (*sai)[i] = NULL; /* just clear; rpool */ + return ret; +} + +# if SASL >= 20000 +/* +** GETSIMPLE -- callback to get userid or authid +** +** Parameters: +** context -- sai +** id -- what to do +** result -- (pointer to) result +** len -- (pointer to) length of result +** +** Returns: +** OK/failure values +*/ + +static int +getsimple(context, id, result, len) + void *context; + int id; + const char **result; + unsigned *len; +{ + SASL_AI_T *sai; + + if (result == NULL || context == NULL) + return SASL_BADPARAM; + sai = (SASL_AI_T *) context; + + switch (id) + { + case SASL_CB_USER: + *result = (*sai)[SASL_USER]; + if (tTd(95, 5)) + sm_syslog(LOG_DEBUG, NOQID, "AUTH username '%s'", + *result); + if (len != NULL) + *len = *result != NULL ? strlen(*result) : 0; + break; + + case SASL_CB_AUTHNAME: + *result = (*sai)[SASL_AUTHID]; + if (tTd(95, 5)) + sm_syslog(LOG_DEBUG, NOQID, "AUTH authid '%s'", + *result); + if (len != NULL) + *len = *result != NULL ? strlen(*result) : 0; + break; + + case SASL_CB_LANGUAGE: + *result = NULL; + if (len != NULL) + *len = 0; + break; + + default: + return SASL_BADPARAM; + } + return SASL_OK; +} +/* +** GETSECRET -- callback to get password +** +** Parameters: +** conn -- connection information +** context -- sai +** id -- what to do +** psecret -- (pointer to) result +** +** Returns: +** OK/failure values +*/ + +static int +getsecret(conn, context, id, psecret) + sasl_conn_t *conn; + SM_UNUSED(void *context); + int id; + sasl_secret_t **psecret; +{ + int len; + char *authpass; + MCI *mci; + + if (conn == NULL || psecret == NULL || id != SASL_CB_PASS) + return SASL_BADPARAM; + + mci = (MCI *) context; + authpass = mci->mci_sai[SASL_PASSWORD]; + len = strlen(authpass); + + /* + ** use an rpool because we are responsible for free()ing the secret, + ** but we can't free() it until after the auth completes + */ + + *psecret = (sasl_secret_t *) sm_rpool_malloc(mci->mci_rpool, + sizeof(sasl_secret_t) + + len + 1); + if (*psecret == NULL) + return SASL_FAIL; + (void) sm_strlcpy((char *) (*psecret)->data, authpass, len + 1); + (*psecret)->len = (unsigned long) len; + return SASL_OK; +} +# else /* SASL >= 20000 */ +/* +** GETSIMPLE -- callback to get userid or authid +** +** Parameters: +** context -- sai +** id -- what to do +** result -- (pointer to) result +** len -- (pointer to) length of result +** +** Returns: +** OK/failure values +*/ + +static int +getsimple(context, id, result, len) + void *context; + int id; + const char **result; + unsigned *len; +{ + char *h, *s; +# if SASL > 10509 + bool addrealm; +# endif /* SASL > 10509 */ + size_t l; + SASL_AI_T *sai; + char *authid = NULL; + + if (result == NULL || context == NULL) + return SASL_BADPARAM; + sai = (SASL_AI_T *) context; + + /* + ** Unfortunately it is not clear whether this routine should + ** return a copy of a string or just a pointer to a string. + ** The Cyrus-SASL plugins treat these return values differently, e.g., + ** plugins/cram.c free()s authid, plugings/digestmd5.c does not. + ** The best solution to this problem is to fix Cyrus-SASL, but it + ** seems there is nobody who creates patches... Hello CMU!? + ** The second best solution is to have flags that tell this routine + ** whether to return an malloc()ed copy. + ** The next best solution is to always return an malloc()ed copy, + ** and suffer from some memory leak, which is ugly for persistent + ** queue runners. + ** For now we go with the last solution... + ** We can't use rpools (which would avoid this particular problem) + ** as explained in sasl.c. + */ + + switch (id) + { + case SASL_CB_USER: + l = strlen((*sai)[SASL_USER]) + 1; + s = sm_sasl_malloc(l); + if (s == NULL) + { + if (len != NULL) + *len = 0; + *result = NULL; + return SASL_NOMEM; + } + (void) sm_strlcpy(s, (*sai)[SASL_USER], l); + *result = s; + if (tTd(95, 5)) + sm_syslog(LOG_DEBUG, NOQID, "AUTH username '%s'", + *result); + if (len != NULL) + *len = *result != NULL ? strlen(*result) : 0; + break; + + case SASL_CB_AUTHNAME: + h = (*sai)[SASL_AUTHID]; +# if SASL > 10509 + /* XXX maybe other mechanisms too?! */ + addrealm = (*sai)[SASL_MECH] != NULL && + sm_strcasecmp((*sai)[SASL_MECH], "CRAM-MD5") == 0; + + /* + ** Add realm to authentication id unless authid contains + ** '@' (i.e., a realm) or the default realm is empty. + */ + + if (addrealm && h != NULL && strchr(h, '@') == NULL) + { + /* has this been done before? */ + if ((*sai)[SASL_ID_REALM] == NULL) + { + char *realm; + + realm = (*sai)[SASL_DEFREALM]; + + /* do not add an empty realm */ + if (*realm == '\0') + { + authid = h; + (*sai)[SASL_ID_REALM] = NULL; + } + else + { + l = strlen(h) + strlen(realm) + 2; + + /* should use rpool, but from where? */ + authid = sm_sasl_malloc(l); + if (authid != NULL) + { + (void) sm_snprintf(authid, l, + "%s@%s", + h, realm); + (*sai)[SASL_ID_REALM] = authid; + } + else + { + authid = h; + (*sai)[SASL_ID_REALM] = NULL; + } + } + } + else + authid = (*sai)[SASL_ID_REALM]; + } + else +# endif /* SASL > 10509 */ + authid = h; + l = strlen(authid) + 1; + s = sm_sasl_malloc(l); + if (s == NULL) + { + if (len != NULL) + *len = 0; + *result = NULL; + return SASL_NOMEM; + } + (void) sm_strlcpy(s, authid, l); + *result = s; + if (tTd(95, 5)) + sm_syslog(LOG_DEBUG, NOQID, "AUTH authid '%s'", + *result); + if (len != NULL) + *len = authid ? strlen(authid) : 0; + break; + + case SASL_CB_LANGUAGE: + *result = NULL; + if (len != NULL) + *len = 0; + break; + + default: + return SASL_BADPARAM; + } + return SASL_OK; +} +/* +** GETSECRET -- callback to get password +** +** Parameters: +** conn -- connection information +** context -- sai +** id -- what to do +** psecret -- (pointer to) result +** +** Returns: +** OK/failure values +*/ + +static int +getsecret(conn, context, id, psecret) + sasl_conn_t *conn; + SM_UNUSED(void *context); + int id; + sasl_secret_t **psecret; +{ + int len; + char *authpass; + SASL_AI_T *sai; + + if (conn == NULL || psecret == NULL || id != SASL_CB_PASS) + return SASL_BADPARAM; + + sai = (SASL_AI_T *) context; + authpass = (*sai)[SASL_PASSWORD]; + len = strlen(authpass); + *psecret = (sasl_secret_t *) sm_sasl_malloc(sizeof(sasl_secret_t) + + len + 1); + if (*psecret == NULL) + return SASL_FAIL; + (void) sm_strlcpy((*psecret)->data, authpass, len + 1); + (*psecret)->len = (unsigned long) len; + return SASL_OK; +} +# endif /* SASL >= 20000 */ + +/* +** SAFESASLFILE -- callback for sasl: is file safe? +** +** Parameters: +** context -- pointer to context between invocations (unused) +** file -- name of file to check +** type -- type of file to check +** +** Returns: +** SASL_OK -- file can be used +** SASL_CONTINUE -- don't use file +** SASL_FAIL -- failure (not used here) +** +*/ + +int +#if SASL > 10515 +safesaslfile(context, file, type) +#else /* SASL > 10515 */ +safesaslfile(context, file) +#endif /* SASL > 10515 */ + void *context; +# if SASL >= 20000 + const char *file; +# else /* SASL >= 20000 */ + char *file; +# endif /* SASL >= 20000 */ +#if SASL > 10515 +# if SASL >= 20000 + sasl_verify_type_t type; +# else /* SASL >= 20000 */ + int type; +# endif /* SASL >= 20000 */ +#endif /* SASL > 10515 */ +{ + long sff; + int r; +#if SASL <= 10515 + size_t len; +#endif /* SASL <= 10515 */ + char *p; + + if (file == NULL || *file == '\0') + return SASL_OK; + sff = SFF_SAFEDIRPATH|SFF_NOWLINK|SFF_NOWWFILES|SFF_ROOTOK; +#if SASL <= 10515 + if ((p = strrchr(file, '/')) == NULL) + p = file; + else + ++p; + + /* everything beside libs and .conf files must not be readable */ + len = strlen(p); + if ((len <= 3 || strncmp(p, "lib", 3) != 0) && + (len <= 5 || strncmp(p + len - 5, ".conf", 5) != 0)) + { + if (!bitnset(DBS_GROUPREADABLESASLDBFILE, DontBlameSendmail)) + sff |= SFF_NORFILES; + if (!bitnset(DBS_GROUPWRITABLESASLDBFILE, DontBlameSendmail)) + sff |= SFF_NOGWFILES; + } +#else /* SASL <= 10515 */ + /* files containing passwords should be not readable */ + if (type == SASL_VRFY_PASSWD) + { + if (bitnset(DBS_GROUPREADABLESASLDBFILE, DontBlameSendmail)) + sff |= SFF_NOWRFILES; + else + sff |= SFF_NORFILES; + if (!bitnset(DBS_GROUPWRITABLESASLDBFILE, DontBlameSendmail)) + sff |= SFF_NOGWFILES; + } +#endif /* SASL <= 10515 */ + + p = (char *) file; + if ((r = safefile(p, RunAsUid, RunAsGid, RunAsUserName, sff, + S_IRUSR, NULL)) == 0) + return SASL_OK; + if (LogLevel > (r != ENOENT ? 8 : 10)) + sm_syslog(LOG_WARNING, NOQID, "error: safesasl(%s) failed: %s", + p, sm_errstring(r)); + return SASL_CONTINUE; +} + +/* +** SASLGETREALM -- return the realm for SASL +** +** return the realm for the client +** +** Parameters: +** context -- context shared between invocations +** availrealms -- list of available realms +** {realm, realm, ...} +** result -- pointer to result +** +** Returns: +** failure/success +*/ + +static int +saslgetrealm(context, id, availrealms, result) + void *context; + int id; + const char **availrealms; + const char **result; +{ + char *r; + SASL_AI_T *sai; + + sai = (SASL_AI_T *) context; + if (sai == NULL) + return SASL_FAIL; + r = (*sai)[SASL_DEFREALM]; + + if (LogLevel > 12) + sm_syslog(LOG_INFO, NOQID, + "AUTH=client, realm=%s, available realms=%s", + r == NULL ? "<No Realm>" : r, + (availrealms == NULL || *availrealms == NULL) + ? "<No Realms>" : *availrealms); + + /* check whether context is in list */ + if (availrealms != NULL && *availrealms != NULL) + { + if (iteminlist(context, (char *)(*availrealms + 1), " ,}") == + NULL) + { + if (LogLevel > 8) + sm_syslog(LOG_ERR, NOQID, + "AUTH=client, realm=%s not in list=%s", + r, *availrealms); + return SASL_FAIL; + } + } + *result = r; + return SASL_OK; +} +/* +** ITEMINLIST -- does item appear in list? +** +** Check whether item appears in list (which must be separated by a +** character in delim) as a "word", i.e. it must appear at the begin +** of the list or after a space, and it must end with a space or the +** end of the list. +** +** Parameters: +** item -- item to search. +** list -- list of items. +** delim -- list of delimiters. +** +** Returns: +** pointer to occurrence (NULL if not found). +*/ + +char * +iteminlist(item, list, delim) + char *item; + char *list; + char *delim; +{ + char *s; + int len; + + if (list == NULL || *list == '\0') + return NULL; + if (item == NULL || *item == '\0') + return NULL; + s = list; + len = strlen(item); + while (s != NULL && *s != '\0') + { + if (sm_strncasecmp(s, item, len) == 0 && + (s[len] == '\0' || strchr(delim, s[len]) != NULL)) + return s; + s = strpbrk(s, delim); + if (s != NULL) + while (*++s == ' ') + continue; + } + return NULL; +} +/* +** REMOVEMECH -- remove item [rem] from list [list] +** +** Parameters: +** rem -- item to remove +** list -- list of items +** rpool -- resource pool from which result is allocated. +** +** Returns: +** pointer to new list (NULL in case of error). +*/ + +static char * +removemech(rem, list, rpool) + char *rem; + char *list; + SM_RPOOL_T *rpool; +{ + char *ret; + char *needle; + int len; + + if (list == NULL) + return NULL; + if (rem == NULL || *rem == '\0') + { + /* take out what? */ + return NULL; + } + + /* find the item in the list */ + if ((needle = iteminlist(rem, list, " ")) == NULL) + { + /* not in there: return original */ + return list; + } + + /* length of string without rem */ + len = strlen(list) - strlen(rem); + if (len <= 0) + { + ret = (char *) sm_rpool_malloc_x(rpool, 1); + *ret = '\0'; + return ret; + } + ret = (char *) sm_rpool_malloc_x(rpool, len); + memset(ret, '\0', len); + + /* copy from start to removed item */ + memcpy(ret, list, needle - list); + + /* length of rest of string past removed item */ + len = strlen(needle) - strlen(rem) - 1; + if (len > 0) + { + /* not last item -- copy into string */ + memcpy(ret + (needle - list), + list + (needle - list) + strlen(rem) + 1, + len); + } + else + ret[(needle - list) - 1] = '\0'; + return ret; +} +/* +** ATTEMPTAUTH -- try to AUTHenticate using one mechanism +** +** Parameters: +** m -- the mailer. +** mci -- the mailer connection structure. +** e -- the envelope (including the sender to specify). +** sai - sasl authinfo +** +** Returns: +** EX_OK -- authentication was successful. +** EX_NOPERM -- authentication failed. +** EX_IOERR -- authentication dialogue failed (I/O problem?). +** EX_TEMPFAIL -- temporary failure. +** +*/ + +static int +attemptauth(m, mci, e, sai) + MAILER *m; + MCI *mci; + ENVELOPE *e; + SASL_AI_T *sai; +{ + int saslresult, smtpresult; +# if SASL >= 20000 + sasl_ssf_t ssf; + const char *auth_id; + const char *out; +# else /* SASL >= 20000 */ + sasl_external_properties_t ssf; + char *out; +# endif /* SASL >= 20000 */ + unsigned int outlen; + sasl_interact_t *client_interact = NULL; + char *mechusing; + sasl_security_properties_t ssp; + + /* MUST NOT be a multiple of 4: bug in some sasl_encode64() versions */ + char in64[MAXOUTLEN + 1]; +#if NETINET || (NETINET6 && SASL >= 20000) + extern SOCKADDR CurHostAddr; +#endif /* NETINET || (NETINET6 && SASL >= 20000) */ + + /* no mechanism selected (yet) */ + (*sai)[SASL_MECH] = NULL; + + /* dispose old connection */ + if (mci->mci_conn != NULL) + sasl_dispose(&(mci->mci_conn)); + + /* make a new client sasl connection */ +# if SASL >= 20000 + /* + ** We provide the callbacks again because global callbacks in + ** sasl_client_init() are ignored if SASL has been initialized + ** before, for example, by a library such as libnss-ldap. + */ + + saslresult = sasl_client_new(bitnset(M_LMTP, m->m_flags) ? "lmtp" + : "smtp", + CurHostName, NULL, NULL, callbacks, 0, + &mci->mci_conn); +# else /* SASL >= 20000 */ + saslresult = sasl_client_new(bitnset(M_LMTP, m->m_flags) ? "lmtp" + : "smtp", + CurHostName, NULL, 0, &mci->mci_conn); +# endif /* SASL >= 20000 */ + if (saslresult != SASL_OK) + return EX_TEMPFAIL; + + /* set properties */ + (void) memset(&ssp, '\0', sizeof(ssp)); + + /* XXX should these be options settable via .cf ? */ + { + ssp.max_ssf = MaxSLBits; + ssp.maxbufsize = MAXOUTLEN; +# if 0 + ssp.security_flags = SASL_SEC_NOPLAINTEXT; +# endif /* 0 */ + } + saslresult = sasl_setprop(mci->mci_conn, SASL_SEC_PROPS, &ssp); + if (saslresult != SASL_OK) + return EX_TEMPFAIL; + +# if SASL >= 20000 + /* external security strength factor, authentication id */ + ssf = 0; + auth_id = NULL; +# if STARTTLS + out = macvalue(macid("{cert_subject}"), e); + if (out != NULL && *out != '\0') + auth_id = out; + out = macvalue(macid("{cipher_bits}"), e); + if (out != NULL && *out != '\0') + ssf = atoi(out); +# endif /* STARTTLS */ + saslresult = sasl_setprop(mci->mci_conn, SASL_SSF_EXTERNAL, &ssf); + if (saslresult != SASL_OK) + return EX_TEMPFAIL; + saslresult = sasl_setprop(mci->mci_conn, SASL_AUTH_EXTERNAL, auth_id); + if (saslresult != SASL_OK) + return EX_TEMPFAIL; + +# if NETINET || NETINET6 + /* set local/remote ipv4 addresses */ + if (mci->mci_out != NULL && ( +# if NETINET6 + CurHostAddr.sa.sa_family == AF_INET6 || +# endif /* NETINET6 */ + CurHostAddr.sa.sa_family == AF_INET)) + { + SOCKADDR_LEN_T addrsize; + SOCKADDR saddr_l; + char localip[60], remoteip[60]; + + switch (CurHostAddr.sa.sa_family) + { + case AF_INET: + addrsize = sizeof(struct sockaddr_in); + break; +# if NETINET6 + case AF_INET6: + addrsize = sizeof(struct sockaddr_in6); + break; +# endif /* NETINET6 */ + default: + break; + } + if (iptostring(&CurHostAddr, addrsize, + remoteip, sizeof(remoteip))) + { + if (sasl_setprop(mci->mci_conn, SASL_IPREMOTEPORT, + remoteip) != SASL_OK) + return EX_TEMPFAIL; + } + addrsize = sizeof(saddr_l); + if (getsockname(sm_io_getinfo(mci->mci_out, SM_IO_WHAT_FD, + NULL), + (struct sockaddr *) &saddr_l, &addrsize) == 0) + { + if (iptostring(&saddr_l, addrsize, + localip, sizeof(localip))) + { + if (sasl_setprop(mci->mci_conn, + SASL_IPLOCALPORT, + localip) != SASL_OK) + return EX_TEMPFAIL; + } + } + } +# endif /* NETINET || NETINET6 */ + + /* start client side of sasl */ + saslresult = sasl_client_start(mci->mci_conn, mci->mci_saslcap, + &client_interact, + &out, &outlen, + (const char **) &mechusing); +# else /* SASL >= 20000 */ + /* external security strength factor, authentication id */ + ssf.ssf = 0; + ssf.auth_id = NULL; +# if STARTTLS + out = macvalue(macid("{cert_subject}"), e); + if (out != NULL && *out != '\0') + ssf.auth_id = out; + out = macvalue(macid("{cipher_bits}"), e); + if (out != NULL && *out != '\0') + ssf.ssf = atoi(out); +# endif /* STARTTLS */ + saslresult = sasl_setprop(mci->mci_conn, SASL_SSF_EXTERNAL, &ssf); + if (saslresult != SASL_OK) + return EX_TEMPFAIL; + +# if NETINET + /* set local/remote ipv4 addresses */ + if (mci->mci_out != NULL && CurHostAddr.sa.sa_family == AF_INET) + { + SOCKADDR_LEN_T addrsize; + struct sockaddr_in saddr_l; + + if (sasl_setprop(mci->mci_conn, SASL_IP_REMOTE, + (struct sockaddr_in *) &CurHostAddr) + != SASL_OK) + return EX_TEMPFAIL; + addrsize = sizeof(struct sockaddr_in); + if (getsockname(sm_io_getinfo(mci->mci_out, SM_IO_WHAT_FD, + NULL), + (struct sockaddr *) &saddr_l, &addrsize) == 0) + { + if (sasl_setprop(mci->mci_conn, SASL_IP_LOCAL, + &saddr_l) != SASL_OK) + return EX_TEMPFAIL; + } + } +# endif /* NETINET */ + + /* start client side of sasl */ + saslresult = sasl_client_start(mci->mci_conn, mci->mci_saslcap, + NULL, &client_interact, + &out, &outlen, + (const char **) &mechusing); +# endif /* SASL >= 20000 */ + + if (saslresult != SASL_OK && saslresult != SASL_CONTINUE) + { + if (saslresult == SASL_NOMECH && LogLevel > 8) + { + sm_syslog(LOG_NOTICE, e->e_id, + "AUTH=client, available mechanisms do not fulfill requirements"); + } + return EX_TEMPFAIL; + } + + /* just point current mechanism to the data in the sasl library */ + (*sai)[SASL_MECH] = mechusing; + + /* send the info across the wire */ + if (out == NULL + /* login and digest-md5 up to 1.5.28 set out="" */ + || (outlen == 0 && + (sm_strcasecmp(mechusing, "LOGIN") == 0 || + sm_strcasecmp(mechusing, "DIGEST-MD5") == 0)) + ) + { + /* no initial response */ + smtpmessage("AUTH %s", m, mci, mechusing); + } + else if (outlen == 0) + { + /* + ** zero-length initial response, per RFC 2554 4.: + ** "Unlike a zero-length client answer to a 334 reply, a zero- + ** length initial response is sent as a single equals sign" + */ + + smtpmessage("AUTH %s =", m, mci, mechusing); + } + else + { + saslresult = sasl_encode64(out, outlen, in64, sizeof(in64), + NULL); + if (saslresult != SASL_OK) /* internal error */ + { + if (LogLevel > 8) + sm_syslog(LOG_ERR, e->e_id, + "encode64 for AUTH failed"); + return EX_TEMPFAIL; + } + smtpmessage("AUTH %s %s", m, mci, mechusing, in64); + } +# if SASL < 20000 + sm_sasl_free(out); /* XXX only if no rpool is used */ +# endif /* SASL < 20000 */ + + /* get the reply */ + smtpresult = reply(m, mci, e, TimeOuts.to_auth, getsasldata, NULL, + XS_AUTH); + + for (;;) + { + /* check return code from server */ + if (smtpresult == 235) + { + macdefine(&mci->mci_macro, A_TEMP, macid("{auth_type}"), + mechusing); + return EX_OK; + } + if (smtpresult == -1) + return EX_IOERR; + if (REPLYTYPE(smtpresult) == 5) + return EX_NOPERM; /* ugly, but ... */ + if (REPLYTYPE(smtpresult) != 3) + { + /* should we fail deliberately, see RFC 2554 4. ? */ + /* smtpmessage("*", m, mci); */ + return EX_TEMPFAIL; + } + + saslresult = sasl_client_step(mci->mci_conn, + mci->mci_sasl_string, + mci->mci_sasl_string_len, + &client_interact, + &out, &outlen); + + if (saslresult != SASL_OK && saslresult != SASL_CONTINUE) + { + if (tTd(95, 5)) + sm_dprintf("AUTH FAIL=%s (%d)\n", + sasl_errstring(saslresult, NULL, NULL), + saslresult); + + /* fail deliberately, see RFC 2554 4. */ + smtpmessage("*", m, mci); + + /* + ** but we should only fail for this authentication + ** mechanism; how to do that? + */ + + smtpresult = reply(m, mci, e, TimeOuts.to_auth, + getsasldata, NULL, XS_AUTH); + return EX_NOPERM; + } + + if (outlen > 0) + { + saslresult = sasl_encode64(out, outlen, in64, + sizeof(in64), NULL); + if (saslresult != SASL_OK) + { + /* give an error reply to the other side! */ + smtpmessage("*", m, mci); + return EX_TEMPFAIL; + } + } + else + in64[0] = '\0'; +# if SASL < 20000 + sm_sasl_free(out); /* XXX only if no rpool is used */ +# endif /* SASL < 20000 */ + smtpmessage("%s", m, mci, in64); + smtpresult = reply(m, mci, e, TimeOuts.to_auth, + getsasldata, NULL, XS_AUTH); + } + /* NOTREACHED */ +} +/* +** SMTPAUTH -- try to AUTHenticate +** +** This will try mechanisms in the order the sasl library decided until: +** - there are no more mechanisms +** - a mechanism succeeds +** - the sasl library fails initializing +** +** Parameters: +** m -- the mailer. +** mci -- the mailer connection info. +** e -- the envelope. +** +** Returns: +** EX_OK -- authentication was successful +** EX_UNAVAILABLE -- authentication not possible, e.g., +** no data available. +** EX_NOPERM -- authentication failed. +** EX_TEMPFAIL -- temporary failure. +** +** Notice: AuthInfo is used for all connections, hence we must +** return EX_TEMPFAIL only if we really want to retry, i.e., +** iff getauth() tempfailed or getauth() was used and +** authentication tempfailed. +*/ + +int +smtpauth(m, mci, e) + MAILER *m; + MCI *mci; + ENVELOPE *e; +{ + int result; + int i; + bool usedgetauth; + + mci->mci_sasl_auth = false; + for (i = 0; i < SASL_MECH ; i++) + mci->mci_sai[i] = NULL; + + result = getauth(mci, e, &(mci->mci_sai)); + if (result == EX_TEMPFAIL) + return result; + usedgetauth = true; + + /* no data available: don't try to authenticate */ + if (result == EX_OK && mci->mci_sai[SASL_AUTHID] == NULL) + return result; + if (result != EX_OK) + { + if (SASLInfo == NULL) + return EX_UNAVAILABLE; + + /* read authinfo from file */ + result = readauth(SASLInfo, true, &(mci->mci_sai), + mci->mci_rpool); + if (result != EX_OK) + return result; + usedgetauth = false; + } + + /* check whether sufficient data is available */ + if (mci->mci_sai[SASL_PASSWORD] == NULL || + *(mci->mci_sai)[SASL_PASSWORD] == '\0') + return EX_UNAVAILABLE; + if ((mci->mci_sai[SASL_AUTHID] == NULL || + *(mci->mci_sai)[SASL_AUTHID] == '\0') && + (mci->mci_sai[SASL_USER] == NULL || + *(mci->mci_sai)[SASL_USER] == '\0')) + return EX_UNAVAILABLE; + + /* set the context for the callback function to sai */ +# if SASL >= 20000 + callbacks[CB_PASS_IDX].context = (void *) mci; +# else /* SASL >= 20000 */ + callbacks[CB_PASS_IDX].context = (void *) &mci->mci_sai; +# endif /* SASL >= 20000 */ + callbacks[CB_USER_IDX].context = (void *) &mci->mci_sai; + callbacks[CB_AUTHNAME_IDX].context = (void *) &mci->mci_sai; + callbacks[CB_GETREALM_IDX].context = (void *) &mci->mci_sai; +#if 0 + callbacks[CB_SAFESASL_IDX].context = (void *) &mci->mci_sai; +#endif /* 0 */ + + /* set default value for realm */ + if ((mci->mci_sai)[SASL_DEFREALM] == NULL) + (mci->mci_sai)[SASL_DEFREALM] = sm_rpool_strdup_x(e->e_rpool, + macvalue('j', CurEnv)); + + /* set default value for list of mechanism to use */ + if ((mci->mci_sai)[SASL_MECHLIST] == NULL || + *(mci->mci_sai)[SASL_MECHLIST] == '\0') + (mci->mci_sai)[SASL_MECHLIST] = AuthMechanisms; + + /* create list of mechanisms to try */ + mci->mci_saslcap = intersect((mci->mci_sai)[SASL_MECHLIST], + mci->mci_saslcap, mci->mci_rpool); + + /* initialize sasl client library */ + result = init_sasl_client(); + if (result != SASL_OK) + return usedgetauth ? EX_TEMPFAIL : EX_UNAVAILABLE; + do + { + result = attemptauth(m, mci, e, &(mci->mci_sai)); + if (result == EX_OK) + mci->mci_sasl_auth = true; + else if (result == EX_TEMPFAIL || result == EX_NOPERM) + { + mci->mci_saslcap = removemech((mci->mci_sai)[SASL_MECH], + mci->mci_saslcap, + mci->mci_rpool); + if (mci->mci_saslcap == NULL || + *(mci->mci_saslcap) == '\0') + return usedgetauth ? result + : EX_UNAVAILABLE; + } + else + return result; + } while (result != EX_OK); + return result; +} +#endif /* SASL */ + +/* +** SMTPMAILFROM -- send MAIL command +** +** Parameters: +** m -- the mailer. +** mci -- the mailer connection structure. +** e -- the envelope (including the sender to specify). +*/ + +int +smtpmailfrom(m, mci, e) + MAILER *m; + MCI *mci; + ENVELOPE *e; +{ + int r; + char *bufp; + char *bodytype; + char *enhsc; + char buf[MAXNAME + 1]; + char optbuf[MAXLINE]; + + if (tTd(18, 2)) + sm_dprintf("smtpmailfrom: CurHost=%s\n", CurHostName); + enhsc = NULL; + + /* + ** Check if connection is gone, if so + ** it's a tempfail and we use mci_errno + ** for the reason. + */ + + if (mci->mci_state == MCIS_CLOSED) + { + errno = mci->mci_errno; + return EX_TEMPFAIL; + } + + /* set up appropriate options to include */ + if (bitset(MCIF_SIZE, mci->mci_flags) && e->e_msgsize > 0) + { + (void) sm_snprintf(optbuf, sizeof(optbuf), " SIZE=%ld", + e->e_msgsize); + bufp = &optbuf[strlen(optbuf)]; + } + else + { + optbuf[0] = '\0'; + bufp = optbuf; + } + + bodytype = e->e_bodytype; + if (bitset(MCIF_8BITMIME, mci->mci_flags)) + { + if (bodytype == NULL && + bitset(MM_MIME8BIT, MimeMode) && + bitset(EF_HAS8BIT, e->e_flags) && + !bitset(EF_DONT_MIME, e->e_flags) && + !bitnset(M_8BITS, m->m_flags)) + bodytype = "8BITMIME"; + if (bodytype != NULL && + SPACELEFT(optbuf, bufp) > strlen(bodytype) + 7) + { + (void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp), + " BODY=%s", bodytype); + bufp += strlen(bufp); + } + } + else if (bitnset(M_8BITS, m->m_flags) || + !bitset(EF_HAS8BIT, e->e_flags) || + bitset(MCIF_8BITOK, mci->mci_flags)) + { + /* EMPTY */ + /* just pass it through */ + } +#if MIME8TO7 + else if (bitset(MM_CVTMIME, MimeMode) && + !bitset(EF_DONT_MIME, e->e_flags) && + (!bitset(MM_PASS8BIT, MimeMode) || + bitset(EF_IS_MIME, e->e_flags))) + { + /* must convert from 8bit MIME format to 7bit encoded */ + mci->mci_flags |= MCIF_CVT8TO7; + } +#endif /* MIME8TO7 */ + else if (!bitset(MM_PASS8BIT, MimeMode)) + { + /* cannot just send a 8-bit version */ + extern char MsgBuf[]; + + usrerrenh("5.6.3", "%s does not support 8BITMIME", CurHostName); + mci_setstat(mci, EX_NOTSTICKY, "5.6.3", MsgBuf); + return EX_DATAERR; + } + + if (bitset(MCIF_DSN, mci->mci_flags)) + { + if (e->e_envid != NULL && + SPACELEFT(optbuf, bufp) > strlen(e->e_envid) + 7) + { + (void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp), + " ENVID=%s", e->e_envid); + bufp += strlen(bufp); + } + + /* RET= parameter */ + if (bitset(EF_RET_PARAM, e->e_flags) && + SPACELEFT(optbuf, bufp) > 9) + { + (void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp), + " RET=%s", + bitset(EF_NO_BODY_RETN, e->e_flags) ? + "HDRS" : "FULL"); + bufp += strlen(bufp); + } + } + + if (bitset(MCIF_AUTH, mci->mci_flags) && e->e_auth_param != NULL && + SPACELEFT(optbuf, bufp) > strlen(e->e_auth_param) + 7 +#if SASL + && (!bitset(SASL_AUTH_AUTH, SASLOpts) || mci->mci_sasl_auth) +#endif /* SASL */ + ) + { + (void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp), + " AUTH=%s", e->e_auth_param); + bufp += strlen(bufp); + } + + /* + ** 17 is the max length required, we could use log() to compute + ** the exact length (and check IS_DLVR_TRACE()) + */ + + if (bitset(MCIF_DLVR_BY, mci->mci_flags) && + IS_DLVR_BY(e) && SPACELEFT(optbuf, bufp) > 17) + { + long dby; + + /* + ** Avoid problems with delays (for R) since the check + ** in deliver() whether min-deliver-time is sufficient. + ** Alternatively we could pass the computed time to this + ** function. + */ + + dby = e->e_deliver_by - (curtime() - e->e_ctime); + if (dby <= 0 && IS_DLVR_RETURN(e)) + dby = mci->mci_min_by <= 0 ? 1 : mci->mci_min_by; + (void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp), + " BY=%ld;%c%s", + dby, + IS_DLVR_RETURN(e) ? 'R' : 'N', + IS_DLVR_TRACE(e) ? "T" : ""); + bufp += strlen(bufp); + } + + /* + ** Send the MAIL command. + ** Designates the sender. + */ + + mci->mci_state = MCIS_MAIL; + + if (bitset(EF_RESPONSE, e->e_flags) && + !bitnset(M_NO_NULL_FROM, m->m_flags)) + buf[0] = '\0'; + else + expand("\201g", buf, sizeof(buf), e); + if (buf[0] == '<') + { + /* strip off <angle brackets> (put back on below) */ + bufp = &buf[strlen(buf) - 1]; + if (*bufp == '>') + *bufp = '\0'; + bufp = &buf[1]; + } + else + bufp = buf; + if (bitnset(M_LOCALMAILER, e->e_from.q_mailer->m_flags) || + !bitnset(M_FROMPATH, m->m_flags)) + { + smtpmessage("MAIL From:<%s>%s", m, mci, bufp, optbuf); + } + else + { + smtpmessage("MAIL From:<@%s%c%s>%s", m, mci, MyHostName, + *bufp == '@' ? ',' : ':', bufp, optbuf); + } + SmtpPhase = mci->mci_phase = "client MAIL"; + sm_setproctitle(true, e, "%s %s: %s", qid_printname(e), + CurHostName, mci->mci_phase); + r = reply(m, mci, e, TimeOuts.to_mail, NULL, &enhsc, XS_DEFAULT); + if (r < 0) + { + /* communications failure */ + mci_setstat(mci, EX_TEMPFAIL, "4.4.2", NULL); + return EX_TEMPFAIL; + } + else if (r == SMTPCLOSING) + { + /* service shutting down: handled by reply() */ + return EX_TEMPFAIL; + } + else if (REPLYTYPE(r) == 4) + { + mci_setstat(mci, EX_NOTSTICKY, ENHSCN(enhsc, smtptodsn(r)), + SmtpReplyBuffer); + return EX_TEMPFAIL; + } + else if (REPLYTYPE(r) == 2) + { + return EX_OK; + } + else if (r == 501) + { + /* syntax error in arguments */ + mci_setstat(mci, EX_NOTSTICKY, ENHSCN(enhsc, "5.5.2"), + SmtpReplyBuffer); + return EX_DATAERR; + } + else if (r == 553) + { + /* mailbox name not allowed */ + mci_setstat(mci, EX_NOTSTICKY, ENHSCN(enhsc, "5.1.3"), + SmtpReplyBuffer); + return EX_DATAERR; + } + else if (r == 552) + { + /* exceeded storage allocation */ + mci_setstat(mci, EX_NOTSTICKY, ENHSCN(enhsc, "5.3.4"), + SmtpReplyBuffer); + if (bitset(MCIF_SIZE, mci->mci_flags)) + e->e_flags |= EF_NO_BODY_RETN; + return EX_UNAVAILABLE; + } + else if (REPLYTYPE(r) == 5) + { + /* unknown error */ + mci_setstat(mci, EX_NOTSTICKY, ENHSCN(enhsc, "5.0.0"), + SmtpReplyBuffer); + return EX_UNAVAILABLE; + } + + if (LogLevel > 1) + { + sm_syslog(LOG_CRIT, e->e_id, + "%.100s: SMTP MAIL protocol error: %s", + CurHostName, + shortenstring(SmtpReplyBuffer, 403)); + } + + /* protocol error -- close up */ + mci_setstat(mci, EX_PROTOCOL, ENHSCN(enhsc, "5.5.1"), + SmtpReplyBuffer); + smtpquit(m, mci, e); + return EX_PROTOCOL; +} +/* +** SMTPRCPT -- designate recipient. +** +** Parameters: +** to -- address of recipient. +** m -- the mailer we are sending to. +** mci -- the connection info for this transaction. +** e -- the envelope for this transaction. +** +** Returns: +** exit status corresponding to recipient status. +** +** Side Effects: +** Sends the mail via SMTP. +*/ + +int +smtprcpt(to, m, mci, e, ctladdr, xstart) + ADDRESS *to; + register MAILER *m; + MCI *mci; + ENVELOPE *e; + ADDRESS *ctladdr; + time_t xstart; +{ + char *bufp; + char optbuf[MAXLINE]; + +#if PIPELINING + /* + ** If there is status waiting from the other end, read it. + ** This should normally happen because of SMTP pipelining. + */ + + while (mci->mci_nextaddr != NULL && + sm_io_getinfo(mci->mci_in, SM_IO_IS_READABLE, NULL) > 0) + { + int r; + + r = smtprcptstat(mci->mci_nextaddr, m, mci, e); + if (r != EX_OK) + { + markfailure(e, mci->mci_nextaddr, mci, r, false); + giveresponse(r, mci->mci_nextaddr->q_status, m, mci, + ctladdr, xstart, e, to); + } + mci->mci_nextaddr = mci->mci_nextaddr->q_pchain; + } +#endif /* PIPELINING */ + + /* + ** Check if connection is gone, if so + ** it's a tempfail and we use mci_errno + ** for the reason. + */ + + if (mci->mci_state == MCIS_CLOSED) + { + errno = mci->mci_errno; + return EX_TEMPFAIL; + } + + optbuf[0] = '\0'; + bufp = optbuf; + + /* + ** Warning: in the following it is assumed that the free space + ** in bufp is sizeof(optbuf) + */ + + if (bitset(MCIF_DSN, mci->mci_flags)) + { + if (IS_DLVR_NOTIFY(e) && + !bitset(MCIF_DLVR_BY, mci->mci_flags)) + { + /* RFC 2852: 4.1.4.2 */ + if (!bitset(QHASNOTIFY, to->q_flags)) + to->q_flags |= QPINGONFAILURE|QPINGONDELAY|QHASNOTIFY; + else if (bitset(QPINGONSUCCESS, to->q_flags) || + bitset(QPINGONFAILURE, to->q_flags) || + bitset(QPINGONDELAY, to->q_flags)) + to->q_flags |= QPINGONDELAY; + } + + /* NOTIFY= parameter */ + if (bitset(QHASNOTIFY, to->q_flags) && + bitset(QPRIMARY, to->q_flags) && + !bitnset(M_LOCALMAILER, m->m_flags)) + { + bool firstone = true; + + (void) sm_strlcat(bufp, " NOTIFY=", sizeof(optbuf)); + if (bitset(QPINGONSUCCESS, to->q_flags)) + { + (void) sm_strlcat(bufp, "SUCCESS", sizeof(optbuf)); + firstone = false; + } + if (bitset(QPINGONFAILURE, to->q_flags)) + { + if (!firstone) + (void) sm_strlcat(bufp, ",", + sizeof(optbuf)); + (void) sm_strlcat(bufp, "FAILURE", sizeof(optbuf)); + firstone = false; + } + if (bitset(QPINGONDELAY, to->q_flags)) + { + if (!firstone) + (void) sm_strlcat(bufp, ",", + sizeof(optbuf)); + (void) sm_strlcat(bufp, "DELAY", sizeof(optbuf)); + firstone = false; + } + if (firstone) + (void) sm_strlcat(bufp, "NEVER", sizeof(optbuf)); + bufp += strlen(bufp); + } + + /* ORCPT= parameter */ + if (to->q_orcpt != NULL && + SPACELEFT(optbuf, bufp) > strlen(to->q_orcpt) + 7) + { + (void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp), + " ORCPT=%s", to->q_orcpt); + bufp += strlen(bufp); + } + } + + smtpmessage("RCPT To:<%s>%s", m, mci, to->q_user, optbuf); + mci->mci_state = MCIS_RCPT; + + SmtpPhase = mci->mci_phase = "client RCPT"; + sm_setproctitle(true, e, "%s %s: %s", qid_printname(e), + CurHostName, mci->mci_phase); + +#if PIPELINING + /* + ** If running SMTP pipelining, we will pick up status later + */ + + if (bitset(MCIF_PIPELINED, mci->mci_flags)) + return EX_OK; +#endif /* PIPELINING */ + + return smtprcptstat(to, m, mci, e); +} +/* +** SMTPRCPTSTAT -- get recipient status +** +** This is only called during SMTP pipelining +** +** Parameters: +** to -- address of recipient. +** m -- mailer being sent to. +** mci -- the mailer connection information. +** e -- the envelope for this message. +** +** Returns: +** EX_* -- protocol status +*/ + +static int +smtprcptstat(to, m, mci, e) + ADDRESS *to; + MAILER *m; + register MCI *mci; + register ENVELOPE *e; +{ + int r; + int save_errno; + char *enhsc; + + /* + ** Check if connection is gone, if so + ** it's a tempfail and we use mci_errno + ** for the reason. + */ + + if (mci->mci_state == MCIS_CLOSED) + { + errno = mci->mci_errno; + return EX_TEMPFAIL; + } + + enhsc = NULL; + r = reply(m, mci, e, TimeOuts.to_rcpt, NULL, &enhsc, XS_DEFAULT); + save_errno = errno; + to->q_rstatus = sm_rpool_strdup_x(e->e_rpool, SmtpReplyBuffer); + to->q_status = ENHSCN_RPOOL(enhsc, smtptodsn(r), e->e_rpool); + if (!bitnset(M_LMTP, m->m_flags)) + to->q_statmta = mci->mci_host; + if (r < 0 || REPLYTYPE(r) == 4) + { + mci->mci_retryrcpt = true; + errno = save_errno; + return EX_TEMPFAIL; + } + else if (REPLYTYPE(r) == 2) + { + char *t; + + if ((t = mci->mci_tolist) != NULL) + { + char *p; + + *t++ = ','; + for (p = to->q_paddr; *p != '\0'; *t++ = *p++) + continue; + *t = '\0'; + mci->mci_tolist = t; + } +#if PIPELINING + mci->mci_okrcpts++; +#endif /* PIPELINING */ + return EX_OK; + } + else if (r == 550) + { + to->q_status = ENHSCN_RPOOL(enhsc, "5.1.1", e->e_rpool); + return EX_NOUSER; + } + else if (r == 551) + { + to->q_status = ENHSCN_RPOOL(enhsc, "5.1.6", e->e_rpool); + return EX_NOUSER; + } + else if (r == 553) + { + to->q_status = ENHSCN_RPOOL(enhsc, "5.1.3", e->e_rpool); + return EX_NOUSER; + } + else if (REPLYTYPE(r) == 5) + { + return EX_UNAVAILABLE; + } + + if (LogLevel > 1) + { + sm_syslog(LOG_CRIT, e->e_id, + "%.100s: SMTP RCPT protocol error: %s", + CurHostName, + shortenstring(SmtpReplyBuffer, 403)); + } + + mci_setstat(mci, EX_PROTOCOL, ENHSCN(enhsc, "5.5.1"), + SmtpReplyBuffer); + return EX_PROTOCOL; +} +/* +** SMTPDATA -- send the data and clean up the transaction. +** +** Parameters: +** m -- mailer being sent to. +** mci -- the mailer connection information. +** e -- the envelope for this message. +** +** Returns: +** exit status corresponding to DATA command. +*/ + +int +smtpdata(m, mci, e, ctladdr, xstart) + MAILER *m; + register MCI *mci; + register ENVELOPE *e; + ADDRESS *ctladdr; + time_t xstart; +{ + register int r; + int rstat; + int xstat; + int timeout; + char *enhsc; + + /* + ** Check if connection is gone, if so + ** it's a tempfail and we use mci_errno + ** for the reason. + */ + + if (mci->mci_state == MCIS_CLOSED) + { + errno = mci->mci_errno; + return EX_TEMPFAIL; + } + + enhsc = NULL; + + /* + ** Send the data. + ** First send the command and check that it is ok. + ** Then send the data (if there are valid recipients). + ** Follow it up with a dot to terminate. + ** Finally get the results of the transaction. + */ + + /* send the command and check ok to proceed */ + smtpmessage("DATA", m, mci); + +#if PIPELINING + if (mci->mci_nextaddr != NULL) + { + char *oldto = e->e_to; + + /* pick up any pending RCPT responses for SMTP pipelining */ + while (mci->mci_nextaddr != NULL) + { + int r; + + e->e_to = mci->mci_nextaddr->q_paddr; + r = smtprcptstat(mci->mci_nextaddr, m, mci, e); + if (r != EX_OK) + { + markfailure(e, mci->mci_nextaddr, mci, r, + false); + giveresponse(r, mci->mci_nextaddr->q_status, m, + mci, ctladdr, xstart, e, + mci->mci_nextaddr); + if (r == EX_TEMPFAIL) + mci->mci_nextaddr->q_state = QS_RETRY; + } + mci->mci_nextaddr = mci->mci_nextaddr->q_pchain; + } + e->e_to = oldto; + + /* + ** Connection might be closed in response to a RCPT command, + ** i.e., the server responded with 421. In that case (at + ** least) one RCPT has a temporary failure, hence we don't + ** need to check mci_okrcpts (as it is done below) to figure + ** out which error to return. + */ + + if (mci->mci_state == MCIS_CLOSED) + { + errno = mci->mci_errno; + return EX_TEMPFAIL; + } + } +#endif /* PIPELINING */ + + /* now proceed with DATA phase */ + SmtpPhase = mci->mci_phase = "client DATA 354"; + mci->mci_state = MCIS_DATA; + sm_setproctitle(true, e, "%s %s: %s", + qid_printname(e), CurHostName, mci->mci_phase); + r = reply(m, mci, e, TimeOuts.to_datainit, NULL, &enhsc, XS_DEFAULT); + if (r < 0 || REPLYTYPE(r) == 4) + { + if (r >= 0) + smtpquit(m, mci, e); + errno = mci->mci_errno; + return EX_TEMPFAIL; + } + else if (REPLYTYPE(r) == 5) + { + smtprset(m, mci, e); +#if PIPELINING + if (mci->mci_okrcpts <= 0) + return mci->mci_retryrcpt ? EX_TEMPFAIL + : EX_UNAVAILABLE; +#endif /* PIPELINING */ + return EX_UNAVAILABLE; + } + else if (REPLYTYPE(r) != 3) + { + if (LogLevel > 1) + { + sm_syslog(LOG_CRIT, e->e_id, + "%.100s: SMTP DATA-1 protocol error: %s", + CurHostName, + shortenstring(SmtpReplyBuffer, 403)); + } + smtprset(m, mci, e); + mci_setstat(mci, EX_PROTOCOL, ENHSCN(enhsc, "5.5.1"), + SmtpReplyBuffer); +#if PIPELINING + if (mci->mci_okrcpts <= 0) + return mci->mci_retryrcpt ? EX_TEMPFAIL + : EX_PROTOCOL; +#endif /* PIPELINING */ + return EX_PROTOCOL; + } + +#if PIPELINING + if (mci->mci_okrcpts > 0) + { +#endif /* PIPELINING */ + + /* + ** Set timeout around data writes. Make it at least large + ** enough for DNS timeouts on all recipients plus some fudge + ** factor. The main thing is that it should not be infinite. + */ + + if (tTd(18, 101)) + { + /* simulate a DATA timeout */ + timeout = 10; + } + else + timeout = DATA_PROGRESS_TIMEOUT * 1000; + sm_io_setinfo(mci->mci_out, SM_IO_WHAT_TIMEOUT, &timeout); + + + /* + ** Output the actual message. + */ + + if (!(*e->e_puthdr)(mci, e->e_header, e, M87F_OUTER)) + goto writeerr; + + if (tTd(18, 101)) + { + /* simulate a DATA timeout */ + (void) sleep(2); + } + + if (!(*e->e_putbody)(mci, e, NULL)) + goto writeerr; + + /* + ** Cleanup after sending message. + */ + + +#if PIPELINING + } +#endif /* PIPELINING */ + +#if _FFR_CATCH_BROKEN_MTAS + if (sm_io_getinfo(mci->mci_in, SM_IO_IS_READABLE, NULL) > 0) + { + /* terminate the message */ + (void) sm_io_fprintf(mci->mci_out, SM_TIME_DEFAULT, ".%s", + m->m_eol); + if (TrafficLogFile != NULL) + (void) sm_io_fprintf(TrafficLogFile, SM_TIME_DEFAULT, + "%05d >>> .\n", (int) CurrentPid); + if (Verbose) + nmessage(">>> ."); + + sm_syslog(LOG_CRIT, e->e_id, + "%.100s: SMTP DATA-1 protocol error: remote server returned response before final dot", + CurHostName); + mci->mci_errno = EIO; + mci->mci_state = MCIS_ERROR; + mci_setstat(mci, EX_PROTOCOL, "5.5.0", NULL); + smtpquit(m, mci, e); + return EX_PROTOCOL; + } +#endif /* _FFR_CATCH_BROKEN_MTAS */ + + if (sm_io_error(mci->mci_out)) + { + /* error during processing -- don't send the dot */ + mci->mci_errno = EIO; + mci->mci_state = MCIS_ERROR; + mci_setstat(mci, EX_IOERR, "4.4.2", NULL); + smtpquit(m, mci, e); + return EX_IOERR; + } + + /* terminate the message */ + if (sm_io_fprintf(mci->mci_out, SM_TIME_DEFAULT, "%s.%s", + bitset(MCIF_INLONGLINE, mci->mci_flags) ? m->m_eol : "", + m->m_eol) == SM_IO_EOF) + goto writeerr; + if (TrafficLogFile != NULL) + (void) sm_io_fprintf(TrafficLogFile, SM_TIME_DEFAULT, + "%05d >>> .\n", (int) CurrentPid); + if (Verbose) + nmessage(">>> ."); + + /* check for the results of the transaction */ + SmtpPhase = mci->mci_phase = "client DATA status"; + sm_setproctitle(true, e, "%s %s: %s", qid_printname(e), + CurHostName, mci->mci_phase); + if (bitnset(M_LMTP, m->m_flags)) + return EX_OK; + r = reply(m, mci, e, TimeOuts.to_datafinal, NULL, &enhsc, XS_DEFAULT); + if (r < 0) + return EX_TEMPFAIL; + if (mci->mci_state == MCIS_DATA) + mci->mci_state = MCIS_OPEN; + xstat = EX_NOTSTICKY; + if (r == 452) + rstat = EX_TEMPFAIL; + else if (REPLYTYPE(r) == 4) + rstat = xstat = EX_TEMPFAIL; + else if (REPLYTYPE(r) == 2) + rstat = xstat = EX_OK; + else if (REPLYCLASS(r) != 5) + rstat = xstat = EX_PROTOCOL; + else if (REPLYTYPE(r) == 5) + rstat = EX_UNAVAILABLE; + else + rstat = EX_PROTOCOL; + mci_setstat(mci, xstat, ENHSCN(enhsc, smtptodsn(r)), + SmtpReplyBuffer); + if (bitset(MCIF_ENHSTAT, mci->mci_flags) && + (r = isenhsc(SmtpReplyBuffer + 4, ' ')) > 0) + r += 5; + else + r = 4; + e->e_statmsg = sm_rpool_strdup_x(e->e_rpool, &SmtpReplyBuffer[r]); + SmtpPhase = mci->mci_phase = "idle"; + sm_setproctitle(true, e, "%s: %s", CurHostName, mci->mci_phase); + if (rstat != EX_PROTOCOL) + return rstat; + if (LogLevel > 1) + { + sm_syslog(LOG_CRIT, e->e_id, + "%.100s: SMTP DATA-2 protocol error: %s", + CurHostName, + shortenstring(SmtpReplyBuffer, 403)); + } + return rstat; + + writeerr: + mci->mci_errno = errno; + mci->mci_state = MCIS_ERROR; + mci_setstat(mci, EX_TEMPFAIL, "4.4.2", NULL); + + /* + ** If putbody() couldn't finish due to a timeout, + ** rewind it here in the timeout handler. See + ** comments at the end of putbody() for reasoning. + */ + + if (e->e_dfp != NULL) + (void) bfrewind(e->e_dfp); + + errno = mci->mci_errno; + syserr("451 4.4.1 timeout writing message to %s", CurHostName); + smtpquit(m, mci, e); + return EX_TEMPFAIL; +} + +/* +** SMTPGETSTAT -- get status code from DATA in LMTP +** +** Parameters: +** m -- the mailer to which we are sending the message. +** mci -- the mailer connection structure. +** e -- the current envelope. +** +** Returns: +** The exit status corresponding to the reply code. +*/ + +int +smtpgetstat(m, mci, e) + MAILER *m; + MCI *mci; + ENVELOPE *e; +{ + int r; + int off; + int status, xstat; + char *enhsc; + + enhsc = NULL; + + /* check for the results of the transaction */ + r = reply(m, mci, e, TimeOuts.to_datafinal, NULL, &enhsc, XS_DEFAULT); + if (r < 0) + return EX_TEMPFAIL; + xstat = EX_NOTSTICKY; + if (REPLYTYPE(r) == 4) + status = EX_TEMPFAIL; + else if (REPLYTYPE(r) == 2) + status = xstat = EX_OK; + else if (REPLYCLASS(r) != 5) + status = xstat = EX_PROTOCOL; + else if (REPLYTYPE(r) == 5) + status = EX_UNAVAILABLE; + else + status = EX_PROTOCOL; + if (bitset(MCIF_ENHSTAT, mci->mci_flags) && + (off = isenhsc(SmtpReplyBuffer + 4, ' ')) > 0) + off += 5; + else + off = 4; + e->e_statmsg = sm_rpool_strdup_x(e->e_rpool, &SmtpReplyBuffer[off]); + mci_setstat(mci, xstat, ENHSCN(enhsc, smtptodsn(r)), SmtpReplyBuffer); + if (LogLevel > 1 && status == EX_PROTOCOL) + { + sm_syslog(LOG_CRIT, e->e_id, + "%.100s: SMTP DATA-3 protocol error: %s", + CurHostName, + shortenstring(SmtpReplyBuffer, 403)); + } + return status; +} +/* +** SMTPQUIT -- close the SMTP connection. +** +** Parameters: +** m -- a pointer to the mailer. +** mci -- the mailer connection information. +** e -- the current envelope. +** +** Returns: +** none. +** +** Side Effects: +** sends the final protocol and closes the connection. +*/ + +void +smtpquit(m, mci, e) + register MAILER *m; + register MCI *mci; + ENVELOPE *e; +{ + bool oldSuprErrs = SuprErrs; + int rcode; + char *oldcurhost; + + if (mci->mci_state == MCIS_CLOSED) + { + mci_close(mci, "smtpquit:1"); + return; + } + + oldcurhost = CurHostName; + CurHostName = mci->mci_host; /* XXX UGLY XXX */ + if (CurHostName == NULL) + CurHostName = MyHostName; + +#if PIPELINING + mci->mci_okrcpts = 0; +#endif /* PIPELINING */ + + /* + ** Suppress errors here -- we may be processing a different + ** job when we do the quit connection, and we don't want the + ** new job to be penalized for something that isn't it's + ** problem. + */ + + SuprErrs = true; + + /* send the quit message if we haven't gotten I/O error */ + if (mci->mci_state != MCIS_ERROR && + mci->mci_state != MCIS_QUITING) + { + SmtpPhase = "client QUIT"; + mci->mci_state = MCIS_QUITING; + smtpmessage("QUIT", m, mci); + (void) reply(m, mci, e, TimeOuts.to_quit, NULL, NULL, + XS_DEFAULT); + SuprErrs = oldSuprErrs; + if (mci->mci_state == MCIS_CLOSED) + goto end; + } + + /* now actually close the connection and pick up the zombie */ + rcode = endmailer(mci, e, NULL); + if (rcode != EX_OK) + { + char *mailer = NULL; + + if (mci->mci_mailer != NULL && + mci->mci_mailer->m_name != NULL) + mailer = mci->mci_mailer->m_name; + + /* look for naughty mailers */ + sm_syslog(LOG_ERR, e->e_id, + "smtpquit: mailer%s%s exited with exit value %d", + mailer == NULL ? "" : " ", + mailer == NULL ? "" : mailer, + rcode); + } + + SuprErrs = oldSuprErrs; + + end: + CurHostName = oldcurhost; + return; +} +/* +** SMTPRSET -- send a RSET (reset) command +** +** Parameters: +** m -- a pointer to the mailer. +** mci -- the mailer connection information. +** e -- the current envelope. +** +** Returns: +** none. +** +** Side Effects: +** closes the connection if there is no reply to RSET. +*/ + +void +smtprset(m, mci, e) + register MAILER *m; + register MCI *mci; + ENVELOPE *e; +{ + int r; + + CurHostName = mci->mci_host; /* XXX UGLY XXX */ + if (CurHostName == NULL) + CurHostName = MyHostName; + +#if PIPELINING + mci->mci_okrcpts = 0; +#endif /* PIPELINING */ + + /* + ** Check if connection is gone, if so + ** it's a tempfail and we use mci_errno + ** for the reason. + */ + + if (mci->mci_state == MCIS_CLOSED) + { + errno = mci->mci_errno; + return; + } + + SmtpPhase = "client RSET"; + smtpmessage("RSET", m, mci); + r = reply(m, mci, e, TimeOuts.to_rset, NULL, NULL, XS_DEFAULT); + if (r < 0) + return; + + /* + ** Any response is deemed to be acceptable. + ** The standard does not state the proper action + ** to take when a value other than 250 is received. + ** + ** However, if 421 is returned for the RSET, leave + ** mci_state alone (MCIS_SSD can be set in reply() + ** and MCIS_CLOSED can be set in smtpquit() if + ** reply() gets a 421 and calls smtpquit()). + */ + + if (mci->mci_state != MCIS_SSD && mci->mci_state != MCIS_CLOSED) + mci->mci_state = MCIS_OPEN; + else if (mci->mci_exitstat == EX_OK) + mci_setstat(mci, EX_TEMPFAIL, "4.5.0", NULL); +} +/* +** SMTPPROBE -- check the connection state +** +** Parameters: +** mci -- the mailer connection information. +** +** Returns: +** none. +** +** Side Effects: +** closes the connection if there is no reply to RSET. +*/ + +int +smtpprobe(mci) + register MCI *mci; +{ + int r; + MAILER *m = mci->mci_mailer; + ENVELOPE *e; + extern ENVELOPE BlankEnvelope; + + CurHostName = mci->mci_host; /* XXX UGLY XXX */ + if (CurHostName == NULL) + CurHostName = MyHostName; + + e = &BlankEnvelope; + SmtpPhase = "client probe"; + smtpmessage("RSET", m, mci); + r = reply(m, mci, e, TimeOuts.to_miscshort, NULL, NULL, XS_DEFAULT); + if (REPLYTYPE(r) != 2) + smtpquit(m, mci, e); + return r; +} +/* +** REPLY -- read arpanet reply +** +** Parameters: +** m -- the mailer we are reading the reply from. +** mci -- the mailer connection info structure. +** e -- the current envelope. +** timeout -- the timeout for reads. +** pfunc -- processing function called on each line of response. +** If null, no special processing is done. +** enhstat -- optional, returns enhanced error code string (if set) +** rtype -- type of SmtpMsgBuffer: does it contains secret data? +** +** Returns: +** reply code it reads. +** +** Side Effects: +** flushes the mail file. +*/ + +int +reply(m, mci, e, timeout, pfunc, enhstat, rtype) + MAILER *m; + MCI *mci; + ENVELOPE *e; + time_t timeout; + void (*pfunc) __P((char *, bool, MAILER *, MCI *, ENVELOPE *)); + char **enhstat; + int rtype; +{ + register char *bufp; + register int r; + bool firstline = true; + char junkbuf[MAXLINE]; + static char enhstatcode[ENHSCLEN]; + int save_errno; + + /* + ** Flush the output before reading response. + ** + ** For SMTP pipelining, it would be better if we didn't do + ** this if there was already data waiting to be read. But + ** to do it properly means pushing it to the I/O library, + ** since it really needs to be done below the buffer layer. + */ + + if (mci->mci_out != NULL) + (void) sm_io_flush(mci->mci_out, SM_TIME_DEFAULT); + + if (tTd(18, 1)) + sm_dprintf("reply\n"); + + /* + ** Read the input line, being careful not to hang. + */ + + bufp = SmtpReplyBuffer; + set_tls_rd_tmo(timeout); + for (;;) + { + register char *p; + + /* actually do the read */ + if (e->e_xfp != NULL) /* for debugging */ + (void) sm_io_flush(e->e_xfp, SM_TIME_DEFAULT); + + /* if we are in the process of closing just give the code */ + if (mci->mci_state == MCIS_CLOSED) + return SMTPCLOSING; + + /* don't try to read from a non-existent fd */ + if (mci->mci_in == NULL) + { + if (mci->mci_errno == 0) + mci->mci_errno = EBADF; + + /* errors on QUIT should be ignored */ + if (strncmp(SmtpMsgBuffer, "QUIT", 4) == 0) + { + errno = mci->mci_errno; + mci_close(mci, "reply:1"); + return -1; + } + mci->mci_state = MCIS_ERROR; + smtpquit(m, mci, e); + errno = mci->mci_errno; + return -1; + } + + if (mci->mci_out != NULL) + (void) sm_io_flush(mci->mci_out, SM_TIME_DEFAULT); + + /* get the line from the other side */ + p = sfgets(bufp, MAXLINE, mci->mci_in, timeout, SmtpPhase); + save_errno = errno; + mci->mci_lastuse = curtime(); + + if (p == NULL) + { + bool oldholderrs; + extern char MsgBuf[]; + + /* errors on QUIT should be ignored */ + if (strncmp(SmtpMsgBuffer, "QUIT", 4) == 0) + { + mci_close(mci, "reply:2"); + return -1; + } + + /* if the remote end closed early, fake an error */ + errno = save_errno; + if (errno == 0) + { + (void) sm_snprintf(SmtpReplyBuffer, + sizeof(SmtpReplyBuffer), + "421 4.4.1 Connection reset by %s", + CURHOSTNAME); +#ifdef ECONNRESET + errno = ECONNRESET; +#else /* ECONNRESET */ + errno = EPIPE; +#endif /* ECONNRESET */ + } + + mci->mci_errno = errno; + oldholderrs = HoldErrs; + HoldErrs = true; + usrerr("451 4.4.1 reply: read error from %s", + CURHOSTNAME); + mci_setstat(mci, EX_TEMPFAIL, "4.4.2", MsgBuf); + + /* if debugging, pause so we can see state */ + if (tTd(18, 100)) + (void) pause(); + mci->mci_state = MCIS_ERROR; + smtpquit(m, mci, e); +#if XDEBUG + { + char wbuf[MAXLINE]; + + p = wbuf; + if (e->e_to != NULL) + { + (void) sm_snprintf(p, + SPACELEFT(wbuf, p), + "%s... ", + shortenstring(e->e_to, MAXSHORTSTR)); + p += strlen(p); + } + (void) sm_snprintf(p, SPACELEFT(wbuf, p), + "reply(%.100s) during %s", + CURHOSTNAME, SmtpPhase); + checkfd012(wbuf); + } +#endif /* XDEBUG */ + HoldErrs = oldholderrs; + errno = save_errno; + return -1; + } + fixcrlf(bufp, true); + + /* EHLO failure is not a real error */ + if (e->e_xfp != NULL && (bufp[0] == '4' || + (bufp[0] == '5' && strncmp(SmtpMsgBuffer, "EHLO", 4) != 0))) + { + /* serious error -- log the previous command */ + if (SmtpNeedIntro) + { + /* inform user who we are chatting with */ + (void) sm_io_fprintf(CurEnv->e_xfp, + SM_TIME_DEFAULT, + "... while talking to %s:\n", + CURHOSTNAME); + SmtpNeedIntro = false; + } + if (SmtpMsgBuffer[0] != '\0') + { + (void) sm_io_fprintf(e->e_xfp, + SM_TIME_DEFAULT, + ">>> %s\n", + (rtype == XS_STARTTLS) + ? "STARTTLS dialogue" + : ((rtype == XS_AUTH) + ? "AUTH dialogue" + : SmtpMsgBuffer)); + SmtpMsgBuffer[0] = '\0'; + } + + /* now log the message as from the other side */ + (void) sm_io_fprintf(e->e_xfp, SM_TIME_DEFAULT, + "<<< %s\n", bufp); + } + + /* display the input for verbose mode */ + if (Verbose) + nmessage("050 %s", bufp); + + /* ignore improperly formatted input */ + if (!ISSMTPREPLY(bufp)) + continue; + + if (bitset(MCIF_ENHSTAT, mci->mci_flags) && + enhstat != NULL && + extenhsc(bufp + 4, ' ', enhstatcode) > 0) + *enhstat = enhstatcode; + + /* process the line */ + if (pfunc != NULL) + (*pfunc)(bufp, firstline, m, mci, e); + + firstline = false; + + /* decode the reply code */ + r = atoi(bufp); + + /* extra semantics: 0xx codes are "informational" */ + if (r < 100) + continue; + + /* if no continuation lines, return this line */ + if (bufp[3] != '-') + break; + + /* first line of real reply -- ignore rest */ + bufp = junkbuf; + } + + /* + ** Now look at SmtpReplyBuffer -- only care about the first + ** line of the response from here on out. + */ + + /* save temporary failure messages for posterity */ + if (SmtpReplyBuffer[0] == '4') + (void) sm_strlcpy(SmtpError, SmtpReplyBuffer, sizeof(SmtpError)); + + /* reply code 421 is "Service Shutting Down" */ + if (r == SMTPCLOSING && mci->mci_state != MCIS_SSD && + mci->mci_state != MCIS_QUITING) + { + /* send the quit protocol */ + mci->mci_state = MCIS_SSD; + smtpquit(m, mci, e); + } + + return r; +} +/* +** SMTPMESSAGE -- send message to server +** +** Parameters: +** f -- format +** m -- the mailer to control formatting. +** a, b, c -- parameters +** +** Returns: +** none. +** +** Side Effects: +** writes message to mci->mci_out. +*/ + +/*VARARGS1*/ +void +#ifdef __STDC__ +smtpmessage(char *f, MAILER *m, MCI *mci, ...) +#else /* __STDC__ */ +smtpmessage(f, m, mci, va_alist) + char *f; + MAILER *m; + MCI *mci; + va_dcl +#endif /* __STDC__ */ +{ + SM_VA_LOCAL_DECL + + SM_VA_START(ap, mci); + (void) sm_vsnprintf(SmtpMsgBuffer, sizeof(SmtpMsgBuffer), f, ap); + SM_VA_END(ap); + + if (tTd(18, 1) || Verbose) + nmessage(">>> %s", SmtpMsgBuffer); + if (TrafficLogFile != NULL) + (void) sm_io_fprintf(TrafficLogFile, SM_TIME_DEFAULT, + "%05d >>> %s\n", (int) CurrentPid, + SmtpMsgBuffer); + if (mci->mci_out != NULL) + { + (void) sm_io_fprintf(mci->mci_out, SM_TIME_DEFAULT, "%s%s", + SmtpMsgBuffer, m == NULL ? "\r\n" + : m->m_eol); + } + else if (tTd(18, 1)) + { + sm_dprintf("smtpmessage: NULL mci_out\n"); + } +} |