diff options
| author | manuel <manuel@mausz.at> | 2026-06-02 14:09:09 +0200 |
|---|---|---|
| committer | manuel <manuel@mausz.at> | 2026-06-02 14:09:09 +0200 |
| commit | eeea9df85e742f312eafca4f81ffae5494d88ca1 (patch) | |
| tree | b569077b9838249a0b2307b883b305315792c79f /qmail-remote.c | |
| parent | a48b423084b6c31aa8d23997fa31e47a2ec3c513 (diff) | |
| download | qmail-eeea9df85e742f312eafca4f81ffae5494d88ca1.tar.gz qmail-eeea9df85e742f312eafca4f81ffae5494d88ca1.tar.bz2 qmail-eeea9df85e742f312eafca4f81ffae5494d88ca1.zip | |
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.
Diffstat (limited to 'qmail-remote.c')
| -rw-r--r-- | qmail-remote.c | 135 |
1 files 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}; | |||
| 47 | stralloc routes = {0}; | 47 | stralloc routes = {0}; |
| 48 | struct constmap maproutes; | 48 | struct constmap maproutes; |
| 49 | stralloc host = {0}; | 49 | stralloc host = {0}; |
| 50 | stralloc idnhost = {0}; | ||
| 51 | stralloc sender = {0}; | 50 | stralloc sender = {0}; |
| 52 | stralloc auth_smtp_user = {0}; | 51 | stralloc auth_smtp_user = {0}; |
| 53 | stralloc auth_smtp_pass = {0}; | 52 | stralloc auth_smtp_pass = {0}; |
| @@ -74,7 +73,9 @@ char **myargv; | |||
| 74 | 73 | ||
| 75 | #ifdef SMTPUTF8 | 74 | #ifdef SMTPUTF8 |
| 76 | # include <idn2.h> | 75 | # include <idn2.h> |
| 77 | int flagutf8 = 0; | 76 | int withutf8 = 0; /* received header has WITH SMTPUTF8 */ |
| 77 | int utf8message = 0; /* headers contain utf8 */ | ||
| 78 | stralloc idnhost = {0}; | ||
| 78 | stralloc header = {0}; | 79 | stralloc header = {0}; |
| 79 | #endif | 80 | #endif |
| 80 | 81 | ||
| @@ -244,6 +245,15 @@ unsigned long smtpcode() | |||
| 244 | saa ehlokw = {0}; /* list of EHLO keywords and parameters */ | 245 | saa ehlokw = {0}; /* list of EHLO keywords and parameters */ |
| 245 | int maxehlokwlen = 0; | 246 | int maxehlokwlen = 0; |
| 246 | 247 | ||
| 248 | /* look for <capa> among EHLO keywords */ | ||
| 249 | int get_capa(const char *capa) | ||
| 250 | { | ||
| 251 | stralloc *sa = ehlokw.sa; | ||
| 252 | unsigned int len = ehlokw.len; | ||
| 253 | for (; len && case_diffs(sa->s, capa); ++sa, --len); | ||
| 254 | return len > 0; | ||
| 255 | } | ||
| 256 | |||
| 247 | unsigned long ehlo() | 257 | unsigned long ehlo() |
| 248 | { | 258 | { |
| 249 | stralloc *sa; | 259 | stralloc *sa; |
| @@ -473,12 +483,13 @@ static int tls_init(unsigned long code, struct ip_mx *current_mx) | |||
| 473 | tls_required = 1; | 483 | tls_required = 1; |
| 474 | } | 484 | } |
| 475 | 485 | ||
| 476 | if (!smtps) { | ||
| 477 | stralloc *sa = ehlokw.sa; | 486 | stralloc *sa = ehlokw.sa; |
| 478 | unsigned int len = ehlokw.len; | 487 | unsigned int len = ehlokw.len; |
| 479 | /* look for STARTTLS among EHLO keywords */ | ||
| 480 | for ( ; len && case_diffs(sa->s, "STARTTLS"); ++sa, --len) ; | 488 | for ( ; len && case_diffs(sa->s, "STARTTLS"); ++sa, --len) ; |
| 481 | if (!len) { | 489 | /* look for STARTTLS among EHLO keywords */ |
| 490 | |||
| 491 | if (!smtps) { | ||
| 492 | if (!get_capa("STARTTLS")) { | ||
| 482 | if (!tls_required) return 0; | 493 | if (!tls_required) return 0; |
| 483 | out(failure_class); | 494 | out(failure_class); |
| 484 | out("TLS is required, but was not offered by host"); | 495 | 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) | |||
| 682 | } | 693 | } |
| 683 | 694 | ||
| 684 | if (servercert) { | 695 | if (servercert) { |
| 685 | X509 *peercert; | ||
| 686 | STACK_OF(GENERAL_NAME) *gens; | ||
| 687 | |||
| 688 | int r = SSL_get_verify_result(myssl); | 696 | int r = SSL_get_verify_result(myssl); |
| 689 | if (r != X509_V_OK) { | 697 | if (r != X509_V_OK) { |
| 690 | out("ZTLS unable to verify server with "); | 698 | out("ZTLS unable to verify server with "); |
| @@ -724,41 +732,74 @@ int utf8string(unsigned char *ch, int len) | |||
| 724 | return 0; | 732 | return 0; |
| 725 | } | 733 | } |
| 726 | 734 | ||
| 727 | int utf8received() | 735 | void checkutf8message() |
| 728 | { | 736 | { |
| 729 | int r, i, received = 0; | 737 | if (utf8string((unsigned char *)sender.s, sender.len)) { |
| 730 | char ch; | 738 | utf8message = 1; |
| 731 | stralloc receivedline = {0}; | 739 | return; |
| 740 | } | ||
| 741 | for (unsigned int i = 0; i < reciplist.len; ++i) { | ||
| 742 | if (utf8string((unsigned char *)reciplist.sa[i].s, reciplist.sa[i].len)) { | ||
| 743 | utf8message = 1; | ||
| 744 | return; | ||
| 745 | } | ||
| 746 | } | ||
| 732 | 747 | ||
| 733 | for (;;) { /* we consider only our own last written header */ | 748 | int state = 0; |
| 734 | r = substdio_get(&ssin,&ch,1); | 749 | int pos = 0; |
| 750 | for (;;) { | ||
| 751 | char ch; | ||
| 752 | int r = substdio_get(&ssin, &ch, 1); | ||
| 735 | if (r == 0) break; | 753 | if (r == 0) break; |
| 736 | if (r == -1) temp_read(); | 754 | if (r == -1) temp_read(); |
| 737 | 755 | ||
| 738 | if (ch == '\n' && receivedline.len) { | 756 | if (ch == '\n' && !stralloc_cats(&header, "\r")) temp_nomem(); /* received.c does not add '\r' */ |
| 739 | if (!stralloc_append(&header,"\r")) temp_nomem(); /* received.c does not add '\r' */ | 757 | if (!stralloc_append(&header, &ch)) temp_nomem(); |
| 740 | if (!stralloc_append(&header,"\n")) temp_nomem(); | 758 | if (ch == '\r') |
| 741 | if (case_startb(receivedline.s,5,"Date:")) return 0; /* header to quit asap */ | 759 | continue; |
| 742 | if (case_startb(receivedline.s,14,"Received: from")) received++; /* found Received header */ | 760 | if ((unsigned char)ch > 127) |
| 743 | if (received) { | 761 | utf8message = 1; |
| 744 | if (case_startb(receivedline.s,5," by ")) { | 762 | if (utf8message) { |
| 745 | for (i = 6; i < receivedline.len-6; ++i) | 763 | /* no need to look any further */ |
| 746 | if (*(receivedline.s+i) == ' ') | 764 | if (ch == '\n') return; /* end of header */ |
| 747 | if (case_startb(receivedline.s+i+1,9,"with UTF8")) return 1; | 765 | continue; |
| 748 | return 0; | 766 | } |
| 749 | } | 767 | if (ch == '\t') |
| 750 | } | 768 | ch = ' '; |
| 751 | if (!stralloc_copys(&receivedline,"")) temp_nomem(); | 769 | |
| 752 | } else if (ch == '\n' && !receivedline.len) { /* we got an empty newline. probably body start */ | 770 | switch (state) { |
| 753 | if (!stralloc_append(&header,"\r")) temp_nomem(); | 771 | case 6: /* in Received, at LF but before WITH clause */ |
| 754 | if (!stralloc_append(&header,"\n")) temp_nomem(); | 772 | if (ch == ' ') { state = 3; pos = 1; continue; } |
| 755 | return 0; | 773 | state = 0; |
| 756 | } else { | 774 | /* FALL THROUGH */ |
| 757 | if (!stralloc_append(&header,&ch)) temp_nomem(); | 775 | case 0: /* start of header field */ |
| 758 | if (!stralloc_catb(&receivedline,&ch,1)) temp_nomem(); | 776 | if (ch == '\n') return; /* end of headers */ |
| 777 | state = 1; | ||
| 778 | pos = 0; | ||
| 779 | /* FALL THROUGH */ | ||
| 780 | case 1: /* partway through "Received:" */ | ||
| 781 | if (ch != "RECEIVED:"[pos] && ch != "received:"[pos]) { state = 2; continue; } | ||
| 782 | if (++pos == 9) { state = 3; pos = 0; } | ||
| 783 | continue; | ||
| 784 | case 2: /* other header field */ | ||
| 785 | if (ch == '\n') state = 0; | ||
| 786 | continue; | ||
| 787 | case 3: /* in Received, before WITH clause or partway though " with " */ | ||
| 788 | if (ch == '\n') { state = 6; continue; } | ||
| 789 | if (ch != " WITH "[pos] && ch != " with "[pos]) { pos = 0; continue; } | ||
| 790 | if (++pos == 6) { state = 4; pos = 0; } | ||
| 791 | continue; | ||
| 792 | case 4: /* in Received, having seen with, before the argument */ | ||
| 793 | if (pos == 0 && (ch == ' ' || ch == '\t')) continue; | ||
| 794 | if (ch != "UTF8"[pos] && ch != "utf8"[pos]) { state = 5; continue; } | ||
| 795 | if (++pos == 4) { withutf8 = 1; state = 5; continue; } | ||
| 796 | continue; | ||
| 797 | case 5: /* after the RECEIVED WITH argument */ | ||
| 798 | if (ch == '\n') state = 0; | ||
| 799 | /* blast() assumes that it copies whole lines */ | ||
| 800 | continue; | ||
| 759 | } | 801 | } |
| 760 | } | 802 | } |
| 761 | return 0; | ||
| 762 | } | 803 | } |
| 763 | #endif | 804 | #endif |
| 764 | 805 | ||
| @@ -768,7 +809,7 @@ void mail_without_auth() | |||
| 768 | substdio_put(&smtpto,sender.s,sender.len); | 809 | substdio_put(&smtpto,sender.s,sender.len); |
| 769 | substdio_put(&smtpto,">",1); | 810 | substdio_put(&smtpto,">",1); |
| 770 | #ifdef SMTPUTF8 | 811 | #ifdef SMTPUTF8 |
| 771 | if (flagutf8 || utf8received()) | 812 | if (utf8message || withutf8) |
| 772 | substdio_puts(&smtpto, " SMTPUTF8"); | 813 | substdio_puts(&smtpto, " SMTPUTF8"); |
| 773 | #endif | 814 | #endif |
| 774 | substdio_puts(&smtpto,"\r\n"); | 815 | substdio_puts(&smtpto,"\r\n"); |
| @@ -826,6 +867,17 @@ static void smtp(struct ip_mx *current_mx) | |||
| 826 | if (code != 250) quit("ZConnected to "," but my name was rejected"); | 867 | if (code != 250) quit("ZConnected to "," but my name was rejected"); |
| 827 | } | 868 | } |
| 828 | 869 | ||
| 870 | #ifdef SMTPUTF8 | ||
| 871 | checkutf8message(); | ||
| 872 | int can_utf8 = get_capa("SMTPUTF8"); | ||
| 873 | if (utf8message && !can_utf8) { | ||
| 874 | smtptext.len = 0; | ||
| 875 | quit("DConnected to "," but the remote host does not support SMTPUTF8 and the message requires it"); | ||
| 876 | } | ||
| 877 | if (!utf8message && withutf8 && !can_utf8) | ||
| 878 | withutf8 = 0; // downgrade | ||
| 879 | #endif | ||
| 880 | |||
| 829 | int i = 0; | 881 | int i = 0; |
| 830 | if (auth_smtp_user.len && auth_smtp_pass.len) { | 882 | if (auth_smtp_user.len && auth_smtp_pass.len) { |
| 831 | while((i += str_chr(smtptext.s+i,'\n') + 1) && | 883 | while((i += str_chr(smtptext.s+i,'\n') + 1) && |
| @@ -857,7 +909,7 @@ static void smtp(struct ip_mx *current_mx) | |||
| 857 | substdio_put(&smtpto,sender.s,sender.len); | 909 | substdio_put(&smtpto,sender.s,sender.len); |
| 858 | substdio_put(&smtpto,">",1); | 910 | substdio_put(&smtpto,">",1); |
| 859 | #ifdef SMTPUTF8 | 911 | #ifdef SMTPUTF8 |
| 860 | if (flagutf8 || utf8received()) | 912 | if (utf8message || withutf8) |
| 861 | substdio_puts(&smtpto, " SMTPUTF8"); | 913 | substdio_puts(&smtpto, " SMTPUTF8"); |
| 862 | #endif | 914 | #endif |
| 863 | substdio_puts(&smtpto,"\r\n"); | 915 | substdio_puts(&smtpto,"\r\n"); |
| @@ -1032,11 +1084,6 @@ char *s; | |||
| 1032 | { | 1084 | { |
| 1033 | int j; | 1085 | int j; |
| 1034 | 1086 | ||
| 1035 | #ifdef SMTPUTF8 | ||
| 1036 | if (!flagutf8) | ||
| 1037 | flagutf8 = utf8string(s, str_len(s)); | ||
| 1038 | #endif | ||
| 1039 | |||
| 1040 | j = str_rchr(s,'@'); | 1087 | j = str_rchr(s,'@'); |
| 1041 | if (!s[j]) { | 1088 | if (!s[j]) { |
| 1042 | if (!stralloc_copys(saout,s)) temp_nomem(); | 1089 | if (!stralloc_copys(saout,s)) temp_nomem(); |
| @@ -1124,7 +1171,7 @@ char **argv; | |||
| 1124 | char *asciihost = 0; | 1171 | char *asciihost = 0; |
| 1125 | if (!stralloc_0(&host)) temp_nomem(); | 1172 | if (!stralloc_0(&host)) temp_nomem(); |
| 1126 | --host.len; | 1173 | --host.len; |
| 1127 | switch (idn2_lookup_u8(host.s,(uint8_t**)&asciihost,IDN2_NFC_INPUT)) { | 1174 | switch (idn2_lookup_u8((uint8_t*)host.s,(uint8_t**)&asciihost,IDN2_NFC_INPUT)) { |
| 1128 | case IDN2_OK: break; | 1175 | case IDN2_OK: break; |
| 1129 | case IDN2_MALLOC: temp_nomem(); | 1176 | case IDN2_MALLOC: temp_nomem(); |
| 1130 | default: perm_dns(); | 1177 | default: perm_dns(); |
