From 68fb25fa3fcff62dda71d2c67894eedf93291e05 Mon Sep 17 00:00:00 2001 From: Steve Holme Date: Fri, 7 Feb 2020 15:53:25 +0000 Subject: [PATCH] smtp: Support UTF-8 based host names in the MAIL command Non-ASCII host names will be ACE encoded if IDN is supported. --- lib/smtp.c | 127 ++++++++++++++++++++++++++++++++++++++-- tests/data/Makefile.inc | 2 +- tests/data/test959 | 3 + tests/data/test962 | 62 ++++++++++++++++++++ 4 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 tests/data/test962 diff --git a/lib/smtp.c b/lib/smtp.c index 7bcff0ce7..18c45afbe 100644 --- a/lib/smtp.c +++ b/lib/smtp.c @@ -27,6 +27,8 @@ * RFC4752 The Kerberos V5 ("GSSAPI") SASL Mechanism * RFC4954 SMTP Authentication * RFC5321 SMTP protocol + * RFC5890 Internationalized Domain Names for Applications (IDNA) + * RFC6531 SMTP Extension for Internationalized Email * RFC6749 OAuth 2.0 Authorization Framework * RFC8314 Use of TLS for Email Submission and Access * Draft SMTP URL Interface @@ -101,6 +103,8 @@ static CURLcode smtp_setup_connection(struct connectdata *conn); static CURLcode smtp_parse_url_options(struct connectdata *conn); static CURLcode smtp_parse_url_path(struct connectdata *conn); static CURLcode smtp_parse_custom_request(struct connectdata *conn); +static CURLcode smtp_parse_address(struct connectdata *conn, const char *fqma, + char **address, struct hostname *host); static CURLcode smtp_perform_auth(struct connectdata *conn, const char *mech, const char *initresp); static CURLcode smtp_continue_auth(struct connectdata *conn, const char *resp); @@ -516,18 +520,58 @@ static CURLcode smtp_perform_mail(struct connectdata *conn) if(!data->set.str[STRING_MAIL_FROM]) /* Null reverse-path, RFC-5321, sect. 3.6.3 */ from = strdup("<>"); - else if(data->set.str[STRING_MAIL_FROM][0] == '<') - from = aprintf("%s", data->set.str[STRING_MAIL_FROM]); - else - from = aprintf("<%s>", data->set.str[STRING_MAIL_FROM]); + else { + char *address = NULL; + struct hostname host = { NULL, NULL, NULL, NULL }; + + /* Parse the FROM mailbox into the local address and host name parts, + converting the host name to an IDN A-label if necessary */ + result = smtp_parse_address(conn, data->set.str[STRING_MAIL_FROM], + &address, &host); + if(result) + return result; + + if(host.name) { + from = aprintf("<%s@%s>", address, host.name); + + Curl_free_idnconverted_hostname(&host); + } + else + /* An invalid mailbox was provided but we'll simply let the server worry + about that and reply with a 501 error */ + from = aprintf("<%s>", address); + + free(address); + } if(!from) return CURLE_OUT_OF_MEMORY; /* Calculate the optional AUTH parameter */ if(data->set.str[STRING_MAIL_AUTH] && conn->proto.smtpc.sasl.authused) { - if(data->set.str[STRING_MAIL_AUTH][0] != '\0') - auth = aprintf("%s", data->set.str[STRING_MAIL_AUTH]); + if(data->set.str[STRING_MAIL_AUTH][0] != '\0') { + char *address = NULL; + struct hostname host = { NULL, NULL, NULL, NULL }; + + /* Parse the AUTH mailbox into the local address and host name parts, + converting the host name to an IDN A-label if necessary */ + result = smtp_parse_address(conn, data->set.str[STRING_MAIL_AUTH], + &address, &host); + if(result) + return result; + + if(host.name) { + from = aprintf("<%s@%s>", address, host.name); + + Curl_free_idnconverted_hostname(&host); + } + else + /* An invalid mailbox was provided but we'll simply let the server + worry about it */ + auth = aprintf("<%s>", address); + + free(address); + } else /* Empty AUTH, RFC-2554, sect. 5 */ auth = strdup("<>"); @@ -561,6 +605,7 @@ static CURLcode smtp_perform_mail(struct connectdata *conn) if(result) { free(from); free(auth); + return result; } @@ -1566,6 +1611,76 @@ static CURLcode smtp_parse_custom_request(struct connectdata *conn) return result; } +/*********************************************************************** + * + * smtp_parse_address() + * + * Parse the fully qualified mailbox address into a local address part and the + * host name, converting the host name to an IDN A-label, as per RFC-5890, if + * necessary. + * + * Parameters: + * + * conn [in] - The connection handle. + * fqma [in] - The fully qualified mailbox address (which may or + * may not contain UTF-8 characters). + * address [in/out] - A new allocated buffer which holds the local + * address part of the mailbox. This buffer must be + * free'ed by the caller. + * host [in/out] - The host name structure that holds the original, + * and optionally encoded, host name. + * Curl_free_idnconverted_hostname() must be called + * once the caller has finished with the structure. + * + * Returns CURLE_OK on success. + * + * Notes: + * + * If an mailbox '@' seperator cannot be located then the mailbox is considered + * to be either a local mailbox or an invalid mailbox (depending on what the + * calling function deems it to be) then the input will simply be returned in + * the address part with the host name being NULL. + */ +static CURLcode smtp_parse_address(struct connectdata *conn, const char *fqma, + char **address, struct hostname *host) +{ + CURLcode result = CURLE_OK; + size_t length; + + /* Duplicate the fully qualified email address so we can manipulate it, + ensuring it doesn't contain the delimiters if specified */ + char *dup = strdup(fqma[0] == '<' ? fqma + 1 : fqma); + if(!dup) + return CURLE_OUT_OF_MEMORY; + + length = strlen(dup); + if(dup[length - 1] == '>') + dup[length - 1] = '\0'; + + /* Extract the host name from the addresss (if we can) */ + host->name = strpbrk(dup, "@"); + if(host->name) { + *host->name = '\0'; + host->name = host->name + 1; + + /* Convert the host name to IDN ACE */ + result = Curl_idnconvert_hostname(conn, host); + if(result) { + free(dup); + host->name = NULL; + + return result; + } + } + else + host->name = NULL; + + /* Extract the local address from the mailbox */ + *address = dup; + + return result; +} + CURLcode Curl_smtp_escape_eob(struct connectdata *conn, const ssize_t nread) { /* When sending a SMTP payload we must detect CRLF. sequences making sure diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index 6cc6beb17..0f0192847 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -108,7 +108,7 @@ test918 test919 test920 test921 test922 test923 test924 test925 test926 \ test927 test928 test929 test930 test931 test932 test933 test934 test935 \ test936 test937 test938 test939 test940 test941 test942 test943 test944 \ test945 test946 test947 test948 test949 test950 test951 test952 test953 \ -test954 test955 test956 test957 test958 test959 test960 test961 \ +test954 test955 test956 test957 test958 test959 test960 test961 test962 \ \ test1000 test1001 test1002 test1003 test1004 test1005 test1006 test1007 \ test1008 test1009 test1010 test1011 test1012 test1013 test1014 test1015 \ diff --git a/tests/data/test959 b/tests/data/test959 index 519aca45a..1de523cb3 100644 --- a/tests/data/test959 +++ b/tests/data/test959 @@ -16,6 +16,9 @@ SMTP smtp + +!idn + LC_ALL=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 diff --git a/tests/data/test962 b/tests/data/test962 new file mode 100644 index 000000000..b6ec405f1 --- /dev/null +++ b/tests/data/test962 @@ -0,0 +1,62 @@ + + + +SMTP +IDN + + + +# +# Server-side + + + +# +# Client-side + + +smtp + + +idn + + +LC_ALL=en_US.UTF-8 +LC_CTYPE=en_US.UTF-8 + + +perl -MI18N::Langinfo=langinfo,CODESET -e 'die "Needs a UTF-8 locale" if (lc(langinfo(CODESET())) ne "utf-8");' + + +SMTP without SMTPUTF8 support - UTF-8 based sender (host part only) + + +From: different +To: another + +body + + +smtp://%HOSTIP:%SMTPPORT/962 --mail-rcpt recipient@example.com --mail-from sender@åäö.se -T - + + + +# +# Verify data after the test has been "shot" + + +EHLO 962 +MAIL FROM: +RCPT TO: +DATA +QUIT + + +From: different +To: another + +body +. + + +