From 89b7b67a13ebb7965cc7f13ad0595e2194a2d34c Mon Sep 17 00:00:00 2001 From: Jannis Hoffmann Date: Wed, 3 Jul 2024 15:48:04 +0200 Subject: add sqmail-4.2.29a --- src/qmail-remote.c | 1458 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1458 insertions(+) create mode 100644 src/qmail-remote.c (limited to 'src/qmail-remote.c') diff --git a/src/qmail-remote.c b/src/qmail-remote.c new file mode 100644 index 0000000..5ef9465 --- /dev/null +++ b/src/qmail-remote.c @@ -0,0 +1,1458 @@ +#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 "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 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(); +} -- cgit v1.2.3