From eeea9df85e742f312eafca4f81ffae5494d88ca1 Mon Sep 17 00:00:00 2001 From: manuel Date: Tue, 2 Jun 2026 14:09:09 +0200 Subject: qmail-remote: reimplement SMTPUTF8 support make sure to deliver the mail to non-SMTPUTF8 capable remotes unless there are upper byte values found in headers/envelopes. this is more-or-less the same as postfix does. --- qmail-remote.c | 135 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 44 deletions(-) diff --git a/qmail-remote.c b/qmail-remote.c index 7eb2714..d7c8746 100644 --- a/qmail-remote.c +++ b/qmail-remote.c @@ -47,7 +47,6 @@ stralloc helohost = {0}; stralloc routes = {0}; struct constmap maproutes; stralloc host = {0}; -stralloc idnhost = {0}; stralloc sender = {0}; stralloc auth_smtp_user = {0}; stralloc auth_smtp_pass = {0}; @@ -74,7 +73,9 @@ char **myargv; #ifdef SMTPUTF8 # include -int flagutf8 = 0; +int withutf8 = 0; /* received header has WITH SMTPUTF8 */ +int utf8message = 0; /* headers contain utf8 */ +stralloc idnhost = {0}; stralloc header = {0}; #endif @@ -244,6 +245,15 @@ unsigned long smtpcode() saa ehlokw = {0}; /* list of EHLO keywords and parameters */ int maxehlokwlen = 0; +/* look for among EHLO keywords */ +int get_capa(const char *capa) +{ + stralloc *sa = ehlokw.sa; + unsigned int len = ehlokw.len; + for (; len && case_diffs(sa->s, capa); ++sa, --len); + return len > 0; +} + unsigned long ehlo() { stralloc *sa; @@ -473,12 +483,13 @@ static int tls_init(unsigned long code, struct ip_mx *current_mx) tls_required = 1; } - if (!smtps) { stralloc *sa = ehlokw.sa; unsigned int len = ehlokw.len; - /* look for STARTTLS among EHLO keywords */ for ( ; len && case_diffs(sa->s, "STARTTLS"); ++sa, --len) ; - if (!len) { + /* look for STARTTLS among EHLO keywords */ + + if (!smtps) { + if (!get_capa("STARTTLS")) { if (!tls_required) return 0; out(failure_class); out("TLS is required, but was not offered by host"); @@ -682,9 +693,6 @@ static int tls_init(unsigned long code, struct ip_mx *current_mx) } if (servercert) { - X509 *peercert; - STACK_OF(GENERAL_NAME) *gens; - int r = SSL_get_verify_result(myssl); if (r != X509_V_OK) { out("ZTLS unable to verify server with "); @@ -724,41 +732,74 @@ int utf8string(unsigned char *ch, int len) return 0; } -int utf8received() +void checkutf8message() { - int r, i, received = 0; - char ch; - stralloc receivedline = {0}; + if (utf8string((unsigned char *)sender.s, sender.len)) { + utf8message = 1; + return; + } + for (unsigned int i = 0; i < reciplist.len; ++i) { + if (utf8string((unsigned char *)reciplist.sa[i].s, reciplist.sa[i].len)) { + utf8message = 1; + return; + } + } - for (;;) { /* we consider only our own last written header */ - r = substdio_get(&ssin,&ch,1); + int state = 0; + int pos = 0; + for (;;) { + char ch; + int r = substdio_get(&ssin, &ch, 1); if (r == 0) break; if (r == -1) temp_read(); - if (ch == '\n' && receivedline.len) { - if (!stralloc_append(&header,"\r")) temp_nomem(); /* received.c does not add '\r' */ - if (!stralloc_append(&header,"\n")) temp_nomem(); - if (case_startb(receivedline.s,5,"Date:")) return 0; /* header to quit asap */ - if (case_startb(receivedline.s,14,"Received: from")) received++; /* found Received header */ - if (received) { - if (case_startb(receivedline.s,5," by ")) { - for (i = 6; i < receivedline.len-6; ++i) - if (*(receivedline.s+i) == ' ') - if (case_startb(receivedline.s+i+1,9,"with UTF8")) return 1; - return 0; - } - } - if (!stralloc_copys(&receivedline,"")) temp_nomem(); - } else if (ch == '\n' && !receivedline.len) { /* we got an empty newline. probably body start */ - if (!stralloc_append(&header,"\r")) temp_nomem(); - if (!stralloc_append(&header,"\n")) temp_nomem(); - return 0; - } else { - if (!stralloc_append(&header,&ch)) temp_nomem(); - if (!stralloc_catb(&receivedline,&ch,1)) temp_nomem(); + if (ch == '\n' && !stralloc_cats(&header, "\r")) temp_nomem(); /* received.c does not add '\r' */ + if (!stralloc_append(&header, &ch)) temp_nomem(); + if (ch == '\r') + continue; + if ((unsigned char)ch > 127) + utf8message = 1; + if (utf8message) { + /* no need to look any further */ + if (ch == '\n') return; /* end of header */ + continue; + } + if (ch == '\t') + ch = ' '; + + switch (state) { + case 6: /* in Received, at LF but before WITH clause */ + if (ch == ' ') { state = 3; pos = 1; continue; } + state = 0; + /* FALL THROUGH */ + case 0: /* start of header field */ + if (ch == '\n') return; /* end of headers */ + state = 1; + pos = 0; + /* FALL THROUGH */ + case 1: /* partway through "Received:" */ + if (ch != "RECEIVED:"[pos] && ch != "received:"[pos]) { state = 2; continue; } + if (++pos == 9) { state = 3; pos = 0; } + continue; + case 2: /* other header field */ + if (ch == '\n') state = 0; + continue; + case 3: /* in Received, before WITH clause or partway though " with " */ + if (ch == '\n') { state = 6; continue; } + if (ch != " WITH "[pos] && ch != " with "[pos]) { pos = 0; continue; } + if (++pos == 6) { state = 4; pos = 0; } + continue; + case 4: /* in Received, having seen with, before the argument */ + if (pos == 0 && (ch == ' ' || ch == '\t')) continue; + if (ch != "UTF8"[pos] && ch != "utf8"[pos]) { state = 5; continue; } + if (++pos == 4) { withutf8 = 1; state = 5; continue; } + continue; + case 5: /* after the RECEIVED WITH argument */ + if (ch == '\n') state = 0; + /* blast() assumes that it copies whole lines */ + continue; } } - return 0; } #endif @@ -768,7 +809,7 @@ void mail_without_auth() substdio_put(&smtpto,sender.s,sender.len); substdio_put(&smtpto,">",1); #ifdef SMTPUTF8 - if (flagutf8 || utf8received()) + if (utf8message || withutf8) substdio_puts(&smtpto, " SMTPUTF8"); #endif substdio_puts(&smtpto,"\r\n"); @@ -826,6 +867,17 @@ static void smtp(struct ip_mx *current_mx) if (code != 250) quit("ZConnected to "," but my name was rejected"); } +#ifdef SMTPUTF8 + checkutf8message(); + int can_utf8 = get_capa("SMTPUTF8"); + if (utf8message && !can_utf8) { + smtptext.len = 0; + quit("DConnected to "," but the remote host does not support SMTPUTF8 and the message requires it"); + } + if (!utf8message && withutf8 && !can_utf8) + withutf8 = 0; // downgrade +#endif + int i = 0; if (auth_smtp_user.len && auth_smtp_pass.len) { while((i += str_chr(smtptext.s+i,'\n') + 1) && @@ -857,7 +909,7 @@ static void smtp(struct ip_mx *current_mx) substdio_put(&smtpto,sender.s,sender.len); substdio_put(&smtpto,">",1); #ifdef SMTPUTF8 - if (flagutf8 || utf8received()) + if (utf8message || withutf8) substdio_puts(&smtpto, " SMTPUTF8"); #endif substdio_puts(&smtpto,"\r\n"); @@ -1032,11 +1084,6 @@ char *s; { int j; -#ifdef SMTPUTF8 - if (!flagutf8) - flagutf8 = utf8string(s, str_len(s)); -#endif - j = str_rchr(s,'@'); if (!s[j]) { if (!stralloc_copys(saout,s)) temp_nomem(); @@ -1124,7 +1171,7 @@ char **argv; char *asciihost = 0; if (!stralloc_0(&host)) temp_nomem(); --host.len; - switch (idn2_lookup_u8(host.s,(uint8_t**)&asciihost,IDN2_NFC_INPUT)) { + switch (idn2_lookup_u8((uint8_t*)host.s,(uint8_t**)&asciihost,IDN2_NFC_INPUT)) { case IDN2_OK: break; case IDN2_MALLOC: temp_nomem(); default: perm_dns(); -- cgit v1.2.3