#ifdef IDN2 #include #endif #include #include #include #include #include #include #include "alloc.h" #include "base64.h" #include "buffer.h" #include "byte.h" #include "case.h" #include "constmap.h" #include "exit.h" #include "fmt.h" #include "genalloc.h" #include "ip.h" #include "logmsg.h" #include "scan.h" #include "sig.h" #include "socket_if.h" #include "str.h" #include "stralloc.h" #include "timeout.h" #include "timeoutconn.h" #include "uint_t.h" #include "auto_qmail.h" #include "control.h" #include "dns.h" #include "hmac_md5.h" #include "ipalloc.h" #include "ipme.h" #include "now.h" #include "quote.h" #include "tcpto.h" #include "tls_errors.h" #include "tls_remote.h" #include "tls_timeoutio.h" #include "ucspissl.h" #define WHO "qmail-remote" #define MAX_SIZE 200000000 #define HUGESMTPTEXT 1000 /* RFC 5322; was 5000 chars/line */ #define PORT_SMTP 25 /* silly rabbit, /etc/services is for users */ #define PORT_QMTP 209 #define PORT_SMTPS 465 #define PORT_SUBMISSION 587 #define PORT_QMTPS 6209 #define VERIFYDEPTH 1 unsigned long port = PORT_SMTP; /** @file qmail-remote.c -- versatile SMTP(S)/QMTP(S) client */ int flagauth = 0; /* 1 = login; 2 = plain; 3 = crammd5 */ int flagsmtps = 0; /* RFC 8314 - 'implicit TLS' */ int flagtlsdomain = 0; /* 0 = no; 1 = yes; 2 = cert */ int flagtls = 0; /* flagtls: XYZ (mode) Z: -2 = rejected; -1 = not; 0 = no, default; Z > 0 see tls_remote.c (prot) Y: 0 = StartTLS; 1 = SMTPS; 2 = QMTPS (active) X: 1 = running TLS connection (after DNS lookup) (done) Z: 1: CA chain; 2: Cert wildname; 3: Cert exactname; 4: Cert fingerprint; 5: TLSA record */ int flagverify = 0; /* 1 = verify Cert against CA; 2 = verify against Dir; 3 = triggerd by TLSA; -2 = Cert pinning; -1 = no TLSA validation */ int flagutf8 = 0; GEN_ALLOC_typedef(saa, stralloc, sa, len, a) GEN_ALLOC_readyplus( saa, stralloc, sa, len, a, i, n, x, 10, saa_readyplus) static stralloc sauninit = {0}; stralloc helohost = {0}; stralloc eaihost = {0}; stralloc host = {0}; stralloc idnhost = {0}; stralloc sender = {0}; stralloc canonhost = {0}; stralloc remotehost = {0}; stralloc canonbox = {0}; stralloc senddomain = {0}; stralloc sendip = {0}; stralloc domainips = {0}; struct constmap mapdomainips; char ip4[4]; char ip6[16]; uint32 ifidx = 0; char *authsender = 0; stralloc smtproutes = {0}; struct constmap mapsmtproutes; stralloc qmtproutes = {0}; struct constmap mapqmtproutes; saa reciplist = {0}; stralloc recip = {0}; char msgsize[FMT_ULONG]; unsigned long msize = 0; struct ip_mx partner; SSL *ssl; SSL_CTX *ctx; char smallbuf[BUFFER_SMALL]; buffer bs = BUFFER_INIT(write, 1, smallbuf, sizeof(smallbuf)); void out(char *s) { if (buffer_puts(&bs, s) == -1) _exit(0); } void zero() { if (buffer_put(&bs, "\0", 1) == -1) _exit(0); } void zerodie() { zero(); buffer_flush(&bs); if (ssl) tls_exit(ssl); _exit(0); } void outsafe(stralloc *sa) { int i; char ch; for (i = 0; i < sa->len; ++i) { ch = sa->s[i]; if (ch == 0) continue; if (ch < 33) ch = '?'; if (ch > 126) ch = '?'; if (buffer_put(&bs, &ch, 1) == -1) _exit(0); } } void temp_noip() { out("ZInvalid ipaddr in control/domainips (#4.3.0)\n"); zerodie(); } void temp_nomem() { out("ZOut of memory. (#4.3.0)\n"); zerodie(); } void temp_oserr() { out("ZSystem resources temporarily unavailable. (#4.3.0)\n"); zerodie(); } void temp_osip() { out("ZCan't bind to local ip address: "); outsafe(&sendip); out(". (#4.3.0)\n"); zerodie(); } void temp_noconn() { out("ZSorry, I wasn't able to establish an SMTP connection: "); outsafe(&canonhost); out(". (#4.3.0)\n"); zerodie(); } void temp_qmtpnoc() { out("ZSorry, I wasn't able to establish an QMTP connection: "); outsafe(&canonhost); out(". (#4.3.1)\n"); zerodie(); } void temp_read() { out("ZUnable to read message. (#4.3.0)\n"); zerodie(); } void temp_dnscanon() { out("ZCNAME lookup failed temporarily for: "); outsafe(&canonhost); out(". (#4.4.3)\n"); zerodie(); } void temp_dns() { out("ZSorry, I couldn't find any host named: "); outsafe(&host); out(". (#4.1.2)\n"); zerodie(); } void temp_nomx() { out("ZSorry, I couldn't find a mail exchanger or IP address for: "); outsafe(&host); out(". Will try again. (#4.1.2)\n"); zerodie(); } void temp_chdir() { out("ZUnable to switch to home directory. (#4.3.0)\n"); zerodie(); } void temp_control() { out("ZUnable to read control files. (#4.3.0)\n"); zerodie(); } void perm_partialline() { out("DSMTP cannot transfer messages with partial final lines. (#5.6.2)\n"); zerodie(); } void temp_proto() { out("ZRecipient did not talk proper QMTP (#4.3.0)\n"); zerodie(); } void perm_usage() { out("Dqmail-remote was invoked improperly. (#5.3.5)\n"); zerodie(); } void perm_dns() { out("DSorry, I couldn't find any host named: "); outsafe(&host); out(". (#5.1.2)\n"); zerodie(); } void perm_nomx() { out("DSorry, I couldn't find a mail exchanger or IP address for: "); outsafe(&host); out(". (#5.4.4)\n"); zerodie(); } void perm_ambigmx() { out("DSorry. Although I'm listed as a best-preference MX or A for that host,\n\ it isn't in my control/locals file, so I don't treat it as local. (#5.4.6)\n"); zerodie(); } void err_authprot() { out("KNo supported AUTH method found, continuing without authentication.\n"); } void outhost() { char ipaddr[IPFMT]; int len; switch (partner.af) { case AF_INET: len = ip4_fmt(ipaddr, (char *)&partner.addr.ip4.d); break; case AF_INET6: len = ip6_fmt(ipaddr, (char *)&partner.addr.ip6.d); break; } if (buffer_put(&bs, ipaddr, len) == -1) _exit(0); } int flagcritical = 0; void dropped() { out("ZConnected to "); outhost(); out(" but connection died. "); if (flagcritical) out("Possible duplicate! "); out("(#4.4.2)\n"); zerodie(); } int timeoutconnect = 60; int smtpfd; int timeout = 1200; ssize_t saferead(int fd, char *buf, int len) { int r; if (ssl) { r = tls_timeoutread(timeout, smtpfd, smtpfd, ssl, buf, len); if (r < 0) temp_tlserr(); } else { r = timeoutread(timeout, smtpfd, buf, len); } if (r <= 0) dropped(); return r; } ssize_t safewrite(int fd, char *buf, int len) { int r; if (ssl) { r = tls_timeoutwrite(timeout, smtpfd, smtpfd, ssl, buf, len); if (r < 0) temp_tlserr(); } else { r = timeoutwrite(timeout, smtpfd, buf, len); } if (r <= 0) dropped(); return r; } char inbuf[BUFFER_MTUSIZE]; buffer bi = BUFFER_INIT(read, 0, inbuf, sizeof(inbuf)); char outbuf[BUFFER_MTUSIZE]; buffer bo = BUFFER_INIT(safewrite, -1, outbuf, sizeof(outbuf)); char frombuf[BUFFER_SMALL]; buffer bf = BUFFER_INIT(saferead, -1, frombuf, sizeof(frombuf)); static stralloc smtptext = {0}; static stralloc header = {0}; void get(char *ch) { buffer_get(&bf, ch, 1); if (*ch != '\r') if (smtptext.len < HUGESMTPTEXT) if (!stralloc_append(&smtptext, ch)) temp_nomem(); } unsigned long smtpcode() { unsigned char ch; unsigned long code; if (!stralloc_copys(&smtptext, "")) temp_nomem(); get(&ch); code = ch - '0'; get(&ch); code = code * 10 + (ch - '0'); get(&ch); code = code * 10 + (ch - '0'); for (;;) { get(&ch); if (ch != '-') break; while (ch != '\n') get(&ch); get(&ch); get(&ch); get(&ch); } while (ch != '\n') get(&ch); return code; } void outsmtptext() { int i; if (smtptext.s) if (smtptext.len) { out("Remote host said: "); for (i = 0; i < smtptext.len; ++i) if (!smtptext.s[i]) smtptext.s[i] = '?'; if (buffer_put(&bs, smtptext.s, smtptext.len) == -1) _exit(0); smtptext.len = 0; } } void quit(char *prepend, char *append) { buffer_putsflush(&bo, "QUIT\r\n"); /* waiting for remote side is just too ridiculous */ out(prepend); outhost(); out(append); out(".\n"); outsmtptext(); zerodie(); } void blast() { int r; char ch; for (;;) { r = buffer_get(&bi, &ch, 1); if (r == 0) break; if (r == -1) temp_read(); if (ch == '.') buffer_put(&bo, ".", 1); while (ch != '\n') { if (ch != '\r') buffer_put(&bo, &ch, 1); // DKIM input r = buffer_get(&bi, &ch, 1); if (r == 0) perm_partialline(); if (r == -1) temp_read(); } buffer_put(&bo, "\r\n", 2); } flagcritical = 1; buffer_put(&bo, ".\r\n", 3); buffer_flush(&bo); } /* this file is too long -------------------------------------- client TLS */ stralloc cafile = {0}; stralloc cadir = {0}; stralloc certfile = {0}; stralloc keyfile = {0}; stralloc keypwd = {0}; stralloc ciphers = {0}; char *tlsdestinfo = 0; char *tlsdomaininfo = 0; stralloc domaincerts = {0}; struct constmap mapdomaincerts; stralloc tlsdestinations = {0}; struct constmap maptlsdestinations; unsigned long verifydepth = VERIFYDEPTH; void tls_init() { ctx = ssl_client(); ssl_errstr(); if (!ctx) temp_tlsctx(); /* Fetch CA infos for dest */ if (flagverify > 0) if (cafile.len || cadir.len) if (!ssl_ca(ctx, cafile.s, cadir.s, (int)verifydepth)) temp_tlsca(); if (ciphers.len) if (!ssl_ciphers(ctx, ciphers.s)) temp_tlscipher(); /* Prepare for Certificate Request */ if (flagtlsdomain == 2) switch (tls_certkey(ctx, certfile.s, keyfile.s, keypwd.s)) { case 0: break; case -1: temp_tlscert(); case -2: temp_tlskey(); case -3: temp_tlschk(); } /* Set SSL Context */ ssl = ssl_new(ctx, smtpfd); if (!ssl) temp_tlsctx(); /* Setup SSL FDs */ if (!tls_conn(ssl, smtpfd)) temp_tlscon(); /* Go on in none-blocking mode */ if (tls_timeoutconn(timeout, smtpfd, smtpfd, ssl) <= 0) temp_tlserr(); } int starttls_peer() { int i = 0; while ((i += str_chr(smtptext.s + i, '\n') + 1) && (i + 8 < smtptext.len)) { if (!str_diffn(smtptext.s + i + 4, "STARTTLS", 8)) return 1; } return 0; } void tls_peercheck() { X509 *cert; STACK_OF(X509) * certs; cert = SSL_get_peer_certificate(ssl); if (!cert) { flagtls = 100; return; } if ((certs = SSL_get_peer_cert_chain(ssl)) == NULL) { certs = sk_X509_new_null(); sk_X509_push(certs, cert); } if (flagverify == -2) { // fingerprinting is silent if (cafile.len) case_lowerb(cafile.s, cafile.len); switch (tls_fingerprint(cert, cafile.s + 1, cafile.len - 2)) { case -1: temp_tlspeercert(); case -2: temp_tlsdigest(); case -3: temp_invaliddigest(); case 0: temp_tlscertfp(); case 1: flagtls = 104; break; } } if (flagverify >= 0) { // TLSA is default switch (tlsa_check(certs, remotehost, port)) { case -4: temp_tlsamissing(); break; /* FIXME */ case -3: temp_tlsainvalid(); break; case -2: break; // unsupported type; may happen case -1: break; // weird TLSA record case 0: break; // no TLSA record given case 1: case 2: flagtls = 107; flagverify = 3; break; // full certchain available (-PKIX) case 3: flagtls = 106; flagverify = 0; break; // TA-CA; verify wont work case 4: flagtls = 105; flagverify = 0; break; // Endpoint only } } if (flagverify > 0) { switch (tls_checkpeer(ssl, cert, remotehost, flagtls, flagverify)) { case -1: temp_tlspeercert(); case -2: temp_tlspeerverify(); case -3: temp_tlspeervalid(); case 1: flagtls = 101; break; case 2: flagtls = 102; break; case 3: flagtls = 103; break; } } if (flagtls < 100) flagtls = 100; X509_free(cert); X509_free(certs); return; } /* this file is too long --------------------------------------- smtp UTF8 */ int utf8string(unsigned char *ch, int len) { int i = 0; while (i < len) if (ch[i++] > 127) return 1; return 0; } int utf8received() { int r; int i; int received = 0; char ch; stralloc receivedline = {0}; /* we consider only our own last written header */ for (;;) { r = buffer_get(&bi, &ch, 1); if (r == 0) break; if (r == -1) temp_read(); if (ch == '\r') continue; // DKIM if (ch == '\n') { if (!stralloc_append(&header, "\r")) temp_nomem(); /* received.c does not add '\r' */ if (!stralloc_append(&header, "\n")) temp_nomem(); if (case_starts(receivedline.s, "Date:")) return 0; /* header to quit asap */ if (case_starts(receivedline.s, "Received: from")) received++; /* found Received header */ if (received) { if (case_starts(receivedline.s, " by ")) { for (i = 6; i < receivedline.len - 6; ++i) if (*(receivedline.s + i) == ' ') if (case_starts(receivedline.s + i + 1, "with UTF8")) return 1; return 0; } } if (!stralloc_copys(&receivedline, "")) temp_nomem(); } else { if (!stralloc_append(&header, &ch)) temp_nomem(); if (!stralloc_catb(&receivedline, &ch, 1)) temp_nomem(); } } return 0; } /* this file is too long -------------------------------------- smtp client */ unsigned long code; int flagsize = 0; int smtp_size() { int i; if (smtptext.len > 10) for (i = 0; i < smtptext.len; ++i) { if (case_starts(smtptext.s + i, "SIZE ")) return 1; } return 0; ; } void smtp_greeting() { buffer_puts(&bo, "EHLO "); buffer_put(&bo, helohost.s, helohost.len); buffer_puts(&bo, "\r\n"); buffer_flush(&bo); if (smtpcode() != 250) { buffer_puts(&bo, "HELO "); buffer_put(&bo, helohost.s, helohost.len); buffer_puts(&bo, "\r\n"); buffer_flush(&bo); code = smtpcode(); authsender = 0; if (code >= 500) quit("DConnected to ", " but my name was rejected"); if (code != 250) quit("ZConnected to ", " but my name was rejected"); } flagsize = smtp_size(); } void smtp_starttls() { buffer_puts(&bo, "STARTTLS\r\n"); buffer_flush(&bo); if (smtpcode() == 220) { tls_init(); tls_peercheck(); smtp_greeting(); } else { flagtls = -2; quit("ZConnected to ", " but STARTTLS was rejected"); } } void mailfrom() { buffer_puts(&bo, "MAIL FROM:<"); buffer_put(&bo, sender.s, sender.len); buffer_puts(&bo, ">"); if (flagutf8 || utf8received()) buffer_puts(&bo, " SMTPUTF8"); if (flagsize && msize) { buffer_puts(&bo, " SIZE="); buffer_puts(&bo, msgsize); } buffer_puts(&bo, "\r\n"); buffer_flush(&bo); } /* this file is too long -------------------------------------- client auth */ stralloc authsenders = {0}; struct constmap mapauthsenders; stralloc user = {0}; stralloc pass = {0}; stralloc auth = {0}; stralloc chal = {0}; stralloc slop = {0}; stralloc plain = {0}; stralloc xuser = {0}; static const char hextab[] = "0123456789abcdef"; int xtext(stralloc *sa, char *s, int len) { int i; unsigned char c; char xch[2]; if (!stralloc_copys(sa, "")) temp_nomem(); for (i = 0; i < len; i++) { c = s[i]; if (c < 33 || c > 126 || c == '=' || c == '+') { xch[0] = hextab[(c >> 4) & 0x0f]; xch[1] = hextab[c & 0x0f]; if (!stralloc_catb(sa, xch, 2)) temp_nomem(); } else if (!stralloc_catb(sa, s + i, 1)) temp_nomem(); } return sa->len; } void mailfrom_xtext() { if (!xtext(&xuser, user.s, user.len)) temp_nomem(); buffer_puts(&bo, "MAIL FROM:<"); buffer_put(&bo, sender.s, sender.len); buffer_puts(&bo, "> AUTH="); buffer_put(&bo, xuser.s, xuser.len); if (flagutf8 || utf8received()) buffer_puts(&bo, " SMTPUTF8"); if (flagsize && msize) { buffer_puts(&bo, " SIZE="); buffer_puts(&bo, msgsize); } buffer_puts(&bo, "\r\n"); buffer_flush(&bo); } int mailfrom_plain() { buffer_puts(&bo, "AUTH PLAIN\r\n"); buffer_flush(&bo); if (smtpcode() != 334) quit("ZConnected to ", " but authentication was rejected (AUTH PLAIN)"); if (!stralloc_cats(&plain, "")) temp_nomem(); /* RFC 4616 section 2 */ if (!stralloc_0(&plain)) temp_nomem(); if (!stralloc_cat(&plain, &user)) temp_nomem(); /* user-id */ if (!stralloc_0(&plain)) temp_nomem(); if (!stralloc_cat(&plain, &pass)) temp_nomem(); /* password */ if (b64encode(&plain, &auth)) quit("ZConnected to ", " but unable to base64encode (plain)"); buffer_put(&bo, auth.s, auth.len); buffer_puts(&bo, "\r\n"); buffer_flush(&bo); switch (smtpcode()) { case 235: mailfrom_xtext(); break; case 432: quit("DConnected to ", " but password expired"); case 534: quit("ZConnected to ", " but authentication mechamism too weak (plain)"); default: quit("ZConnected to ", " but authentication was rejected (plain)"); } return 0; } int mailfrom_login() { buffer_puts(&bo, "AUTH LOGIN\r\n"); buffer_flush(&bo); if (smtpcode() != 334) quit("ZConnected to ", " but authentication was rejected (AUTH LOGIN)"); if (!stralloc_copys(&auth, "")) temp_nomem(); if (b64encode(&user, &auth)) quit("ZConnected to ", " but unable to base64encode user"); buffer_put(&bo, auth.s, auth.len); buffer_puts(&bo, "\r\n"); buffer_flush(&bo); if (smtpcode() != 334) quit("ZConnected to ", " but authentication was rejected (username)"); if (!stralloc_copys(&auth, "")) temp_nomem(); if (b64encode(&pass, &auth)) quit("ZConnected to ", " but unable to base64encode pass"); buffer_put(&bo, auth.s, auth.len); buffer_puts(&bo, "\r\n"); buffer_flush(&bo); switch (smtpcode()) { case 235: mailfrom_xtext(); break; case 432: quit("DConnected to ", " but password expired"); case 534: quit("ZConnected to ", " but authentication mechanism is too weak (login)"); default: quit("ZConnected to ", " but authentication was rejected (login)"); } return 0; } int mailfrom_cram() { int j; unsigned char digest[16]; unsigned char digascii[33]; buffer_puts(&bo, "AUTH CRAM-MD5\r\n"); buffer_flush(&bo); if (smtpcode() != 334) quit("ZConnected to ", " but authentication was rejected (AUTH CRAM-MD5)"); if (str_chr(smtptext.s + 4, ' ')) { /* Challenge */ if (!stralloc_copys(&slop, "")) temp_nomem(); if (!stralloc_copyb(&slop, smtptext.s + 4, smtptext.len - 5)) temp_nomem(); if (b64decode(slop.s, slop.len, &chal)) quit("ZConnected to ", " but unable to base64decode challenge"); } hmac_md5((unsigned char *)chal.s, chal.len, pass.s, pass.len, digest); for (j = 0; j < 16; j++) { /* HEX => ASCII */ digascii[2 * j] = hextab[digest[j] >> 4]; digascii[2 * j + 1] = hextab[digest[j] & 0x0f]; } digascii[32] = 0; if (!stralloc_copys(&slop, "")) temp_nomem(); if (!stralloc_cat(&slop, &user)) temp_nomem(); /* user-id */ if (!stralloc_cats(&slop, " ")) temp_nomem(); if (!stralloc_catb(&slop, digascii, 32)) temp_nomem(); /* digest */ if (!stralloc_copys(&auth, "")) temp_nomem(); if (b64encode(&slop, &auth)) quit("ZConnected to ", " but unable to base64encode username+digest"); buffer_put(&bo, auth.s, auth.len); buffer_puts(&bo, "\r\n"); buffer_flush(&bo); switch (smtpcode()) { case 235: mailfrom_xtext(); break; case 432: quit("DConnected to ", " but password expired"); case 534: quit("ZConnected to ", " but authentication mechamism too weak (cram)"); default: quit("ZConnected to ", " but authentication was rejected (cram)"); } return 0; } void smtp_auth() { int i; if (smtptext.len > 8) for (i = 4; i < smtptext.len - 5; ++i) { if (case_starts(smtptext.s + i, "CRAM")) if (mailfrom_cram() >= 0) return; if (case_starts(smtptext.s + i, "LOGIN")) if (mailfrom_login() >= 0) return; if (case_starts(smtptext.s + i, "PLAIN")) if (mailfrom_plain() >= 0) return; } err_authprot(); mailfrom(); } /* this file is too long ------------------------------------------- GO ON */ void smtp() { int flagbother; int i; if (flagtls > 10 && flagtls < 20) { /* SMTPS */ tls_init(); tls_peercheck(); } code = smtpcode(); if (code >= 500) quit("DConnected to ", " but sender was rejected"); if (code == 421 || code == 450) quit("ZConnected to ", " but probably greylisted"); /* RFC 6647 */ if (code >= 400) quit("ZConnected to ", " but sender was rejected"); if (code != 220) quit("ZConnected to ", " but greeting failed"); smtp_greeting(); if (flagtls > 0 && flagtls < 10) { /* STARTTLS */ if (starttls_peer()) smtp_starttls(); else if (flagtls > 3 && flagtls != 9) { if (!stralloc_0(&host)) temp_nomem(); temp_tlshost(); } } if (user.len && pass.len) /* AUTH */ smtp_auth(); else mailfrom(); /* Mail From */ code = smtpcode(); if (code >= 500) quit("DConnected to ", " but sender was rejected"); if (code >= 400) quit("ZConnected to ", " but sender was probably greylisted"); flagbother = 0; /* Rcpt To */ for (i = 0; i < reciplist.len; ++i) { buffer_puts(&bo, "RCPT TO:<"); buffer_put(&bo, reciplist.sa[i].s, reciplist.sa[i].len); buffer_puts(&bo, ">\r\n"); buffer_flush(&bo); code = smtpcode(); /* Data */ if (flagsize) { if (code == 552) quit("DConnected to ", " but message size is too large"); if (code == 452) quit("ZConnected to ", " however insufficient storage space available"); } if (code == 421 || code == 450) { // Postfix merde ;-) out("s"); outhost(); out(" sender is greylisting.\n"); outsmtptext(); zero(); } else if (code >= 500) { out("h"); outhost(); out(" does not like recipient.\n"); outsmtptext(); zero(); } else if (code >= 400) { out("s"); outhost(); out(" does not like recipient.\n"); outsmtptext(); zero(); } else { out("r"); zero(); flagbother = 1; } } if (!flagbother) quit("DGiving up on ", ""); buffer_putsflush(&bo, "DATA\r\n"); code = smtpcode(); if (code >= 500) quit("D", " failed on DATA command"); if (code >= 400) quit("Z", " failed on DATA command"); buffer_putflush(&bo, header.s, header.len); blast(); code = smtpcode(); flagcritical = 0; if (code >= 500) quit("D", " failed after I sent the message"); if (code >= 400) quit("Z", " failed after I sent the message"); switch (flagtls) { // StartTLS + SMTPS case 100: case 110: quit("K", " TLS transmitted message accepted"); break; case 101: case 111: quit("K", " TLS (verified CA) transmitted message accepted"); break; case 102: case 112: quit("K", " TLS (validated CA+DN*) transmitted message accepted"); break; case 103: case 113: quit("K", " TLS (validated CA+DN) transmitted message accepted"); break; case 104: case 114: quit("K", " TLS (CERT pinning) transmitted message accepted"); break; case 105: case 115: quit("K", " TLS (TLSA EE validated) transmitted message accepted"); break; case 106: case 116: quit("K", " TLS (TLSA TA validated) transmitted message accepted"); break; case 107: case 117: quit("K", " TLS (TLSA PKIX verified) transmitted message accepted"); break; default: quit("K", " accepted message"); break; } } /* this file is too long -------------------------------------- qmtp client */ int qmtpsend = 0; void qmtp() { unsigned long len; char *x; int i; int n; unsigned char ch; char num[FMT_ULONG]; int flagallok; if (qmtpsend == 2) { /* QMTPS */ tls_init(); tls_peercheck(); } /* the following code was substantially taken from serialmail's serialqmtp.c */ scan_ulong(msgsize, &len); buffer_put(&bo, num, fmt_ulong(num, len + 1)); buffer_put(&bo, ":\n", 2); while (len > 0) { n = buffer_feed(&bi); if (n <= 0) _exit(1); /* wise guy again */ x = buffer_PEEK(&bi); buffer_put(&bo, x, n); buffer_SEEK(&bi, n); len -= n; } buffer_put(&bo, ",", 1); len = sender.len; buffer_put(&bo, num, fmt_ulong(num, len)); buffer_put(&bo, ":", 1); buffer_put(&bo, sender.s, sender.len); buffer_put(&bo, ",", 1); len = 0; for (i = 0; i < reciplist.len; ++i) len += fmt_ulong(num, reciplist.sa[i].len) + 1 + reciplist.sa[i].len + 1; buffer_put(&bo, num, fmt_ulong(num, len)); buffer_put(&bo, ":", 1); for (i = 0; i < reciplist.len; ++i) { buffer_put(&bo, num, fmt_ulong(num, reciplist.sa[i].len)); buffer_put(&bo, ":", 1); buffer_put(&bo, reciplist.sa[i].s, reciplist.sa[i].len); buffer_put(&bo, ",", 1); } buffer_put(&bo, ",", 1); buffer_flush(&bo); flagallok = 1; for (i = 0; i < reciplist.len; ++i) { len = 0; for (;;) { get(&ch); if (ch == ':') break; if (len > 200000000) temp_proto(); if (ch - '0' > 9) temp_proto(); len = 10 * len + (ch - '0'); } if (!len) temp_proto(); get(&ch); --len; if ((ch != 'Z') && (ch != 'D') && (ch != 'K')) temp_proto(); if (!stralloc_copyb(&smtptext, &ch, 1)) temp_proto(); if (flagtls == 100) { if (!stralloc_cats(&smtptext, "qmtps:")) temp_nomem(); } else { if (!stralloc_cats(&smtptext, "qmtp:")) temp_nomem(); } while (len > 0) { get(&ch); --len; } for (len = 0; len < smtptext.len; ++len) { ch = smtptext.s[len]; if ((ch < 32) || (ch > 126)) smtptext.s[len] = '?'; } get(&ch); if (ch != ',') temp_proto(); smtptext.s[smtptext.len - 1] = '\n'; if (smtptext.s[0] == 'K') out("r"); else if (smtptext.s[0] == 'D') { out("h"); flagallok = 0; } else { /* if (smtptext.s[0] == 'Z') */ out("s"); flagallok = 0; } if (buffer_put(&bs, smtptext.s + 1, smtptext.len - 1) == -1) temp_qmtpnoc(); zero(); } if (!flagallok) { out("DGiving up on "); outhost(); out("\n"); } else { out("KAll received okay by "); outhost(); out("\n"); } zerodie(); } /* this file is too long -------------------------------------- common */ /* host has to be canonical [A/AAAA record], box has to be quoted */ void addrmangle(stralloc *saout, char *address, int *flagalias, int flagcname) { int at; int r = 0; stralloc cn = {0}; *flagalias = flagcname; /* saout + flagalias are output */ if (!flagutf8) flagutf8 = utf8string(address, str_len(address)); at = str_rchr(address, '@'); if (!address[at]) { if (!stralloc_copys(saout, address)) temp_nomem(); return; } if (!stralloc_copys(&canonbox, address)) temp_nomem(); canonbox.len = at; if (!quote(saout, &canonbox)) temp_nomem(); /* saout = 'inbox' name without quotes ;-) */ if (!stralloc_cats(saout, "@")) temp_nomem(); if (!stralloc_copys(&canonhost, address + at + 1)) temp_nomem(); if (flagcname) { /* no relayhost */ DNS_INIT switch ((r = dns_cname(&cn, &canonhost))) { case DNS_MEM: temp_nomem(); case DNS_SOFT: temp_dnscanon(); case DNS_HARD: ; /* alias loop, not our problem */ default: if (r > 0) *flagalias = 0; } } if (!stralloc_cat(saout, &canonhost)) temp_nomem(); } void getcontrols() { if (control_init() == -1) temp_control(); if (control_readint(&timeout, "control/timeoutremote") == -1) temp_control(); if (control_readint(&timeoutconnect, "control/timeoutconnect") == -1) temp_control(); if (control_rldef(&helohost, "control/helohost", 1, (char *)0) != 1) temp_control(); switch (control_readfile(&smtproutes, "control/smtproutes", 0)) { case -1: temp_control(); case 0: if (!constmap_init(&mapsmtproutes, "", 0, 1)) temp_nomem(); break; case 1: if (!constmap_init(&mapsmtproutes, smtproutes.s, smtproutes.len, 1)) temp_nomem(); break; } switch (control_readfile(&domainips, "control/domainips", 0)) { case -1: temp_control(); case 0: if (!constmap_init(&mapdomainips, "", 0, 1)) temp_nomem(); break; case 1: if (!constmap_init(&mapdomainips, domainips.s, domainips.len, 1)) temp_nomem(); break; } switch (control_readfile(&authsenders, "control/authsenders", 0)) { case -1: temp_control(); case 0: if (!constmap_init(&mapauthsenders, "", 0, 1)) temp_nomem(); break; case 1: if (!constmap_init(&mapauthsenders, authsenders.s, authsenders.len, 1)) temp_nomem(); break; } switch (control_readfile(&qmtproutes, "control/qmtproutes", 0)) { case -1: temp_control(); case 0: if (!constmap_init(&mapqmtproutes, "", 0, 1)) temp_nomem(); break; case 1: if (!constmap_init(&mapqmtproutes, qmtproutes.s, qmtproutes.len, 1)) temp_nomem(); break; } switch (control_readfile(&domaincerts, "control/domaincerts", 0)) { case -1: temp_control(); case 0: if (!constmap_init(&mapdomaincerts, "", 0, 1)) temp_nomem(); break; case 1: if (!constmap_init(&mapdomaincerts, domaincerts.s, domaincerts.len, 1)) temp_nomem(); break; } switch (control_readfile(&tlsdestinations, "control/tlsdestinations", 0)) { case -1: temp_control(); case 0: if (!constmap_init(&maptlsdestinations, "", 0, 1)) temp_nomem(); break; case 1: if (!constmap_init(&maptlsdestinations, tlsdestinations.s, tlsdestinations.len, 1)) temp_nomem(); break; } } int main(int argc, char **argv) { static ipalloc ip = {0}; stralloc netif = {0}; struct stat st; int i, j, k; int p; /* reserved for port */ int r; /* reserved for return code */ unsigned long random; char **recips; unsigned long prefme; int flagallaliases; int flagalias; char *relayhost; char *localip; int ip6flag = 0; sig_pipeignore(); if (argc < 4) perm_usage(); if (chdir(auto_qmail) == -1) temp_chdir(); getcontrols(); if (!stralloc_copys(&host, argv[1])) temp_nomem(); authsender = 0; relayhost = 0; addrmangle(&sender, argv[2], &flagalias, 0); if (sender.len > 1) { i = str_chr(sender.s, '@'); if (sender.s[i] == '@') if (!stralloc_copyb(&senddomain, sender.s + i + 1, sender.len - i - 1)) temp_nomem(); // un-terminated } /* this file is too long -------------------------------------- set domain ip + helohost */ localip = 0; for (i = 0; i <= senddomain.len; ++i) if ((i == 0) || (senddomain.s[i] == '.')) if ((localip = constmap(&mapdomainips, senddomain.s + i, senddomain.len - i))) break; if (!localip) localip = constmap(&mapdomainips, "*", 1); /* one for all */ if (localip) { j = str_chr(localip, '%'); if (localip[j] != '%') j = 0; k = str_chr(localip, '|'); if (localip[k] != '|') k = 0; if (k) { /* helohost */ if (!stralloc_copys(&helohost, localip + k + 1)) temp_nomem(); if (!stralloc_0(&helohost)) temp_nomem(); localip[k] = 0; } if (j) { /* IF index */ localip[j] = 0; if (!stralloc_copys(&netif, localip + j + 1)) temp_nomem(); if (!stralloc_0(&netif)) temp_nomem(); } } /* this file is too long -------------------------------------- authsender routes */ for (i = 0; i <= sender.len; ++i) if ((i == 0) || (i == sender.len) || (sender.s[i] == '.') || (sender.s[i] == '@')) if ((authsender = constmap(&mapauthsenders, sender.s + i, sender.len - i))) break; if (authsender && !*authsender) authsender = 0; if (authsender) { i = str_chr(authsender, '|'); if (authsender[i] == '|') { j = str_chr(authsender + i + 1, '|'); if (authsender[i + j + 1] == '|') { authsender[i] = 0; authsender[i + j + 1] = 0; if (!stralloc_copys(&user, "")) temp_nomem(); if (!stralloc_copys(&user, authsender + i + 1)) temp_nomem(); if (!stralloc_copys(&pass, "")) temp_nomem(); if (!stralloc_copys(&pass, authsender + i + j + 2)) temp_nomem(); } } p = str_chr(authsender, ';'); if (authsender[p] == ';') { if (authsender[p + 1] == 's') { flagsmtps = 1, p++; } scan_ulong(authsender + p + 1, &port); authsender[p] = 0; } relayhost = authsender; if (!stralloc_copys(&host, authsender)) temp_nomem(); } /* this file is too long -------------------------------------- standard routes */ if (!authsender) { if (sender.len == 0) { /* bounce routes */ if ((relayhost = constmap(&mapqmtproutes, "!@", 2))) { qmtpsend = 1; port = PORT_QMTP; } else relayhost = constmap(&mapsmtproutes, "!@", 2); } if (relayhost && !*relayhost) relayhost = 0; if (!relayhost) { for (i = 0; i <= host.len; ++i) { /* qmtproutes */ if ((i == 0) || (i == host.len) || (host.s[i] == '.')) if ((relayhost = constmap(&mapqmtproutes, host.s + i, host.len - i))) { qmtpsend = 1; port = PORT_QMTP; break; } /* default smtproutes */ if ((relayhost = constmap(&mapsmtproutes, host.s + i, host.len - i))) break; } } if (relayhost && !*relayhost) relayhost = 0; if (relayhost) { /* default smtproutes -- authenticated */ i = str_chr(relayhost, '|'); if (relayhost[i] == '|') { j = str_chr(relayhost + i + 1, '|'); // authenticate if (relayhost[i + j + 1] == '|') { relayhost[i] = 0; relayhost[i + j + 1] = 0; if (!stralloc_copys(&user, "")) temp_nomem(); if (!stralloc_copys(&user, relayhost + i + 1)) temp_nomem(); if (!stralloc_copys(&pass, "")) temp_nomem(); k = str_chr(relayhost + i + j + 2, '|'); // local ip if (relayhost[i + j + k + 2] == '|') { relayhost[i + j + k + 2] = 0; localip = relayhost + i + j + k + 3; } if (!stralloc_copys(&pass, relayhost + i + j + 2)) temp_nomem(); } } p = str_chr(relayhost, ';'); if (relayhost[p] == ';') { if (relayhost[p + 1] == 's') { flagsmtps = 1; p++; } // RFC 8314 scan_ulong(relayhost + p + 1, &port); if (qmtpsend && port == PORT_QMTPS) qmtpsend = 2; relayhost[p] = 0; } if (!stralloc_copys(&host, relayhost)) temp_nomem(); #ifdef IDN2 } else { char *asciihost = 0; if (!stralloc_0(&host)) temp_nomem(); switch (idn2_lookup_u8(host.s, (uint8_t **)&asciihost, IDN2_NFC_INPUT)) { case IDN2_OK: break; case IDN2_MALLOC: temp_nomem(); default: perm_dns(); } if (!stralloc_copys(&idnhost, asciihost)) temp_nomem(); #endif } } /* this file is too long -------------------------------------- TLS destinations */ flagtls = tls_destination((const stralloc)host); // host may not be 0-terminated if (flagtls > 0) { if (tlsdestinfo) { i = str_chr(tlsdestinfo, '|'); /* ca file/dir or cert fingerprint */ if (tlsdestinfo[i] == '|') { tlsdestinfo[i] = 0; j = str_chr(tlsdestinfo + i + 1, '|'); /* cipher */ if (tlsdestinfo[i + j + 1] == '|') { tlsdestinfo[i + j + 1] = 0; k = str_chr(tlsdestinfo + i + j + 2, '|'); /* cone domain */ if (tlsdestinfo[i + j + k + 2] == '|') { tlsdestinfo[i + j + k + 2] = 0; if (str_diffn(tlsdestinfo + j + k + 3, canonhost.s, canonhost.len)) flagtls = 0; } p = str_chr(tlsdestinfo + i + j + 2, ';'); /* verifydepth;port */ if (tlsdestinfo[i + j + p + 2] == ';') { tlsdestinfo[i + j + p + 2] = 0; if (p > 0) scan_ulong(tlsdestinfo + i + j + 2, &verifydepth); if (tlsdestinfo[i + j + p + 3] == 's') { flagsmtps = 1; p++; } /* RFC 8314 */ scan_ulong(tlsdestinfo + i + j + p + 3, &port); } } if (j) if (!stralloc_copys(&ciphers, tlsdestinfo + i + 1)) temp_nomem(); } /* either ':[=]cafile/cadir' -or- ':;port' */ if (tlsdestinfo[0] == ';') scan_ulong(tlsdestinfo + 1, &port); else if (!stralloc_copys(&cafile, tlsdestinfo)) temp_nomem(); } /* cafile starts with '=' => it is a fingerprint cafile ends with '/' => consider it as cadir cafile and cadir are now 0-terminated ciphers are alway 0-terminated if given */ if (cafile.len > 2) { flagverify = 1; if (cafile.s[cafile.len] == '/') { cafile.len = 0; flagverify = 2; if (!stralloc_copys(&cadir, tlsdestinfo)) temp_nomem(); if (!stralloc_0(&cadir)) temp_nomem(); } else { if (cafile.s[0] == '=') flagverify = -2; if (!stralloc_0(&cafile)) temp_nomem(); } } else cafile.len = cadir.len = 0; if (ciphers.len > 4) /* otherwise garbage */ if (!stralloc_0(&ciphers)) temp_nomem(); else ciphers.len = 0; if (port == PORT_SMTPS || flagsmtps) flagtls += 10; if (port == PORT_QMTPS) flagtls += 20; } if (flagtls == 8) flagverify = -1; if (!flagtls && qmtpsend == 2) flagtls = 20; /* QMTPS */ /* this file is too long -------------------------------------- Our Certs - per senddomain */ if (flagtls > 0) { flagtlsdomain = tls_domaincerts((const stralloc)senddomain); // senddomain un-terminated if (flagtlsdomain && tlsdomaininfo) { i = str_chr(tlsdomaininfo, '|'); if (tlsdomaininfo[i] == '|') { tlsdomaininfo[i] = 0; j = str_chr(tlsdomaininfo + i + 1, '|'); if (tlsdomaininfo[i + j + 1] == '|') { tlsdomaininfo[i + j + 1] = 0; if (!stralloc_copys(&keypwd, "")) temp_nomem(); if (!stralloc_copys(&keypwd, tlsdomaininfo + i + j + 2)) temp_nomem(); if (!stralloc_0(&keypwd)) temp_nomem(); } if (!stralloc_copys(&keyfile, tlsdomaininfo + i + 1)) temp_nomem(); if (!stralloc_0(&keyfile)) temp_nomem(); } if (!stralloc_copys(&certfile, tlsdomaininfo)) temp_nomem(); if (!stralloc_0(&certfile)) temp_nomem(); flagtlsdomain = 2; } } /* this file is too long -------------------------------------- work thru reciplist */ if (!saa_readyplus(&reciplist, 0)) temp_nomem(); if (ipme_init() != 1) temp_oserr(); flagallaliases = 1; recips = argv + 3; if (fstat(0, &st) == -1) quit("Z", " unable to fstat stdin"); msize = st.st_size; fmt_ulong(msgsize, msize); while (*recips) { if (!saa_readyplus(&reciplist, 1)) temp_nomem(); reciplist.sa[reciplist.len] = sauninit; addrmangle(reciplist.sa + reciplist.len, *recips, &flagalias, !relayhost); if (!flagalias) flagallaliases = 0; ++reciplist.len; ++recips; } random = now() + (getpid() << 16); #ifdef IDN2 switch (relayhost ? dns_ip(&ip, &host) : dns_mxip(&ip, &idnhost, random)) { #else switch (relayhost ? dns_ip(&ip, &host) : dns_mxip(&ip, &host, random)) { #endif case DNS_MEM: temp_nomem(); case DNS_ERR: temp_dns(); case DNS_COM: temp_dns(); case DNS_SOFT: temp_dns(); #ifdef DEFERREDBOUNCES default: if (!ip.len) temp_nomx(); #else default: if (!ip.len) perm_nomx(); #endif } prefme = 100000; for (i = 0; i < ip.len; ++i) if (ipme_is(&ip.ix[i])) if (ip.ix[i].pref < prefme) prefme = ip.ix[i].pref; if (relayhost) prefme = 300000; if (flagallaliases) prefme = 500000; if (localip) { i = str_chr(localip, ':'); if (localip[i] == ':') ip6flag = 1; else ip6flag = -1; } for (i = 0; i < ip.len; ++i) { /* MX with smallest distance */ if (ip6flag == -1 && ip.ix[i].af == AF_INET6) continue; if (ip6flag == 1 && ip.ix[i].af == AF_INET) continue; if (ip.ix[i].pref < prefme) break; } if (i >= ip.len) perm_ambigmx(); if (!stralloc_copys(&remotehost, ip.ix[i].mxh)) temp_nomem(); /* take MX hostname for TLSA */ if (!stralloc_0(&remotehost)) temp_nomem(); for (i = 0; i < ip.len; ++i) { if (ip.ix[i].pref < prefme) { if (ip6flag == -1 && ip.ix[i].af == AF_INET6) continue; /* explicit binding */ if (ip6flag == 1 && ip.ix[i].af == AF_INET) continue; if (tcpto(&ip.ix[i])) continue; smtpfd = socket(ip.ix[i].af, SOCK_STREAM, 0); if (smtpfd == -1) continue; if (localip) { /* set domain ip */ if (!stralloc_copyb(&sendip, localip, str_len(localip))) temp_nomem(); j = str_chr(localip, ':'); if (localip[j] == ':') { if (!ip6_scan(localip, ip6)) temp_noip(); /* IPv6 */ if (byte_equal(ip.ix[i].addr.ip6.d, 16, ip6)) continue; ifidx = socket_getifidx(netif.s); if (socket_bind6(smtpfd, ip6, 0, ifidx) < 0) temp_osip(); } else { if (!ip4_scan(localip, ip4)) temp_noip(); /* IPv4 */ if (byte_equal(ip.ix[i].addr.ip4.d, 4, ip4)) continue; if (socket_bind4(smtpfd, ip4, 0) < 0) temp_osip(); } } AGAIN: if (ip.ix[i].af == AF_INET6) r = timeoutconn6( smtpfd, (char *)&ip.ix[i].addr.ip6.d, (unsigned int)port, timeoutconnect, ifidx); else r = timeoutconn4(smtpfd, (char *)&ip.ix[i].addr.ip4.d, (unsigned int)port, timeoutconnect); if (r == 0) { tcpto_err(&ip.ix[i], 0); partner = ip.ix[i]; if (qmtpsend) qmtp(); else smtp(); /* read qmail/THOUGHTS; section 6 */ } if (flagtls == 9 && errno == EPROTO) { flagtls = 0; goto AGAIN; } if (errno == ETIMEDOUT || errno == ECONNREFUSED || errno == EPROTO) tcpto_err(&ip.ix[i], 1); close(smtpfd); } } temp_noconn(); }