#ifdef IDN2 #include #endif #include #include #include #include #include #include #include "sig.h" #include "stralloc.h" #include "buffer.h" #include "scan.h" #include "case.h" #include "byte.h" #include "logmsg.h" #include "qmail.h" #include "auto_qmail.h" #include "control.h" #include "dns.h" #include "alloc.h" #include "genalloc.h" #include "quote.h" #include "fmt.h" #include "ip.h" #include "ipalloc.h" #include "ipme.h" #include "str.h" #include "now.h" #include "exit.h" #include "constmap.h" #include "tcpto.h" #include "timeout.h" #include "timeoutconn.h" #include "base64.h" #include "socket_if.h" #include "ucspissl.h" #include "hmac_md5.h" #include "tls_remote.h" #include "tls_errors.h" #include "tls_timeoutio.h" #include "uint_t.h" #define WHO "qmail-remote" #define QMTP_MAX 200000000 /* 190 MB for QMTP */ #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 #define TCP_TIMEOUT 60 #define SMTP_TIMEOUT 1200 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 bufsmall[BUFFER_SMALL]; buffer bs = BUFFER_INIT(write,1,bufsmall,sizeof(bufsmall)); 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 a SMTP connection: "); outsafe(&canonhost); out(". (#4.3.0)\n"); zerodie(); } void temp_qmtpnoc() { out("ZSorry, I wasn't able to establish a 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 line. (#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("ZSorry, no supported AUTH method found, trying later again. (#4.7.1)\n"); zerodie(); } 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 = TCP_TIMEOUT; int smtpfd; int timeout = SMTP_TIMEOUT; 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[BUFSIZE_LINE]; buffer bi = BUFFER_INIT(read,0,inbuf,sizeof(inbuf)); char outbuf[BUFSIZE_LINE]; 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; int in; int out; int eom = 1; // end-of-message . char tmpbuf[BUFSIZE_MESS + 2]; // intermediate write buffer // New blast code; inspired by Bruce Guenter's 'fastremote patch' (2005) while ((r = buffer_get(&bi,inbuf,sizeof(inbuf)))) { // read into buffer if (r == -1) temp_read(); for (in = out = 0; in < r;) { if (eom && inbuf[in] == '.') { tmpbuf[out++] = '.'; tmpbuf[out++] = inbuf[in++]; } eom = 0; while (in < r) { if (inbuf[in] == '\r') { in++; continue; } // CR is DKIM input if (inbuf[in] == '\n') { eom = 1; in++; tmpbuf[out++] = '\r'; tmpbuf[out++] = '\n'; break; } tmpbuf[out++] = inbuf[in++]; } } if (out) buffer_put(&bo,tmpbuf,out); } if (!eom) perm_partialline(); flagcritical = 1; buffer_put(&bo,".\r\n",3); // LF seen; finish with . 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 ehlo_starttls() { int i = 0; do { if (case_startb(smtptext.s + i + 4,8,"STARTTLS")) return 1; } while ((i += str_chr(smtptext.s + i,'\n') + 1) && (i < smtptext.len - 12)); 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 ehlo_size() { int i = 0; do { if (case_startb(smtptext.s + i + 4,4,"SIZE")) return 1; } while ((i += str_chr(smtptext.s + i,'\n') + 1) && (i < smtptext.len - 8)); 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 = ehlo_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 ehlo_auth() { int i = 0; do { if (case_startb(smtptext.s + i + 4,4,"AUTH")) for (i = 4; i < smtptext.len - 5; ++i) { if (case_startb(smtptext.s + i,4,"CRAM")) if (mailfrom_cram() >= 0) return; if (case_startb(smtptext.s + i,5,"LOGIN")) if (mailfrom_login() >= 0) return; if (case_startb(smtptext.s + i,5,"PLAIN")) if (mailfrom_plain() >= 0) return; } } while ((i += str_chr(smtptext.s + i,'\n') + 1) && (i < smtptext.len - 12)); 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 (ehlo_starttls()) smtp_starttls(); else if (flagtls > 3 && flagtls != 9) { if (!stralloc_0(&host)) temp_nomem(); temp_tlshost(); } } if (user.len && pass.len) /* AUTH */ ehlo_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 > QMTP_MAX) 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(); }