/* lmtpengine.c: LMTP protocol engine * $Id: lmtpengine.c,v 1.91.2.1 2004/03/17 16:34:44 rjs3 Exp $ * * Copyright (c) 1998-2003 Carnegie Mellon University. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The name "Carnegie Mellon University" must not be used to * endorse or promote products derived from this software without * prior written permission. For permission or any other legal * details, please contact * Office of Technology Transfer * Carnegie Mellon University * 5000 Forbes Avenue * Pittsburgh, PA 15213-3890 * (412) 268-4387, fax: (412) 268-7395 * tech-transfer@andrew.cmu.edu * * 4. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by Computing Services * at Carnegie Mellon University (http://www.cmu.edu/computing/)." * * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "assert.h" #include "util.h" #include "auth.h" #include "prot.h" #include "rfc822date.h" #include "imapconf.h" #include "iptostring.h" #include "exitcodes.h" #include "imap_err.h" #include "mupdate_err.h" #include "xmalloc.h" #include "version.h" #include "lmtpengine.h" #include "lmtpstats.h" #include "tls.h" #include "telemetry.h" #define RCPT_GROW 30 /* data per message */ struct Header { char *name; int ncontents; char *contents[1]; }; struct address_data { char *user; char *all; int ignorequota; int status; }; struct clientdata { struct protstream *pin; struct protstream *pout; int fd; char clienthost[250]; char lhlo_param[250]; sasl_conn_t *conn; #ifdef HAVE_SSL SSL *tls_conn; #endif /* HAVE_SSL */ int starttls_done; }; /* defined in lmtpd.c or lmtpproxyd.c */ extern int deliver_logfd; extern int saslserver(sasl_conn_t *conn, const char *mech, const char *init_resp, const char *continuation, struct protstream *pin, struct protstream *pout, int *sasl_result, char **success_data); /* Enable the resetting of a sasl_conn_t */ static int reset_saslconn(sasl_conn_t **conn); static struct { char *ipremoteport; char *iplocalport; sasl_ssf_t ssf; char *authid; } saslprops = {NULL,NULL,0,NULL}; #ifdef USING_SNMPGEN /* round to nearest 1024 bytes and return number of Kbytes. used for SNMP updates. */ static int roundToK(int x) { double rd = (x*1.0)/1024.0; int ri = x/1024; if (rd-ri < 0.5) return ri; else return ri+1; } #else #define roundToK(x) #endif /* USING_SNMPGEN */ static void send_lmtp_error(struct protstream *pout, int r) { switch (r) { case 0: prot_printf(pout, "250 2.1.5 Ok\r\n"); break; case IMAP_IOERROR: prot_printf(pout, "451 4.3.0 System I/O error\r\n"); break; case IMAP_SERVER_UNAVAILABLE: case MUPDATE_NOCONN: case MUPDATE_NOAUTH: case MUPDATE_TIMEOUT: case MUPDATE_PROTOCOL_ERROR: prot_printf(pout, "451 4.4.3 Remote server unavailable\r\n"); break; case IMAP_NOSPACE: prot_printf(pout, "451 4.3.1 cannot create file: out of space\r\n"); break; case IMAP_AGAIN: prot_printf(pout, "451 4.3.0 transient system error\r\n"); break; case IMAP_PERMISSION_DENIED: if (LMTP_LONG_ERROR_MSGS) { prot_printf(pout, "550-You do not have permission to post a message to this mailbox.\r\n" "550-Please contact the owner of this mailbox in order to submit\r\n" "550-your message, or %s if you believe you\r\n" "550-received this message in error.\r\n" "550 5.7.1 Permission denied\r\n", POSTMASTER); } else { prot_printf(pout, "550 5.7.1 Permission denied\r\n"); } break; case IMAP_QUOTA_EXCEEDED: if(config_getswitch("lmtp_overquota_perm_failure",0)) { /* Not Default - Perm Failure */ prot_printf(pout, "552 5.2.2 Over quota\r\n"); } else { /* Default - Temp Failure */ prot_printf(pout, "452 4.2.2 Over quota\r\n"); } break; case IMAP_MAILBOX_BADFORMAT: case IMAP_MAILBOX_NOTSUPPORTED: prot_printf(pout, "451 4.2.0 Mailbox has an invalid format\r\n"); break; case IMAP_MAILBOX_MOVED: prot_printf(pout, "451 4.2.1 Mailbox Moved\r\n"); break; case IMAP_MESSAGE_CONTAINSNULL: prot_printf(pout, "554 5.6.0 Message contains NUL characters\r\n"); break; case IMAP_MESSAGE_CONTAINSNL: prot_printf(pout, "554 5.6.0 Message contains bare newlines\r\n"); break; case IMAP_MESSAGE_CONTAINS8BIT: prot_printf(pout, "554 5.6.0 Message contains non-ASCII characters in headers\r\n"); break; case IMAP_MESSAGE_BADHEADER: prot_printf(pout, "554 5.6.0 Message contains invalid header\r\n"); break; case IMAP_MESSAGE_NOBLANKLINE: prot_printf(pout, "554 5.6.0 Message has no header/body separator\r\n"); break; case IMAP_MAILBOX_NONEXISTENT: /* XXX Might have been moved to other server */ if (LMTP_LONG_ERROR_MSGS) { prot_printf(pout, "550-Mailbox unknown. Either there is no mailbox associated with this\r\n" "550-name or you do not have authorization to see it.\r\n" "550 5.1.1 User unknown\r\n"); } else { prot_printf(pout, "550 5.1.1 User unknown\r\n"); } break; case IMAP_PROTOCOL_BAD_PARAMETERS: prot_printf(pout, "501 5.5.4 Syntax error in parameters\r\n"); break; case MUPDATE_BADPARAM: default: /* Some error we're not expecting. */ prot_printf(pout, "554 5.0.0 Unexpected internal error\r\n"); break; } } /* ----- this section defines functions on message_data_t. ----- access functions and the like, etc. */ /* returns non-zero on failure */ int msg_new(message_data_t **m) { message_data_t *ret = (message_data_t *) xmalloc(sizeof(message_data_t)); int i; ret->data = NULL; ret->f = NULL; ret->id = NULL; ret->size = 0; ret->return_path = NULL; ret->rcpt = NULL; ret->rcpt_num = 0; ret->authuser = NULL; ret->authstate = NULL; ret->rock = NULL; for (i = 0; i < HEADERCACHESIZE; i++) ret->cache[i] = NULL; *m = ret; return 0; } void msg_free(message_data_t *m) { int i; if (m->data) { prot_free(m->data); } if (m->f) { fclose(m->f); } if (m->id) { free(m->id); } if (m->return_path) { free(m->return_path); } if (m->rcpt) { for (i = 0; i < m->rcpt_num; i++) { if (m->rcpt[i]->all) free(m->rcpt[i]->all); if (m->rcpt[i]->user) free(m->rcpt[i]->user); free(m->rcpt[i]); } free(m->rcpt); } if (m->authuser) { free(m->authuser); if (m->authstate) auth_freestate(m->authstate); } for (i = 0; i < HEADERCACHESIZE; i++) { if (m->cache[i]) { int j; free(m->cache[i]->name); for (j = 0; j < m->cache[i]->ncontents; j++) { free(m->cache[i]->contents[j]); } free(m->cache[i]); } } free(m); } /* hash function used for header cache in struct msg */ static int hashheader(char *header) { int x = 0; /* any CHAR except ' ', :, or a ctrl char */ for (; !iscntrl((int) *header) && (*header != ' ') && (*header != ':'); header++) { x *= 256; x += *header; x %= HEADERCACHESIZE; } return x; } const char **msg_getheader(message_data_t *m, const char *phead) { char *head; const char **ret = NULL; int clinit, cl; assert(m && phead); head = xstrdup(phead); lcase(head); /* check the cache */ clinit = cl = hashheader(head); while (m->cache[cl] != NULL) { if (!strcmp(head, m->cache[cl]->name)) { ret = (const char **) m->cache[cl]->contents; break; } cl++; /* try next hash bin */ cl %= HEADERCACHESIZE; if (cl == clinit) break; /* gone all the way around */ } free(head); return ret; } int msg_getsize(message_data_t *m) { return m->size; } int msg_getnumrcpt(message_data_t *m) { return m->rcpt_num; } const char *msg_getrcpt(message_data_t *m, int rcpt_num) { assert(0 <= rcpt_num && rcpt_num < m->rcpt_num); return m->rcpt[rcpt_num]->user; } const char *msg_getrcptall(message_data_t *m, int rcpt_num) { assert(0 <= rcpt_num && rcpt_num < m->rcpt_num); return m->rcpt[rcpt_num]->all; } int msg_getrcpt_ignorequota(message_data_t *m, int rcpt_num) { assert(0 <= rcpt_num && rcpt_num < m->rcpt_num); return m->rcpt[rcpt_num]->ignorequota; } /* set a recipient status; 'r' should be an IMAP error code that will be translated into an LMTP status code */ void msg_setrcpt_status(message_data_t *m, int rcpt_num, int r) { assert(0 <= rcpt_num && rcpt_num < m->rcpt_num); m->rcpt[rcpt_num]->status = r; } void *msg_getrock(message_data_t *m) { return m->rock; } void msg_setrock(message_data_t *m, void *rock) { m->rock = rock; } /* return a malloc'd string representing the authorized user. advance 'strp' over the parameter */ static char *parseautheq(char **strp) { char *ret; char *str; char *s = *strp; if (!strcmp(s, "<>")) { *strp = s + 2; return NULL; } ret = (char *) xmalloc(strlen(s)+1); ret[0]='\0'; str = ret; if (*s == '<') s++; /* we'll be liberal and accept "" */ while (1) { /* hexchar */ if (*s == '+') { int lup; *str = '\0'; s++; for (lup=0;lup<2;lup++) { if ((*s>='0') && (*s<='9')) (*str) = (*str) & (*s - '0'); else if ((*s>='A') && (*s<='F')) (*str) = (*str) & (*s - 'A' + 10); else { free(ret); *strp = s; return NULL; } if (lup==0) { (*str) = (*str) << 4; s++; } } str++; } else if ((*s >= '!') && (*s <='~') && (*s!='+') && (*s!='=')) { /* ascii char */ *str = *s; str++; } else { /* bad char or end-of-line */ break; } s++; } *strp = s; if (*s && (*s!=' ')) { free(ret); return NULL; } *str = '\0'; /* take off trailing '>' */ if ((str!=ret) && ( *(str-1)=='>')) { *(str-1) = '\0'; } return ret; } /* return malloc'd string containing the address */ static char *parseaddr(char *s) { char *p, *ret; int len; p = s; if (*p++ != '<') return 0; /* at-domain-list */ while (*p == '@') { p++; if (*p == '[') { p++; while (isdigit((int) *p) || *p == '.') p++; if (*p++ != ']') return 0; } else { while (isalnum((int) *p) || *p == '.' || *p == '-') p++; } if (*p == ',' && p[1] == '@') p++; else if (*p == ':' && p[1] != '@') p++; else return 0; } /* local-part */ if (*p == '\"') { p++; while (*p && *p != '\"') { if (*p == '\\') { if (!*++p) return 0; } p++; } if (!*p++) return 0; } else { while (*p && *p != '@' && *p != '>') { if (*p == '\\') { if (!*++p) return 0; } else { if (*p <= ' ' || (*p & 128) || strchr("<>()[]\\,;:\"", *p)) return 0; } p++; } } /* @domain */ if (*p == '@') { p++; if (*p == '[') { p++; while (isdigit((int) *p) || *p == '.') p++; if (*p++ != ']') return 0; } else { while (isalnum((int) *p) || *p == '.' || *p == '-') p++; } } if (*p++ != '>') return 0; if (*p && *p != ' ') return 0; len = p - s; ret = xmalloc(len + 1); memcpy(ret, s, len); ret[len] = '\0'; return ret; } /* clean off the <> from the return path */ void clean_retpath(char *rpath) { int sl; /* Remove any angle brackets around return path */ if (*rpath == '<') { sl = strlen(rpath); /* use strlen(rpath) so we move the NUL too */ memmove(rpath, rpath+1, sl); sl--; /* string is one shorter now */ if (rpath[sl-1] == '>') { rpath[sl-1] = '\0'; } } } /* * Destructively remove any whitespace and 822 comments * from string pointed to by 'buf'. Does not handle continuation header * lines. */ void clean822space(buf) char *buf; { char *from=buf, *to=buf; int c; int commentlevel = 0; while ((c = *from++)!=0) { switch (c) { case '\r': case '\n': case '\0': *to = '\0'; return; case ' ': case '\t': continue; case '(': commentlevel++; break; case ')': if (commentlevel) commentlevel--; break; case '\\': if (commentlevel && *from) from++; /* FALL THROUGH */ default: if (!commentlevel) *to++ = c; break; } } } /* copies the message from fin to fout, massaging accordingly: . newlines are fiddled to \r\n . "." terminates . embedded NULs are rejected . bare \r are removed */ static int copy_msg(struct protstream *fin, FILE *fout) { char buf[8192], *p; int r = 0; /* -2: Might need room to add a \r\n\0 set */ while (prot_fgets(buf, sizeof(buf)-2, fin)) { p = buf + strlen(buf) - 1; if (p < buf) { /* buffer start with a \0 */ r = IMAP_MESSAGE_CONTAINSNULL; continue; /* need to eat the rest of the message */ } else if (buf[0] == '\r' && buf[1] == '\0') { /* The message contained \r\0, and fgets is confusing us. */ r = IMAP_MESSAGE_CONTAINSNULL; continue; /* need to eat the rest of the message */ } else if (p[0] == '\r') { /* * We were unlucky enough to get a CR just before we ran * out of buffer--put it back. */ prot_ungetc('\r', fin); *p = '\0'; } else if (p[0] == '\n' && (p == buf || p[-1] != '\r')) { /* found an \n without a \r */ p[0] = '\r'; p[1] = '\n'; p[2] = '\0'; } else if (p[0] != '\n') { /* line contained a \0 not at the end */ r = IMAP_MESSAGE_CONTAINSNULL; continue; } /* Remove any lone CR characters */ while ((p = strchr(buf, '\r')) && p[1] != '\n') { /* Src/Target overlap, use memmove */ /* strlen(p) will result in copying the NUL byte as well */ memmove(p, p+1, strlen(p)); } if (buf[0] == '.') { if (buf[1] == '\r' && buf[2] == '\n') { /* End of message */ goto lmtpdot; } /* Remove the dot-stuffing */ fputs(buf+1, fout); } else { fputs(buf, fout); } } /* wow, serious error---got a premature EOF. */ return IMAP_IOERROR; lmtpdot: return r; } /* take a list of headers, pull the first one out and return it in name and contents. copies fin to fout, massaging returns 0 on success, negative on failure */ typedef enum { NAME_START, NAME, COLON, BODY_START, BODY } state; enum { NAMEINC = 128, BODYINC = 1024 }; /* we don't have to worry about dotstuffing here, since it's illegal for a header to begin with a dot! returns 0 on success, filling in 'headname' and 'contents' with a static pointer (blech). on end of headers, returns 0 with NULL 'headname' and NULL 'contents' on error, returns < 0 */ static int parseheader(struct protstream *fin, FILE *fout, char **headname, char **contents) { int c; static char *name = NULL, *body = NULL; static int namelen = 0, bodylen = 0; int off = 0; state s = NAME_START; int r = 0; int reject8bit = config_getswitch("reject8bit", 0); if (namelen == 0) { namelen += NAMEINC; name = (char *) xrealloc(name, namelen * sizeof(char)); } if (bodylen == 0) { bodylen += BODYINC; body = (char *) xrealloc(body, bodylen * sizeof(char)); } /* there are two ways out of this loop, both via gotos: either we successfully read a header (got_header) or we hit an error (ph_error) */ while ((c = prot_getc(fin)) != EOF) { /* examine each character */ switch (s) { case NAME_START: if (c == '.') { int peek; peek = prot_getc(fin); prot_ungetc(peek, fin); if (peek == '\r' || peek == '\n') { /* just reached the end of message */ r = IMAP_MESSAGE_NOBLANKLINE; goto ph_error; } } if (c == '\r' || c == '\n') { /* just reached the end of headers */ r = 0; goto ph_error; } /* field-name = 1*ftext ftext = %d33-57 / %d59-126 ; Any character except ; controls, SP, and ; ":". */ if (!((c >= 33 && c <= 57) || (c >= 59 && c <= 126))) { /* invalid header name */ r = IMAP_MESSAGE_BADHEADER; goto ph_error; } name[0] = tolower(c); off = 1; s = NAME; break; case NAME: if (c == ' ' || c == '\t' || c == ':') { name[off] = '\0'; s = (c == ':' ? BODY_START : COLON); break; } if (!((c >= 33 && c <= 57) || (c >= 59 && c <= 126))) { r = IMAP_MESSAGE_BADHEADER; goto ph_error; } name[off++] = tolower(c); if (off >= namelen - 3) { namelen += NAMEINC; name = (char *) xrealloc(name, namelen); } break; case COLON: if (c == ':') { s = BODY_START; } else if (c != ' ' && c != '\t') { /* i want to avoid confusing dot-stuffing later */ while (c == '.') { fputc(c, fout); c = prot_getc(fin); } r = IMAP_MESSAGE_BADHEADER; goto ph_error; } break; case BODY_START: if (c == ' ' || c == '\t') /* eat the whitespace */ break; off = 0; s = BODY; /* falls through! */ case BODY: /* now we want to convert all newlines into \r\n */ if (c == '\r' || c == '\n') { int peek; peek = prot_getc(fin); fputc('\r', fout); fputc('\n', fout); /* we should peek ahead to see if it's folded whitespace */ if (c == '\r' && peek == '\n') { c = prot_getc(fin); } else { c = peek; /* single newline seperator */ } if (c != ' ' && c != '\t') { /* this is the end of the header */ body[off] = '\0'; prot_ungetc(c, fin); goto got_header; } /* ignore this whitespace, but we'll copy all the rest in */ break; } else { if (c >= 0x80) { if (reject8bit) { /* We have been configured to reject all mail of this form. */ r = IMAP_MESSAGE_CONTAINS8BIT; goto ph_error; } else { /* We have been configured to munge all mail of this form. */ c = 'X'; } } /* just an ordinary character */ body[off++] = c; if (off >= bodylen - 3) { bodylen += BODYINC; body = (char *) xrealloc(body, bodylen); } } } /* copy this to the output */ fputc(c, fout); } /* if we fall off the end of the loop, we hit some sort of error condition */ ph_error: /* put the last character back; we'll copy it later */ prot_ungetc(c, fin); /* and we didn't get a header */ if (headname != NULL) *headname = NULL; if (contents != NULL) *contents = NULL; return r; got_header: if (headname != NULL) *headname = xstrdup(name); if (contents != NULL) *contents = xstrdup(body); return 0; } static int fill_cache(struct protstream *fin, FILE *fout, message_data_t *m) { int r = 0; /* let's fill that header cache */ for (;;) { char *name, *body; int cl, clinit; if ((r = parseheader(fin, fout, &name, &body)) < 0) { break; } if (!name) { /* reached the end of headers */ break; } /* put it in the hash table */ clinit = cl = hashheader(name); while (m->cache[cl] != NULL && strcmp(name, m->cache[cl]->name)) { cl++; /* resolve collisions linearly */ cl %= HEADERCACHESIZE; if (cl == clinit) break; /* gone all the way around, so bail */ } /* found where to put it, so insert it into a list */ if (m->cache[cl]) { /* add this body on */ m->cache[cl]->contents[m->cache[cl]->ncontents++] = body; /* whoops, won't have room for the null at the end! */ if (!(m->cache[cl]->ncontents % 8)) { /* increase the size */ m->cache[cl] = (header_t *) xrealloc(m->cache[cl],sizeof(header_t) + ((8 + m->cache[cl]->ncontents) * sizeof(char *))); } /* have no need of this */ free(name); } else { /* create a new entry in the hash table */ m->cache[cl] = (header_t *) xmalloc(sizeof(header_t) + 8 * sizeof(char*)); m->cache[cl]->name = name; m->cache[cl]->contents[0] = body; m->cache[cl]->ncontents = 1; } /* we always want a NULL at the end */ m->cache[cl]->contents[m->cache[cl]->ncontents] = NULL; } if (r) { /* got a bad header */ /* flush the remaining output */ copy_msg(fin, fout); /* and return the error */ return r; } else { return copy_msg(fin, fout); } } /* * file in the message structure 'm' from 'pin', assuming a dot-stuffed * stream a la lmtp. * * returns 0 on success, imap error code on failure */ static int savemsg(struct clientdata *cd, const struct lmtp_func *func, message_data_t *m) { FILE *f; struct stat sbuf; const char **body; int r; int nrcpts = m->rcpt_num; time_t t; char datestr[80]; /* Copy to spool file */ f = func->spoolfile(m); if (!f) { prot_printf(cd->pout, "451 4.3.%c cannot create temporary file: %s\r\n", ( #ifdef EDQUOT errno == EDQUOT || #endif errno == ENOSPC) ? '1' : '2', error_message(errno)); return IMAP_IOERROR; } prot_printf(cd->pout, "354 go ahead\r\n"); if (m->return_path && func->addretpath) { /* add the return path */ char *rpath = m->return_path; const char *hostname = 0; clean_retpath(rpath); /* Append our hostname if there's no domain in address */ hostname = NULL; if (!strchr(rpath, '@') && strlen(rpath) > 0) { hostname = config_servername; } fprintf(f, "Return-Path: <%s%s%s>\r\n", rpath, hostname ? "@" : "", hostname ? hostname : ""); } /* add a received header */ t = time(NULL); rfc822date_gen(datestr, sizeof(datestr), t); fprintf(f, "Received: from %s (%s)", cd->lhlo_param, cd->clienthost); if (m->authuser) { const int *ssfp; sasl_getprop(cd->conn, SASL_SSF, (const void **) &ssfp); fprintf(f, " (authenticated user=%s bits=%d)", m->authuser, *ssfp); } fprintf(f, "\r\n\tby %s (Cyrus %s) with LMTP", config_servername, CYRUS_VERSION); #ifdef HAVE_SSL if (cd->tls_conn) { char tls_info[250]; tls_info[0] = '\0'; /* grab TLS info for Received: header */ tls_get_info(cd->tls_conn, tls_info, sizeof(tls_info)); if (*tls_info) fprintf(f, " (%s)", tls_info); } #endif /* HAVE_SSL */ fprintf(f, "; %s\r\n", datestr); /* add any requested headers */ if (func->addheaders) { fputs(func->addheaders, f); } /* fill the cache */ r = fill_cache(cd->pin, f, m); if (r) { fclose(f); if (func->removespool) { /* remove the spool'd message */ func->removespool(m); } while (nrcpts--) { send_lmtp_error(cd->pout, r); } return r; } /* now, using our header cache, fill in the data that we want */ /* first check resent-message-id */ if ((body = msg_getheader(m, "resent-message-id")) != NULL) { m->id = xstrdup(body[0]); } else if ((body = msg_getheader(m, "message-id")) != NULL) { m->id = xstrdup(body[0]); } else { m->id = NULL; /* no message-id */ } if (!m->return_path && (body = msg_getheader(m, "return-path"))) { /* let's grab return_path */ m->return_path = xstrdup(body[0]); clean822space(m->return_path); clean_retpath(m->return_path); } fflush(f); if (ferror(f)) { while (nrcpts--) { prot_printf(cd->pout, "451 4.3.%c cannot copy message to temporary file: %s\r\n", ( #ifdef EDQUOT errno == EDQUOT || #endif errno == ENOSPC) ? '1' : '2', error_message(errno)); } fclose(f); func->removespool(m); return IMAP_IOERROR; } if (fstat(fileno(f), &sbuf) == -1) { while (nrcpts--) { prot_printf(cd->pout, "451 4.3.2 cannot stat message temporary file: %s\r\n", error_message(errno)); } fclose(f); func->removespool(m); return IMAP_IOERROR; } m->size = sbuf.st_size; m->f = f; m->data = prot_new(fileno(f), 0); return 0; } /* see if 'addr' exists. if so, fill in 'ad' appropriately. on success, return NULL. on failure, return the error. */ static int process_recipient(char *addr, int ignorequota, int (*verify_user)(const char *, long, struct auth_state *), message_data_t *msg) { char *dest; char *user; int r, sl; address_data_t *ret = (address_data_t *) xmalloc(sizeof(address_data_t)); int forcedowncase = config_getswitch("lmtp_downcase_rcpt", 0); assert(addr != NULL && msg != NULL); if (*addr == '<') addr++; dest = user = addr; /* preserve the entire address */ ret->all = xstrdup(addr); sl = strlen(ret->all); if (ret->all[sl-1] == '>') ret->all[sl-1] = '\0'; /* now find just the user */ /* Skip at-domain-list */ if (*addr == '@') { addr = strchr(addr, ':'); if (!addr) { free(ret->all); free(ret); return IMAP_PROTOCOL_BAD_PARAMETERS; } addr++; } if (*addr == '\"') { addr++; while (*addr && *addr != '\"') { if (*addr == '\\') addr++; *dest++ = *addr++; } } else { if(forcedowncase) { /* We should downcase the localpart up to the first + */ while(*addr != '@' && *addr != '>' && *addr != '+') { if(*addr == '\\') addr++; *dest++ = TOLOWER(*addr++); } } /* Now finish the remainder of the localpart */ while (*addr != '@' && *addr != '>') { if (*addr == '\\') addr++; *dest++ = *addr++; } } *dest = '\0'; r = verify_user(user, ignorequota ? -1 : msg->size, msg->authstate); if (r) { /* we lost */ free(ret->all); free(ret); return r; } ret->user = xstrdup(user); ret->ignorequota = ignorequota; msg->rcpt[msg->rcpt_num] = ret; return 0; } static int localauth_mechlist_override( void *context __attribute__((unused)), const char *plugin_name __attribute__((unused)), const char *option, const char **result, unsigned *len) { /* If we are doing local auth, we only support EXTERNAL */ if (strcmp(option,"mech_list")==0) { *result = "EXTERNAL"; if (len) *len = strlen(*result); return SASL_OK; } /* if we don't find the option, this should percolate to the global getopt */ return SASL_FAIL; } static struct sasl_callback localauth_override_cb[] = { { SASL_CB_GETOPT, &localauth_mechlist_override, NULL }, { SASL_CB_LIST_END, NULL, NULL }, }; void lmtpmode(struct lmtp_func *func, struct protstream *pin, struct protstream *pout, int fd) { message_data_t *msg = NULL; char shutdownfilename[1024]; int shutdown_fd = -1; int max_msgsize; char buf[4096]; char *p; int r; struct clientdata cd; struct sockaddr_storage localaddr, remoteaddr; int havelocal = 0, haveremote = 0; char localip[60], remoteip[60]; socklen_t salen; char hbuf[NI_MAXHOST]; sasl_ssf_t ssf; char *auth_id; int plaintext_result; int secflags = 0; sasl_security_properties_t *secprops = NULL; enum { EXTERNAL_AUTHED = -1, /* -1: external auth'd, but no AUTH issued */ NOAUTH = 0, DIDAUTH = 1 } authenticated = NOAUTH; /* setup the clientdata structure */ cd.pin = pin; cd.pout = pout; cd.fd = fd; cd.clienthost[0] = '\0'; cd.lhlo_param[0] = '\0'; #ifdef HAVE_SSL cd.tls_conn = NULL; #endif cd.starttls_done = 0; snprintf(shutdownfilename, sizeof(shutdownfilename), "%s/msg/shutdown", config_dir); max_msgsize = config_getint("maxmessagesize", INT_MAX); msg_new(&msg); /* don't leak old connections */ if(saslprops.iplocalport) { free(saslprops.iplocalport); saslprops.iplocalport = NULL; } if(saslprops.ipremoteport) { free(saslprops.ipremoteport); saslprops.ipremoteport = NULL; } /* determine who we're talking to */ salen = sizeof(remoteaddr); r = getpeername(fd, (struct sockaddr *)&remoteaddr, &salen); if (!r && (remoteaddr.ss_family == AF_INET || remoteaddr.ss_family == AF_INET6) ) { /* connected to an internet socket */ getnameinfo((struct sockaddr *)&remoteaddr, salen, hbuf, sizeof(hbuf), NULL, 0, NI_WITHSCOPEID); strlcpy(cd.clienthost, hbuf, sizeof(cd.clienthost) - 30); strlcat(cd.clienthost, " [", sizeof(cd.clienthost)); getnameinfo((struct sockaddr *)&remoteaddr, salen, hbuf, sizeof(hbuf), NULL, 0, NI_NUMERICHOST | NI_WITHSCOPEID); strlcat(cd.clienthost, hbuf, sizeof(cd.clienthost)); strlcat(cd.clienthost, "]", sizeof(cd.clienthost)); salen = sizeof(localaddr); if (!getsockname(fd, (struct sockaddr *)&localaddr, &salen)) { /* set the ip addresses here */ if(iptostring((struct sockaddr *)&localaddr, salen, localip, sizeof(localip)) == 0) { havelocal = 1; saslprops.iplocalport = xstrdup(localip); } if(iptostring((struct sockaddr *)&remoteaddr, salen, remoteip, sizeof(remoteip)) == 0) { haveremote = 1; saslprops.ipremoteport = xstrdup(remoteip); } } else { fatal("can't get local addr", EC_SOFTWARE); } syslog(LOG_DEBUG, "connection from %s%s", cd.clienthost, func->preauth ? " preauth'd as postman" : ""); } else { /* we're not connected to a internet socket! */ func->preauth = 1; strcpy(cd.clienthost, "[unix socket]"); syslog(LOG_DEBUG, "lmtp connection preauth'd as postman"); } /* Setup SASL to go. We need to do this *after* we decide if * we are preauthed or not. */ if (sasl_server_new("lmtp", config_servername, NULL, NULL, NULL, (func->preauth ? localauth_override_cb : NULL), 0, &cd.conn) != SASL_OK) { fatal("SASL failed initializing: sasl_server_new()", EC_TEMPFAIL); } /* set my allowable security properties */ /* ANONYMOUS is silly because we allow that anyway */ secflags = SASL_SEC_NOANONYMOUS; plaintext_result = config_getswitch("allowplaintext",1); if (!config_getswitch("lmtp_allowplaintext", plaintext_result)) { secflags |= SASL_SEC_NOPLAINTEXT; } secprops = mysasl_secprops(secflags); sasl_setprop(cd.conn, SASL_SEC_PROPS, secprops); if (func->preauth) { authenticated = EXTERNAL_AUTHED; /* we'll allow commands, but we still accept the AUTH command */ ssf = 2; auth_id = "postman"; sasl_setprop(cd.conn, SASL_SSF_EXTERNAL, &ssf); sasl_setprop(cd.conn, SASL_AUTH_EXTERNAL, auth_id); deliver_logfd = telemetry_log(auth_id, pin, pout, 0); } else { if(havelocal) sasl_setprop(cd.conn, SASL_IPLOCALPORT, &localip ); if(haveremote) sasl_setprop(cd.conn, SASL_IPREMOTEPORT, &remoteip); } prot_printf(pout, "220 %s LMTP Cyrus %s ready\r\n", config_servername, CYRUS_VERSION); for (;;) { nextcmd: signals_poll(); if (!prot_fgets(buf, sizeof(buf), pin)) { const char *err = prot_error(pin); if (err != NULL) { prot_printf(pout, "421 4.4.1 bye %s\r\n", err); prot_flush(pout); } goto cleanup; } p = buf + strlen(buf) - 1; if (p >= buf && *p == '\n') *p-- = '\0'; if (p >= buf && *p == '\r') *p-- = '\0'; /* Only allow LHLO/NOOP/QUIT when there is a shutdown file */ if (!strchr("LlNnQq", buf[0]) && (shutdown_fd = open(shutdownfilename, O_RDONLY, 0)) != -1) { struct protstream *shutdown_in = prot_new(shutdown_fd, 0); prot_fgets(buf, sizeof(buf), shutdown_in); if ((p = strchr(buf, '\r'))!=NULL) *p = 0; if ((p = strchr(buf, '\n'))!=NULL) *p = 0; prot_printf(pout, "421 4.3.2 %s\r\n", buf); prot_flush(pout); func->shutdown(0); } switch (buf[0]) { case 'a': case 'A': if (!strncasecmp(buf, "auth ", 5)) { char mech[128]; int sasl_result; const char *user; if (authenticated > 0) { prot_printf(pout, "503 5.5.0 already authenticated\r\n"); continue; } if (msg->rcpt_num != 0) { prot_printf(pout, "503 5.5.0 AUTH not permitted now\r\n"); continue; } /* ok, what mechanism ? */ p = buf + 5; while ((*p != ' ') && (*p != '\0')) { p++; } if (*p == ' ') { *p = '\0'; p++; } else { p = NULL; } strlcpy(mech, buf + 5, sizeof(mech)); r = saslserver(cd.conn, mech, p, "334 ", pin, pout, &sasl_result, NULL); if (r) { const char *errorstring = NULL; switch (r) { case IMAP_SASL_CANCEL: prot_printf(pout, "501 5.5.4 client canceled authentication\r\n"); break; case IMAP_SASL_PROTERR: errorstring = prot_error(pin); prot_printf(pout, "501 5.5.4 Error reading client response: %s\r\n", errorstring ? errorstring : ""); break; default: if (sasl_result == SASL_NOMECH) { prot_printf(pout, "504 Unrecognized authentication type.\r\n"); continue; } else { sleep(3); if (remoteaddr.ss_family == AF_INET || remoteaddr.ss_family == AF_INET6) getnameinfo((struct sockaddr *)&remoteaddr, salen, hbuf, sizeof(hbuf), NULL, 0, NI_NUMERICHOST | NI_WITHSCOPEID); else strlcpy(hbuf, "[unix socket]", sizeof(hbuf)); syslog(LOG_ERR, "badlogin: %s %s %s", hbuf, mech, sasl_errdetail(cd.conn)); snmp_increment_args(AUTHENTICATION_NO, 1, VARIABLE_AUTH, hash_simple(mech), VARIABLE_LISTEND); prot_printf(pout, "501 5.5.4 %s\r\n", sasl_errstring((r == SASL_NOUSER ? SASL_BADAUTH : r), NULL, NULL)); } } reset_saslconn(&cd.conn); continue; } r = sasl_getprop(cd.conn, SASL_USERNAME, (const void **) &user); if (r != SASL_OK) { prot_printf(pout, "501 5.5.4 SASL Error\r\n"); reset_saslconn(&cd.conn); goto nextcmd; } /* Create telemetry log */ deliver_logfd = telemetry_log(user, pin, pout, 0); /* authenticated successfully! */ snmp_increment_args(AUTHENTICATION_YES,1, VARIABLE_AUTH, hash_simple(mech), VARIABLE_LISTEND); syslog(LOG_NOTICE, "login: %s %s %s%s %s", cd.clienthost, user, mech, cd.starttls_done ? "+TLS" : "", "User logged in"); authenticated += 2; prot_printf(pout, "235 Authenticated!\r\n"); /* set protection layers */ prot_setsasl(pin, cd.conn); prot_setsasl(pout, cd.conn); continue; } goto syntaxerr; case 'd': case 'D': if (!strcasecmp(buf, "data")) { int delivered = 0; int j; if (!msg->rcpt_num) { prot_printf(pout, "503 5.5.1 No recipients\r\n"); continue; } /* copy message from input to msg structure */ r = savemsg(&cd, func, msg); if (r) { goto rset; } if (msg->size > max_msgsize) { prot_printf(pout, "552 5.2.3 Message size (%d) exceeds fixed " "maximum message size (%d)\r\n", msg->size, max_msgsize); continue; } snmp_increment(mtaReceivedMessages, 1); snmp_increment(mtaReceivedVolume, roundToK(msg->size)); snmp_increment(mtaReceivedRecipients, msg->rcpt_num); /* do delivery, report status */ r = func->deliver(msg, msg->authuser, msg->authstate); for (j = 0; j < msg->rcpt_num; j++) { if (!msg->rcpt[j]->status) delivered++; send_lmtp_error(pout, msg->rcpt[j]->status); } snmp_increment(mtaTransmittedMessages, delivered); snmp_increment(mtaTransmittedVolume, roundToK(delivered * msg->size)); goto rset; } goto syntaxerr; case 'l': case 'L': if (!strncasecmp(buf, "lhlo ", 5)) { unsigned int mechcount; const char *mechs; prot_printf(pout, "250-%s\r\n" "250-8BITMIME\r\n" "250-ENHANCEDSTATUSCODES\r\n" "250-PIPELINING\r\n", config_servername); if (max_msgsize < INT_MAX) prot_printf(pout, "250-SIZE %d\r\n", max_msgsize); else prot_printf(pout, "250-SIZE\r\n"); if (tls_enabled("lmtp") && !cd.starttls_done && !authenticated) { prot_printf(pout, "250-STARTTLS\r\n"); } if (authenticated <= 0 && sasl_listmech(cd.conn, NULL, "AUTH ", " ", "", &mechs, NULL, &mechcount) == SASL_OK && mechcount > 0) { prot_printf(pout,"250-%s\r\n", mechs); } prot_printf(pout, "250 IGNOREQUOTA\r\n"); strlcpy(cd.lhlo_param, buf + 5, sizeof(cd.lhlo_param)); continue; } goto syntaxerr; case 'm': case 'M': if (!authenticated) { if (config_getswitch("soft_noauth", 1)) { prot_printf(pout, "430 Authentication required\r\n"); } else { prot_printf(pout, "530 Authentication required\r\n"); } continue; } if (!strncasecmp(buf, "mail ", 5)) { char *tmp; if (msg->return_path) { prot_printf(pout, "503 5.5.1 Nested MAIL command\r\n"); continue; } /* +5 to get past "mail " * +10 to get past "mail from:" */ if (strncasecmp(buf+5, "from:", 5) != 0 || !(msg->return_path = parseaddr(buf+10))) { prot_printf(pout, "501 5.5.4 Syntax error in parameters\r\n"); continue; } tmp = buf+10+strlen(msg->return_path); /* is any other whitespace allow seperating? */ while (*tmp == ' ') { tmp++; switch (*tmp) { case 'a': case 'A': if (strncasecmp(tmp, "auth=", 5) != 0) { goto badparam; } tmp += 5; msg->authuser = parseautheq(&tmp); if (msg->authuser) { msg->authstate = auth_newstate(msg->authuser, NULL); } else { /* do we want to bounce mail because of this? */ /* i guess not. accept with no auth user */ msg->authstate = NULL; } break; case 'b': case 'B': if (strncasecmp(tmp, "body=", 5) != 0) { goto badparam; } tmp += 5; /* just verify it's one of body-value ::= "7BIT" / "8BITMIME" */ if (!strncasecmp(tmp, "7bit", 4)) { tmp += 4; } else if (!strncasecmp(tmp, "8bitmime", 8)) { tmp += 8; } else { prot_printf(pout, "501 5.5.4 Unrecognized BODY type\r\n"); goto nextcmd; } break; case 's': case 'S': if (strncasecmp(tmp, "size=", 5) != 0) { goto badparam; } tmp += 5; /* make sure we have a value */ if (!isdigit((int) *tmp)) { prot_printf(pout, "501 5.5.2 SIZE requires a value\r\n"); goto nextcmd; } msg->size = strtoul(tmp, &p, 10); tmp = p; /* make sure the value is in range */ if (errno == ERANGE || msg->size < 0 || msg->size > max_msgsize) { prot_printf(pout, "552 5.2.3 Message SIZE exceeds fixed " "maximum message size (%d)\r\n", max_msgsize); goto nextcmd; } break; default: badparam: prot_printf(pout, "501 5.5.4 Unrecognized parameters\r\n"); goto nextcmd; } } if (*tmp != '\0') { prot_printf(pout, "501 5.5.4 Syntax error in parameters\r\n"); continue; } prot_printf(pout, "250 2.1.0 ok\r\n"); continue; } goto syntaxerr; case 'n': case 'N': if (!strcasecmp(buf, "noop")) { prot_printf(pout,"250 2.0.0 ok\r\n"); continue; } goto syntaxerr; case 'q': case 'Q': if (!strcasecmp(buf, "quit")) { prot_printf(pout,"221 2.0.0 bye\r\n"); prot_flush(pout); goto cleanup; } goto syntaxerr; case 'r': case 'R': if (!strncasecmp(buf, "rcpt ", 5)) { char *rcpt = NULL; int ignorequota = 0; char *tmp; if (!msg->return_path) { prot_printf(pout, "503 5.5.1 Need MAIL command\r\n"); continue; } if (!(msg->rcpt_num % RCPT_GROW)) { /* time to alloc more */ msg->rcpt = (address_data_t **) xrealloc(msg->rcpt, (msg->rcpt_num + RCPT_GROW + 1) * sizeof(address_data_t *)); } /* +5 to get past "rcpt " * +8 to get past "rcpt to:" */ if (strncasecmp(buf+5, "to:", 3) != 0 || !(rcpt = parseaddr(buf+8))) { prot_printf(pout, "501 5.5.4 Syntax error in parameters\r\n"); continue; } tmp = buf+8+strlen(rcpt); while (*tmp == ' ') { tmp++; switch (*tmp) { case 'i': case 'I': if (strncasecmp(tmp, "ignorequota", 12) != 0) { goto badrparam; } tmp += 12; ignorequota = 1; break; default: badrparam: prot_printf(pout, "501 5.5.4 Unrecognized parameters\r\n"); goto nextcmd; } } if (*tmp != '\0') { prot_printf(pout, "501 5.5.4 Syntax error in parameters\r\n"); continue; } r = process_recipient(rcpt, ignorequota, func->verify_user, msg); if (rcpt) free(rcpt); /* malloc'd in parseaddr() */ if (r) { send_lmtp_error(pout, r); continue; } msg->rcpt_num++; msg->rcpt[msg->rcpt_num] = NULL; prot_printf(pout, "250 2.1.5 ok\r\n"); continue; } else if (!strcasecmp(buf, "rset")) { prot_printf(pout, "250 2.0.0 ok\r\n"); rset: if (msg) msg_free(msg); msg_new(&msg); continue; } goto syntaxerr; case 's': case 'S': #ifdef HAVE_SSL if (!strcasecmp(buf, "starttls") && tls_enabled("lmtp") && !func->preauth) { /* don't need TLS for preauth'd connect */ int *layerp; sasl_ssf_t ssf; char *auth_id; /* SASL and openssl have different ideas about whether ssf is signed */ layerp = &ssf; if (cd.starttls_done == 1) { prot_printf(pout, "454 4.3.3 %s\r\n", "Already successfully executed STARTTLS"); continue; } if (msg->rcpt_num != 0) { prot_printf(pout, "503 5.5.0 STARTTLS not permitted now\r\n"); continue; } r=tls_init_serverengine("lmtp", 5, /* depth to verify */ 1, /* can client auth? */ 1); /* TLS only? */ if (r == -1) { syslog(LOG_ERR, "[lmtpd] error initializing TLS"); prot_printf(pout, "454 4.3.3 %s\r\n", "Error initializing TLS"); continue; } prot_printf(pout, "220 %s\r\n", "Begin TLS negotiation now"); /* must flush our buffers before starting tls */ prot_flush(pout); r=tls_start_servertls(0, /* read */ 1, /* write */ layerp, &auth_id, &(cd.tls_conn)); /* if error */ if (r==-1) { prot_printf(pout, "454 4.3.3 STARTTLS failed\r\n"); syslog(LOG_NOTICE, "[lmtpd] STARTTLS failed: %s", cd.clienthost); continue; } /* tell SASL about the negotiated layer */ r=sasl_setprop(cd.conn, SASL_SSF_EXTERNAL, &ssf); if (r != SASL_OK) fatal("sasl_setprop(SASL_SSF_EXTERNAL) failed: STARTTLS", EC_TEMPFAIL); saslprops.ssf = ssf; r=sasl_setprop(cd.conn, SASL_AUTH_EXTERNAL, auth_id); if (r != SASL_OK) fatal("sasl_setprop(SASL_AUTH_EXTERNAL) failed: STARTTLS", EC_TEMPFAIL); if(saslprops.authid) { free(saslprops.authid); saslprops.authid = NULL; } if(auth_id) saslprops.authid = xstrdup(auth_id); /* tell the prot layer about our new layers */ prot_settls(pin, cd.tls_conn); prot_settls(pout, cd.tls_conn); cd.starttls_done = 1; continue; } #endif /* HAVE_SSL*/ goto syntaxerr; case 'v': case 'V': if (!strncasecmp(buf, "vrfy ", 5)) { prot_printf(pout, "252 2.3.3 try RCPT to attempt delivery\r\n"); continue; } goto syntaxerr; default: syntaxerr: prot_printf(pout, "500 5.5.2 Syntax error\r\n"); continue; } } cleanup: /* free resources and return; this connection has been closed */ if (msg) msg_free(msg); /* security */ if (cd.conn) sasl_dispose(&cd.conn); cd.starttls_done = 0; #ifdef HAVE_SSL if (cd.tls_conn) { tls_reset_servertls(&cd.tls_conn); cd.tls_conn = NULL; } #endif } /************** client-side LMTP ****************/ enum { CAPA_PIPELINING = 1 << 0, CAPA_AUTH = 1 << 1, CAPA_IGNOREQUOTA = 1 << 2 }; struct lmtp_conn { char *host; int sock; struct protstream *pin, *pout; sasl_conn_t *saslconn; /* lmtp specific properties */ int capability; char *mechs; }; #define ISGOOD(r) (((r) / 100) == 2) #define TEMPFAIL(r) (((r) / 100) == 4) #define PERMFAIL(r) (((r) / 100) == 5) #define ISCONT(s) (s && (s[3] == '-')) static int revconvert_lmtp(const char *code) { int c = atoi(code); switch (c) { case 250: case 251: return 0; case 451: if (code[4] == '4' && code[6] == '3') { if (code[8] == '0') { return IMAP_IOERROR; } else if (code[8] == '1') { return IMAP_NOSPACE; } else { return IMAP_IOERROR; } } else if (code[4] == '4' && code [6] == '4') { return IMAP_SERVER_UNAVAILABLE; } else { return IMAP_IOERROR; } case 452: return IMAP_QUOTA_EXCEEDED; case 550: if (code[4] == '5' && code[6] == '7') { return IMAP_PERMISSION_DENIED; } else if (code[4] == '5' && code[6] == '1') { return IMAP_MAILBOX_NONEXISTENT; } return IMAP_PERMISSION_DENIED; case 552: if (code[6] == '2') { return IMAP_QUOTA_EXCEEDED; } else if (code[6] == '3') { return IMAP_MESSAGE_TOO_LARGE; } return IMAP_QUOTA_EXCEEDED; case 554: return IMAP_MESSAGE_BADHEADER; /* sigh, pick one */ default: if (ISGOOD(c)) return 0; else if (TEMPFAIL(c)) return IMAP_AGAIN; else if (PERMFAIL(c)) return IMAP_PROTOCOL_ERROR; else return IMAP_AGAIN; } } static int ask_code(const char *s) { int ret = 0; if (s==NULL) return -1; if (strlen(s) < 3) return -1; /* check to make sure 0-2 are digits */ if ((isdigit((int) s[0])==0) || (isdigit((int) s[1])==0) || (isdigit((int) s[2])==0)) { return -1; } ret = ((s[0]-'0')*100)+((s[1]-'0')*10)+(s[2]-'0'); return ret; } static void chop(char *s) { char *p; assert(s); p = s + strlen(s) - 1; if (p[0] == '\n') { *p-- = '\0'; } if (p >= s && p[0] == '\r') { *p-- = '\0'; } } static int mysasl_getauthline(struct protstream *p, char **line, unsigned int *linelen) { char buf[2096]; char *str = (char *) buf; if (!prot_fgets(str, sizeof(buf), p)) { return SASL_FAIL; } if (str[0] == '2') { return SASL_OK; } if (str[0] == '5') { return SASL_BADAUTH; } if (str[0] != '3') { return SASL_BADPROT; } else { size_t len; str += 4; /* jump past the "334 " */ len = strlen(str) + 1; *line = xmalloc(strlen(str) + 1); if (*str != '\r') { /* decode it */ int r; r = sasl_decode64(str, strlen(str), *line, len, linelen); if (r != SASL_OK) { return r; } return SASL_CONTINUE; } else { /* blank challenge */ *line = NULL; *linelen = 0; return SASL_CONTINUE; } } } /* getlastresp reads from 'pin' until we get an LMTP that isn't a continuation. it puts it in 'buf', which must be at least 'len' big. '*code' will contain the integer three digit response code. if a read failed, '*code == 400', a temporary failure. returns an IMAP error code. */ static int getlastresp(char *buf, int len, int *code, struct protstream *pin) { do { if (!prot_fgets(buf, len, pin)) { *code = 400; return IMAP_SERVER_UNAVAILABLE; } } while (ISCONT(buf)); *code = ask_code(buf); return 0; } /* perform authentication against connection 'conn' returns the SMTP error code from the AUTH attempt */ static int do_auth(struct lmtp_conn *conn) { int r; const int AUTH_ERROR = 420, AUTH_OK = 250; sasl_security_properties_t *secprops = NULL; struct sockaddr_storage saddr_l; struct sockaddr_storage saddr_r; socklen_t addrsize; char buf[2048]; char *in; const char *out; unsigned int inlen, outlen; const char *mechusing; unsigned b64len; char localip[60], remoteip[60]; secprops = mysasl_secprops(0); r = sasl_setprop(conn->saslconn, SASL_SEC_PROPS, secprops); if (r != SASL_OK) { syslog(LOG_ERR, "lmtpengine do_auth: could not sasl_setprop the security properties"); return AUTH_ERROR; } /* set the IP addresses */ addrsize=sizeof(struct sockaddr_storage); if (getpeername(conn->sock, (struct sockaddr *)&saddr_r, &addrsize) != 0) { syslog(LOG_ERR, "lmtpengine do_auth: getpeername() failed"); return AUTH_ERROR; } addrsize=sizeof(struct sockaddr_storage); if (getsockname(conn->sock, (struct sockaddr *)&saddr_l,&addrsize)!=0) { syslog(LOG_ERR, "lmtpengine do_auth: getsockname() failed"); return AUTH_ERROR; } if (iptostring((struct sockaddr *)&saddr_r, addrsize, remoteip, sizeof(remoteip)) != 0) { syslog(LOG_ERR, "lmtpengine do_auth: iptostring() (remote) failed"); return AUTH_ERROR; } if (iptostring((struct sockaddr *)&saddr_l, addrsize, localip, sizeof(localip)) != 0) { syslog(LOG_ERR, "lmtpengine do_auth: iptostring() (local) failed"); return AUTH_ERROR; } r = sasl_setprop(conn->saslconn, SASL_IPLOCALPORT, localip); if (r != SASL_OK) { syslog(LOG_ERR, "lmtpengine do_auth: sasl_setprop(SASL_IPLOCALPORT) failed"); return AUTH_ERROR; } r = sasl_setprop(conn->saslconn, SASL_IPREMOTEPORT, remoteip); if (r != SASL_OK) { syslog(LOG_ERR, "lmtpengine do_auth: sasl_setprop(SASL_IPREMOTEPORT) failed"); return AUTH_ERROR; } /* we now do the actual SASL exchange */ r = sasl_client_start(conn->saslconn, conn->mechs, NULL, &out, &outlen, &mechusing); if ((r != SASL_OK) && (r != SASL_CONTINUE)) { syslog(LOG_ERR, "lmtpengine do_auth: sasl_client_start failed (%s)", sasl_errdetail(conn->saslconn)); return AUTH_ERROR; } if (out == NULL) { prot_printf(conn->pout, "AUTH %s\r\n", mechusing); } else { /* send initial challenge */ r = sasl_encode64(out, outlen, buf, sizeof(buf), &b64len); if (r != SASL_OK) { syslog(LOG_ERR, "lmtpengine do_auth: sasl_encode64[1] failed (%s)", sasl_errstring(r, NULL, NULL)); return AUTH_ERROR; } prot_printf(conn->pout, "AUTH %s %s\r\n", mechusing, buf); } in = NULL; inlen = 0; r = mysasl_getauthline(conn->pin, &in, &inlen); while (r == SASL_CONTINUE) { r = sasl_client_step(conn->saslconn, in, inlen, NULL, &out, &outlen); if (in) { free(in); } if (r != SASL_OK && r != SASL_CONTINUE) { syslog(LOG_ERR, "lmtpengine do_auth: sasl_client_step failed (%s)", sasl_errdetail(conn->saslconn)); return AUTH_ERROR; } r = sasl_encode64(out, outlen, buf, sizeof(buf), &b64len); if (r != SASL_OK) { syslog(LOG_ERR, "lmtpengine do_auth: sasl_encode64[2] failed (%s)", sasl_errstring(r, NULL, NULL)); return AUTH_ERROR; } prot_write(conn->pout, buf, b64len); prot_printf(conn->pout, "\r\n"); r = mysasl_getauthline(conn->pin, &in, &inlen); } if (r == SASL_OK) { prot_setsasl(conn->pin, conn->saslconn); prot_setsasl(conn->pout, conn->saslconn); /* success */ return AUTH_OK; } else { syslog(LOG_ERR, "lmtpengine do_auth: failed to authenticate"); /* don't bounce the message just because *we* can't authenticate */ return AUTH_ERROR; } } /* establish connection, LHLO, and AUTH if possible */ int lmtp_connect(const char *phost, sasl_callback_t *cb, struct lmtp_conn **ret) { int sock = -1; char *host = xstrdup(phost); struct lmtp_conn *conn; char buf[8192]; int code; int unix_socket = 0; assert(host); assert(ret); if (host[0] == '/') { struct sockaddr_un addr; /* open unix socket */ if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { syslog(LOG_ERR, "socket() failed %m"); goto errsock; } addr.sun_family = AF_UNIX; strlcpy(addr.sun_path, host, sizeof(addr.sun_path)); if (connect(sock, (struct sockaddr *) &addr, sizeof(addr.sun_family) + strlen(addr.sun_path) + 1) < 0) { syslog(LOG_ERR, "connect(%s) failed: %m", addr.sun_path); goto errsock; } /* set that we are preauthed */ unix_socket = 1; /* change host to 'config_servername' */ free(host); host = xstrdup(config_servername); } else { struct addrinfo hints, *res0 = NULL, *res; int err; char *p; if (*host == '[' && (p = strchr(host + 1, ']')) != NULL && (*++p == '\0' || *p == ':')) { host++; *(p - 1) = '\0'; if (*p != ':') p = NULL; } else p = strchr(host, ':'); if (p) { *p++ = '\0'; } else { p = "lmtp"; } memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; err = getaddrinfo(host, p, &hints, &res0); if (err) { syslog(LOG_ERR, "getaddrinfo(%s, %s) failed: %s", host, p, gai_strerror(err)); goto errsock; } for (res = res0; res; res = res->ai_next) { sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sock < 0) continue; if (connect(sock, res->ai_addr, res->ai_addrlen) >= 0) break; close(sock); sock = -1; } freeaddrinfo(res0); if (sock < 0) { syslog(LOG_ERR, "connect(%s:%s) failed: %m", host, p); goto errsock; } } conn = xmalloc(sizeof(struct lmtp_conn)); conn->host = host; conn->sock = sock; conn->capability = 0; conn->mechs = NULL; conn->saslconn = NULL; /* setup prot layers */ conn->pin = prot_new(sock, 0); conn->pout = prot_new(sock, 1); prot_setflushonread(conn->pin, conn->pout); /* read greeting */ getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (!ISGOOD(code)) goto done; /* LHLO */ prot_printf(conn->pout, "LHLO %s\r\n", config_servername); /* read responses */ for (;;) { if (prot_fgets(buf, sizeof(buf), conn->pin)) { code = ask_code(buf); if (code == 250) { chop(buf); /* check capability */ if (!strcasecmp(buf + 4, "PIPELINING")) { conn->capability |= CAPA_PIPELINING; } if (!strncasecmp(buf + 4, "AUTH ", 5)) { conn->capability |= CAPA_AUTH; /* save mechanisms for later */ conn->mechs = xstrdup(buf + 9); } if (!strcasecmp(buf + 4, "IGNOREQUOTA")) { conn->capability |= CAPA_IGNOREQUOTA; } } if (ISCONT(buf) && ISGOOD(code)) { continue; } else { break; } } /* can't read response */ code = 400; break; } /* check status code */ if (!ISGOOD(code)) goto done; /* AUTH (but only if we're not preauthed as postman!) */ if (!unix_socket && (conn->capability & CAPA_AUTH) && (conn->mechs)) { sasl_client_new("lmtp", host, NULL, NULL, cb, 0, &conn->saslconn); code = do_auth(conn); } done: if (ISGOOD(code)) { /* return connection */ *ret = conn; return 0; } else { /* not a successful connection; tear it down and return failure */ if (conn) { if (conn->host) free(conn->host); if (conn->mechs) free(conn->mechs); if (conn->saslconn) sasl_dispose(&conn->saslconn); if (conn->sock) close(conn->sock); free(conn); } return IMAP_SERVER_UNAVAILABLE; } errsock: /* error during connection */ if (sock != -1) close(sock); free(host); return IMAP_IOERROR; } static void pushmsg(struct protstream *in, struct protstream *out, int isdotstuffed) { char buf[8192], *p; int lastline_hadendline = 1; /* -2: Might need room to add a \r\n\0 set */ while (prot_fgets(buf, sizeof(buf)-2, in)) { /* dot stuff */ if (!isdotstuffed && (lastline_hadendline == 1) && (buf[0]=='.')) { prot_putc('.', out); } p = buf + strlen(buf) - 1; if (*p == '\n') { if (p == buf || p[-1] != '\r') { p[0] = '\r'; p[1] = '\n'; p[2] = '\0'; } lastline_hadendline = 1; } else if (*p == '\r') { if (buf[0] == '\r' && buf[1] == '\0') { /* The message contained \r\0, and fgets is confusing us. XXX ignored */ lastline_hadendline = 1; } else { /* * We were unlucky enough to get a CR just before we ran * out of buffer--put it back. */ prot_ungetc('\r', in); *p = '\0'; lastline_hadendline = 0; } } else { lastline_hadendline = 0; } /* Remove any lone CR characters */ while ((p = strchr(buf, '\r')) && p[1] != '\n') { /* Src/Target overlap, use memmove */ /* strlen(p) will result in copying the NUL byte as well */ memmove(p, p+1, strlen(p)); } prot_write(out, buf, strlen(buf)); } if (!isdotstuffed) { /* signify end of message */ prot_printf(out, "\r\n.\r\n"); } } int lmtp_runtxn(struct lmtp_conn *conn, struct lmtp_txn *txn) { int j, code, r = 0; char buf[8192]; int onegood; assert(conn && txn); /* pipelining v. no pipelining? */ /* here's the straightforward non-pipelining version */ /* rset */ prot_printf(conn->pout, "RSET\r\n"); r = getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (!ISGOOD(code)) { goto failall; } /* mail from */ if (!txn->from) { prot_printf(conn->pout, "MAIL FROM:<>"); } else if (txn->from[0] == '<') { prot_printf(conn->pout, "MAIL FROM:%s", txn->from); } else { prot_printf(conn->pout, "MAIL FROM:<%s>", txn->from); } if (conn->capability & CAPA_AUTH) { prot_printf(conn->pout, " AUTH=%s", txn->auth && txn->auth[0] ? txn->auth : "<>"); } prot_printf(conn->pout, "\r\n"); r = getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (!ISGOOD(code)) { goto failall; } /* rcpt to */ onegood = 0; for (j = 0; j < txn->rcpt_num; j++) { prot_printf(conn->pout, "RCPT TO:<%s>", txn->rcpt[j].addr); if (txn->rcpt[j].ignorequota && (conn->capability & CAPA_IGNOREQUOTA)) { prot_printf(conn->pout, " IGNOREQUOTA"); } prot_printf(conn->pout, "\r\n"); r = getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (r) { goto failall; } txn->rcpt[j].r = revconvert_lmtp(buf); if (ISGOOD(code)) { onegood = 1; txn->rcpt[j].result = RCPT_GOOD; } else if (TEMPFAIL(code)) { txn->rcpt[j].result = RCPT_TEMPFAIL; } else if (PERMFAIL(code)) { txn->rcpt[j].result = RCPT_PERMFAIL; } else { /* yikes?!? */ code = 400; goto failall; } } if (!onegood) { /* all recipients failed! */ return 0; } /* data */ prot_printf(conn->pout, "DATA\r\n"); r = getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (r) { goto failall; } if (code != 354) { /* erg? */ if (ISGOOD(code)) code = 400; r = IMAP_PROTOCOL_ERROR; goto failall; } /* send the data, dot-stuffing as needed */ pushmsg(txn->data, conn->pout, txn->isdotstuffed); /* read the response codes, one for each accepted RCPT TO */ for (j = 0; j < txn->rcpt_num; j++) { if (txn->rcpt[j].result == RCPT_GOOD) { /* expecting a status code for this recipient */ r = getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (r) { /* technically, some recipients might've succeeded here, but we'll be paranoid */ goto failall; } txn->rcpt[j].r = revconvert_lmtp(buf); if (ISGOOD(code)) { onegood = 1; txn->rcpt[j].result = RCPT_GOOD; } else if (TEMPFAIL(code)) { txn->rcpt[j].result = RCPT_TEMPFAIL; } else if (PERMFAIL(code)) { txn->rcpt[j].result = RCPT_PERMFAIL; } else { /* yikes?!? */ txn->rcpt[j].result = RCPT_TEMPFAIL; } } } /* done with txn */ return 0; failall: /* something fatal happened during the transaction; we should assign 'code' to all recipients and return */ for (j = 0; j < txn->rcpt_num; j++) { if (ISGOOD(code)) { txn->rcpt[j].r = 0; txn->rcpt[j].result = RCPT_GOOD; } else if (TEMPFAIL(code)) { txn->rcpt[j].r = IMAP_AGAIN; txn->rcpt[j].result = RCPT_TEMPFAIL; } else if (PERMFAIL(code)) { txn->rcpt[j].r = IMAP_PROTOCOL_ERROR; txn->rcpt[j].result = RCPT_PERMFAIL; } else { /* code should have been a valid number */ abort(); } } /* return overall error code already set */ return r; } /* send a NOOP to the conn to verify it's still ok */ int lmtp_verify_conn(struct lmtp_conn *conn) { char buf[8192]; int r = 0; int code = 0; /* noop me */ prot_printf(conn->pout, "NOOP\r\n"); r = getlastresp(buf, sizeof(buf)-1, &code, conn->pin); if (!r && !ISGOOD(code)) { r = IMAP_SERVER_UNAVAILABLE; } return r; } int lmtp_disconnect(struct lmtp_conn *conn) { /* quit */ prot_printf(conn->pout, "QUIT\r\n"); /* wait for any response */ prot_getc(conn->pin); /* close connection */ close(conn->sock); /* free 'conn' */ free(conn->host); prot_free(conn->pin); prot_free(conn->pout); if (conn->saslconn) sasl_dispose(&conn->saslconn); if (conn->mechs) free(conn->mechs); return 0; } /* Reset the given sasl_conn_t to a sane state */ static int reset_saslconn(sasl_conn_t **conn) { int ret, secflags, plaintext_result; sasl_security_properties_t *secprops = NULL; sasl_dispose(conn); /* do initialization typical of service_main */ ret = sasl_server_new("lmtp", config_servername, NULL, NULL, NULL, NULL, 0, conn); if(ret != SASL_OK) return ret; if(saslprops.ipremoteport) ret = sasl_setprop(*conn, SASL_IPREMOTEPORT, saslprops.ipremoteport); if(ret != SASL_OK) return ret; if(saslprops.iplocalport) ret = sasl_setprop(*conn, SASL_IPLOCALPORT, saslprops.iplocalport); if(ret != SASL_OK) return ret; secflags = SASL_SEC_NOANONYMOUS; plaintext_result = config_getswitch("allowplaintext", 1); if (!config_getswitch("lmtp_allowplaintext", plaintext_result)) { secflags |= SASL_SEC_NOPLAINTEXT; } secprops = mysasl_secprops(secflags); ret = sasl_setprop(*conn, SASL_SEC_PROPS, secprops); if(ret != SASL_OK) return ret; /* end of service_main initialization excepting SSF */ /* If we have TLS/SSL info, set it */ if(saslprops.ssf) { ret = sasl_setprop(*conn, SASL_SSF_EXTERNAL, &saslprops.ssf); } if(ret != SASL_OK) return ret; if(saslprops.authid) { ret = sasl_setprop(*conn, SASL_AUTH_EXTERNAL, saslprops.authid); if(ret != SASL_OK) return ret; } /* End TLS/SSL Info */ return SASL_OK; }