/** \file data.c \brief receive and queue message data */ #include #include #include #include #include #include #include "netio.h" #include "log.h" #include "qsmtpd.h" #include "antispam.h" #include "version.h" #include "tls.h" #include "qsdata.h" #define MAXHOPS 100 /* maximum number of "Received:" lines allowed in a mail (loop prevention) */ static const char *noqueue = "451 4.3.2 can not connect to queue\r\n"; static int fd0[2], fd1[2]; /* the fds to communicate with qmail-queue */ static pid_t qpid; /* the pid of qmail-queue */ unsigned long maxbytes; static int chunked; /* set to 1 if BDAT transfer is running */ static int err_pipe(void) { log_write(LOG_ERR, "cannot create pipe to qmail-queue"); return netwrite(noqueue) ? errno : 0; } static int err_fork(void) { log_write(LOG_ERR, "cannot fork qmail-queue"); return netwrite(noqueue) ? errno : 0; } /** * reset queue descriptors */ void rset_queue(void) { if (fd0[1]) { while (close(fd0[1]) && (errno == EINTR)); fd0[1] = 0; } while (close(fd1[1]) && (errno == EINTR)); } static const char *qqbin; static int queue_init(void) { int i; if (pipe(fd0)) { if ( (i = err_pipe()) ) return i; return EDONE; } if (pipe(fd1)) { /* EIO on pipe operations? Shit just happens (although I don't know why this could ever happen) */ while (close(fd0[0]) && (errno == EINTR)); while (close(fd0[1]) && (errno == EINTR)); if ( (i = err_pipe()) ) return i; return EDONE; } /* DJB uses vfork at this point (qmail.c::open_qmail) which looks broken * because he modifies data before calling execve */ switch (qpid = fork()) { case -1: if ( (i = err_fork()) ) return i; return EDONE; case 0: if (!qqbin) { qqbin = getenv("QMAILQUEUE"); if (!qqbin) { qqbin = "bin/qmail-queue"; } } while (close(fd0[1])) { if (errno != EINTR) _exit(120); } while (close(fd1[1])) { if (errno != EINTR) _exit(120); } munmap(rcpthosts, rcpthsize); while (close(rcpthfd)) { if (errno != EINTR) _exit(120); } if (dup2(fd0[0], 0) == -1) _exit(120); if (dup2(fd1[0], 1) == -1) _exit(120); /* no chdir here, we already _are_ there (and qmail-queue does it again) */ execlp(qqbin, qqbin, NULL); _exit(120); default: while (close(fd0[0]) && (errno == EINTR)); while (close(fd1[0]) && (errno == EINTR)); } /* check if the child already returned, which means something went wrong */ if (waitpid(qpid, NULL, WNOHANG)) { /* error here may just happen, we are already in trouble */ while (close(fd0[1]) && (errno == EINTR)); while (close(fd1[1]) && (errno == EINTR)); if ( (i = err_fork()) ) return i; return EDONE; } return 0; } static inline void two_digit(char *buf, int num) { *buf = '0' + (num / 10); *(buf + 1) = '0' + (num % 10); } /** * write RfC822 date information to buffer * * @param buf buffer to store string in, must have at least 32 bytes free * * exactly 31 bytes in buffer are filled, it will _not_ be 0-terminated */ static void date822(char *buf) { time_t ti; struct tm stm; const char *weekday[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; const char *month[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; long tz; ti = time(NULL); localtime_r(&ti, &stm); memcpy(buf, weekday[stm.tm_wday], 3); memcpy(buf + 3, ", ", 2); two_digit(buf + 5, stm.tm_mday); buf[7] = ' '; memcpy(buf + 8, month[stm.tm_mon], 3); buf[11] = ' '; ultostr(1900 + stm.tm_year, buf + 12); buf[16] = ' '; /* this will fail after 9999, but that I'll fix then */ two_digit(buf + 17, stm.tm_hour); buf[19] = ':'; two_digit(buf + 20, stm.tm_min); buf[22] = ':'; two_digit(buf + 23, stm.tm_sec); buf[25] = ' '; tzset(); tz = timezone / 60; buf[26] = (tz <= 0) ? '+' : '-'; if (tz < 0) tz = -tz; if (stm.tm_isdst > 0) { two_digit(buf + 27, 1 + (tz / 60)); } else { two_digit(buf + 27, tz / 60); } two_digit(buf + 29, tz % 60); } #define WRITE(fd,buf,len) if ( (rc = write(fd, buf, len)) < 0 ) \ return rc static int queue_header(void) { int fd = fd0[1]; int rc; char datebuf[35] = ">; "; /* the date for the Received-line */ size_t i = (authhide && (xmitstat.authname.len || xmitstat.tlsclient)) ? 2 : 0; const char *afterprot = "A\n\tfor <"; /* the string to be written after the protocol */ const char *authstr = ") (auth="; /* write the "Received: " line to mail header */ WRITE(fd, "Received: from ", 15); if (!i) { if (xmitstat.remotehost.s) { WRITE(fd, xmitstat.remotehost.s, xmitstat.remotehost.len); } else { WRITE(fd, "unknown", 7); } WRITE(fd, " ([", 3); WRITE(fd, xmitstat.remoteip, strlen(xmitstat.remoteip)); WRITE(fd, "]", 1); if (xmitstat.helostr.len) { WRITE(fd, " HELO ", 6); WRITE(fd, xmitstat.helostr.s, xmitstat.helostr.len); } } if (xmitstat.authname.len) { WRITE(fd, authstr + i, 8 - i); WRITE(fd, xmitstat.authname.s, xmitstat.authname.len); } else if (xmitstat.remoteinfo) { WRITE(fd, ") (", 3); WRITE(fd, xmitstat.remoteinfo, strlen(xmitstat.remoteinfo)); } WRITE(fd, ")\n\tby ", 6); WRITE(fd, heloname.s, heloname.len); WRITE(fd, " (" VERSIONSTRING ") with ", 9 + strlen(VERSIONSTRING)); if (chunked) WRITE(fd, "(chunked) ", 10); WRITE(fd, protocol, strlen(protocol)); /* add the 'A' to the end of ESMTP or ESMTPS as described in RfC 3848 */ i = xmitstat.authname.len ? 0 : 1; WRITE(fd, afterprot + i, 8 - i); WRITE(fd, head.tqh_first->to.s, head.tqh_first->to.len); date822(datebuf + 3); datebuf[34] = '\n'; WRITE(fd, datebuf, 35); /* write "Received-SPF: " line */ if (!(xmitstat.authname.len || xmitstat.tlsclient) && (relayclient != 1)) { if ( (rc = spfreceived(fd, xmitstat.spf)) ) return rc; } return 0; } #undef WRITE #define WRITE(fd,buf,len) if ( (rc = write(fd, buf, len)) < 0 ) \ goto err_write static int queue_envelope(const unsigned long msgsize) { char s[ULSTRLEN]; /* msgsize */ char t[ULSTRLEN]; /* goodrcpt */ char bytes[] = " bytes, "; const char *logmail[] = {"received ", "", "", "message to <", NULL, "> from <", MAILFROM, "> ", "from ip [", xmitstat.remoteip, "] (", s, bytes, NULL, " recipients)", NULL}; char *authmsg = NULL; int rc, e; int fd = fd1[1]; if (ssl) logmail[1] = "encrypted "; if (chunked) logmail[2] = "chunked "; ultostr(msgsize, s); if (goodrcpt > 1) { ultostr(goodrcpt, t); logmail[13] = t; } else { bytes[6] = ')'; bytes[7] = '\0'; /* logmail[14] is already NULL so that logging will stop there */ } /* print the authname.s into a buffer for the log message */ if (xmitstat.authname.len) { if (strcasecmp(xmitstat.authname.s, MAILFROM)) { authmsg = malloc(xmitstat.authname.len + 23); if (!authmsg) return errno; memcpy(authmsg, "> (authenticated as ", 20); memcpy(authmsg + 20, xmitstat.authname.s, xmitstat.authname.len); memcpy(authmsg + 20 + xmitstat.authname.len, ") ", 3); logmail[7] = authmsg; } else { logmail[7] = "> (authenticated) "; } } /* write the envelope information to qmail-queue */ /* write the return path to qmail-queue */ WRITE(fd, "F", 1); WRITE(fd, MAILFROM, xmitstat.mailfrom.len + 1); while (head.tqh_first != NULL) { struct recip *l = head.tqh_first; logmail[4] = l->to.s; if (l->ok) { const char *at = strchr(l->to.s, '@'); log_writen(LOG_INFO, logmail); WRITE(fd, "T", 1); if (at && (*(at + 1) == '[')) { WRITE(fd, l->to.s, at - l->to.s + 1); WRITE(fd, liphost.s, liphost.len + 1); } else { WRITE(fd, l->to.s, l->to.len + 1); } } TAILQ_REMOVE(&head, head.tqh_first, entries); free(l->to.s); free(l); } WRITE(fd, "", 1); err_write: e = errno; while ( (rc = close(fd)) ) { if (errno != EINTR) { e = errno; break; } } while (head.tqh_first != NULL) { struct recip *l = head.tqh_first; TAILQ_REMOVE(&head, head.tqh_first, entries); free(l->to.s); free(l); } freedata(); free(authmsg); errno = e; return rc; } static int queue_result(void) { int status; while(waitpid(qpid, &status, 0) == -1) { /* don't know why this could ever happen, but we want to be sure */ if (errno == EINTR) { log_write(LOG_ERR, "waitpid(qmail-queue) went wrong"); return netwrite("451 4.3.2 error while writing mail to queue\r\n") ? errno : EDONE; } } if (WIFEXITED(status)) { int exitcode = WEXITSTATUS(status); if (!exitcode) { return netwrite("250 2.5.0 accepted message for delivery\r\n") ? errno : 0; } else { char ec[ULSTRLEN]; const char *logmess[] = {"qmail-queue failed with exitcode ", ec, NULL}; const char *netmsg; ultostr(exitcode, ec); log_writen(LOG_ERR, logmess); /* stolen from qmail.c::qmail_close */ switch(exitcode) { case 11: netmsg = "554 5.1.3 envelope address too long for qq\r\n"; break; case 31: netmsg = "554 5.3.0 mail server permanently rejected message\r\n"; break; case 51: netmsg = "451 4.3.0 qq out of memory\r\n"; break; case 52: netmsg = "451 4.3.0 qq timeout\r\n"; break; case 53: netmsg = "451 4.3.0 qq write error or disk full\r\n"; break; case 54: netmsg = "451 4.3.0 qq read error\r\n"; break; case 55: netmsg = "451 4.3.0 qq unable to read configuration\r\n"; break; case 56: netmsg = "451 4.3.0 qq trouble making network connection\r\n"; break; case 61: netmsg = "451 4.3.0 qq trouble in home directory\r\n"; break; case 63: case 64: case 65: case 66: case 62: netmsg = "451 4.3.0 qq trouble creating files in queue\r\n"; break; case 71: netmsg = "451 4.3.0 mail server temporarily rejected message\r\n"; break; case 72: netmsg = "451 4.4.1 connection to mail server timed out\r\n"; break; case 73: netmsg = "451 4.4.1 connection to mail server rejected\r\n"; break; case 74: netmsg = "451 4.4.2 communication with mail server failed\r\n"; break; case 91: /* this happens when the 'F' and 'T' are not correctly sent or understood. */ case 81: netmsg = "451 4.3.0 qq internal bug\r\n"; break; default: if ((exitcode >= 11) && (exitcode <= 40)) netmsg = "554 5.3.0 qq permanent problem\r\n"; else netmsg = "451 4.3.0 qq temporary problem\r\n"; } return netwrite(netmsg) ? errno : EDONE; } } else { log_write(LOG_ERR, "WIFEXITED(qmail-queue) went wrong"); return netwrite("451 4.3.2 error while writing mail to queue\r\n") ? errno : EDONE; } } static unsigned long msgsize; /** * handle DATA command and store data into queue * * @return 0 on success, else error code */ int smtp_data(void) { char s[ULSTRLEN]; /* msgsize */ const char *logmail[] = {"rejected message to <", NULL, "> from <", MAILFROM, "> from ip [", xmitstat.remoteip, "] (", s, " bytes) {", NULL, NULL}; int i, rc; int fd; int flagdate = 0, flagfrom = 0; /* Date: and From: are required in header, * else message is bogus (RfC 2822, section 3.6). * RfC 2821 says server SHOULD NOT check for this, * but we let the user decide */ const char *errmsg = NULL; unsigned int hops = 0; /* number of "Received:"-lines */ msgsize = 0; chunked = 0; if (badbounce || !goodrcpt) { tarpit(); return netwrite("554 5.1.1 no valid recipients\r\n") ? errno : EDONE; } if ( (i = queue_init()) ) return i; if ( (rc = hasinput()) ) { while (close(fd0[1]) && (errno == EINTR)); while (close(fd1[1]) && (errno == EINTR)); while ((waitpid(qpid, NULL, 0) == -1) && (errno == EINTR)); return rc; } if (netwrite("354 Start mail input; end with .\r\n")) { int e = errno; while (close(fd0[1]) && (errno == EINTR)); while (close(fd1[1]) && (errno == EINTR)); while ((waitpid(qpid, NULL, 0) == -1) && (errno == EINTR)); return e; } #ifdef DEBUG_IO in_data = 1; #endif /* fd is now the file descriptor we are writing to. This is better than always * calculating the offset to fd0[1] */ fd = fd0[1]; if ( (rc = queue_header()) ) goto err_write; /* loop until: * -the message is bigger than allowed * -we reach the empty line between header and body * -we reach the end of the transmission */ if (net_read()) goto err_write; /* write the data to mail */ while (!((linelen == 1) && (linein[0] == '.')) && (msgsize <= maxbytes) && linelen && (hops <= MAXHOPS)) { if (linein[0] == '.') { /* write buffer beginning at [1], we do not have to check if the second character * is also a '.', RfC 2821 says only we should discard the '.' beginning the line */ WRITE(fd, linein + 1, linelen - 1); msgsize += linelen + 1; } else { int flagr = 1; /* if the line may be a "Received:" or "Delivered-To:"-line */ if (xmitstat.check2822 & 1) { if (!strncasecmp("Date:", linein, 5)) { if (flagdate) { logmail[9] = "more than one 'Date:' in header}"; errmsg = "550 5.6.0 message does not comply to RfC2822: " "more than one 'Date:'\r\n"; goto loop_data; } else { flagdate = 1; flagr = 0; } } else if (!strncasecmp("From:", linein, 5)) { if (flagfrom) { logmail[9] = "more than one 'From:' in header}"; errmsg = "550 5.6.0 message does not comply to RfC2822: " "more than one 'From:'\r\n"; goto loop_data; } else { flagfrom = 1; flagr = 0; } } for (i = linelen - 1; i >= 0; i--) { if (linein[i] < 0) { logmail[9] = "8bit-character in message header}"; errmsg = "550 5.6.0 message does not comply to RfC2822: " "8bit character in message header\r\n"; goto loop_data; } } } if (flagr) { if (!strncasecmp("Received:", linein, 9)) { if (++hops > MAXHOPS) { logmail[9] = "mail loop}"; errmsg = "554 5.4.6 too many hops, this message is looping\r\n"; goto loop_data; } } else if ((linelen > 20) && !strncmp("Delivered-To:", linein, 13)) { /* we write it exactly this way, noone else is allowed to * change our header lines so we do not need to use strncasecmp * * The minimum length of 21 are a sum of: * 13: Delivered-To: * 1: ' ' * 1: at least 1 character localpart * 1: @ * 2: at least 2 characters domain name * 1: '.' * 2: at least 2 characters top level domain */ struct recip *np; for (np = head.tqh_first; np != NULL; np = np->entries.tqe_next) { if (np->ok && !strcmp(linein + 14, np->to.s)) { logmail[9] = "mail loop}"; errmsg = "554 5.4.6 message is looping, found a \"Delivered-To:\" line with one of the recipients\r\n"; goto loop_data; } } } } /* write buffer beginning at [0] */ WRITE(fd, linein, linelen); msgsize += linelen + 2; } WRITE(fd, "\n", 1); /* this has to stay here and can't be combined with the net_read before the while loop: * if we combine them we add an extra new line for the line that ends the transmission */ if (net_read()) goto err_write; } if (xmitstat.check2822 & 1) { if (!flagdate) { logmail[9] = "no 'Date:' in header}"; errmsg = "550 5.6.0 message does not comply to RfC2822: 'Date:' missing\r\n"; goto loop_data; } else if (!flagfrom) { logmail[9] = "no 'From:' in header}"; errmsg = "550 5.6.0 message does not comply to RfC2822: 'From:' missing\r\n"; goto loop_data; } } if (!linelen) { /* if(linelen) message has no body and we already are at the end */ WRITE(fd, "\n", 1); if (net_read()) goto err_write; while (!((linelen == 1) && (linein[0] == '.')) && (msgsize <= maxbytes)) { int offset; if ((xmitstat.check2822 & 1) && !xmitstat.datatype) { for (i = linelen - 1; i >= 0; i--) if (linein[i] < 0) { logmail[9] = "8bit-character in message body}"; errmsg = "550 5.6.0 message contains 8bit characters\r\n"; goto loop_data; } } offset = (linein[0] == '.') ? 1 : 0; WRITE(fd, linein + offset, linelen - offset); msgsize += linelen + 2 - offset; WRITE(fd, "\n", 1); if (net_read()) goto err_write; } } if (msgsize > maxbytes) { logmail[9] = "message too big}"; rc = EMSGSIZE; errmsg = NULL; goto loop_data; } /* the message body is sent to qmail-queue. Close the file descriptor and send the envelope information */ while (close(fd)) { if (errno != EINTR) goto err_write; } fd0[1] = 0; if (queue_envelope(msgsize)) goto err_write; #ifdef DEBUG_IO in_data = 0; #endif commands[7].state = (0x008 << xmitstat.esmtp); return queue_result(); loop_data: while (close(fd0[1]) && (errno == EINTR)); fd0[1] = 0; /* eat all data until the transmission ends. But just drop it and return * an error defined before jumping here */ do { msgsize += linelen + 2; if (linein[0] == '.') msgsize--; if (net_read()) goto err_write; } while ((linelen != 1) && (linein[0] != '.')); while (close(fd1[1]) && (errno == EINTR)); ultostr(msgsize, s); while (head.tqh_first != NULL) { struct recip *l = head.tqh_first; TAILQ_REMOVE(&head, head.tqh_first, entries); if (l->ok) { logmail[1] = l->to.s; log_writen(LOG_INFO, logmail); } free(l->to.s); free(l); } freedata(); #ifdef DEBUG_IO in_data = 0; #endif if (errmsg) return netwrite(errmsg) ? errno : EDONE; return rc; err_write: rc = errno; rset_queue(); freedata(); /* first check, then read: if the error happens on the last line nothing will be read here */ while ((linelen != 1) || (linein[0] != '.')) { if (net_read()) break; } #ifdef DEBUG_IO in_data = 0; #endif if ((rc == ENOSPC) || (rc == EFBIG)) { rc = EMSGSIZE; } else if ((errno != ENOMEM) && (errno != EMSGSIZE) && (errno != E2BIG) && (errno != EINVAL)) { if (netwrite("451 4.3.0 error writing mail to queue\r\n")) return errno; } switch (rc) { case EMSGSIZE: case E2BIG: case ENOMEM: return rc; case EPIPE: log_write(LOG_ERR, "broken pipe to qmail-queue"); return EDONE; case EINTR: log_write(LOG_ERR, "interrupt while writing to qmail-queue"); return EDONE; /* This errors happen if client sends invalid data (e.g. bad sequences). */ case EINVAL: return netwrite("500 5.5.2 bad sequence\r\n") ? errno : EBOGUS; /* normally none of the other errors may ever occur. But who knows what I'm missing here? */ default: { const char *logmsg[] = {"error in DATA: ", strerror(rc), NULL}; log_writen(LOG_ERR, logmsg); return EDONE; // will not be caught in main } } } static int bdaterr; /** * handle BDAT command and store data into queue * * @return 0 on success, else error code */ int smtp_bdat(void) { char s[ULSTRLEN]; /* msgsize */ const char *logmail[] = {"rejected message to <", NULL, "> from <", MAILFROM, "> from ip [", xmitstat.remoteip, "] (", s, " bytes) {", NULL, NULL}; int rc; int fd; #warning FIXME: loop detection missing unsigned int hops = 0; /* number of "Received:"-lines */ long chunksize; char *more; if (badbounce || !goodrcpt) { tarpit(); return netwrite("554 5.1.1 no valid recipients\r\n") ? errno : EDONE; } if ((linein[5] < '0') || (linein[5] > '9')) return EINVAL; chunksize = strtol(linein + 5, &more, 10); if ((chunksize < 0) || (*more && (*more != ' '))) return EINVAL; if (*more && strcasecmp(more + 1, "LAST")) return EINVAL; /* fd is now the file descriptor we are writing to. This is better than always * calculating the offset to fd0[1] */ if (comstate != 0x0800) { msgsize = 0; bdaterr = 0; comstate = 0x0800; chunked = 1; bdaterr = queue_init(); if (!bdaterr && (rc = queue_header()) ) { bdaterr = rc; } } fd = fd0[1]; while (chunksize > 0) { size_t chunk; char inbuf[2048]; if (chunksize >= sizeof(inbuf)) { chunk = net_readbin(sizeof(inbuf) - 1, inbuf); } else { chunk = net_readbin(chunksize, inbuf); } if (chunk == (size_t) -1) { if (!bdaterr) { bdaterr = errno; } } else { WRITE(fd, inbuf, chunk); chunksize -= chunk; msgsize += chunk; } } if ((msgsize > maxbytes) && !bdaterr) { logmail[9] = "message too big}"; while (head.tqh_first != NULL) { struct recip *l = head.tqh_first; TAILQ_REMOVE(&head, head.tqh_first, entries); if (l->ok) { logmail[1] = l->to.s; log_writen(LOG_INFO, logmail); } free(l->to.s); free(l); } bdaterr = EMSGSIZE; } /* send envelope data if this is last chunk */ if (*more && !bdaterr) { /* the message body is sent to qmail-queue. Close the file descriptor and * send the envelope information */ while (close(fd)) { if (errno != EINTR) goto err_write; } fd0[1] = 0; if (queue_envelope(msgsize)) goto err_write; commands[11].state = 0x010; return queue_result(); } if (bdaterr) { if (fd1[1]) { rset_queue(); fd1[1] = 0; } } else { const char *bdatmess[] = {"250 2.5.0 ", linein + 5, " octets received", NULL}; bdaterr = net_writen(bdatmess) ? errno : 0; chunked = 0; } return bdaterr; err_write: rc = errno; rset_queue(); freedata(); if ((rc == ENOSPC) || (rc == EFBIG)) { rc = EMSGSIZE; } else if ((errno != ENOMEM) && (errno != EMSGSIZE) && (errno != E2BIG)) { if (netwrite("451 4.3.0 error writing mail to queue\r\n")) return errno; } switch (rc) { case EMSGSIZE: case ENOMEM: return rc; case EPIPE: log_write(LOG_ERR, "broken pipe to qmail-queue"); return EDONE; case EINTR: log_write(LOG_ERR, "interrupt while writing to qmail-queue"); return EDONE; case E2BIG: return rc; /* normally none of the other errors may ever occur. But who knows what I'm missing here? */ default: { const char *logmsg[] = {"error in BDAT: ", strerror(rc), NULL}; log_writen(LOG_ERR, logmsg); return EDONE; // will not be caught in main } } }