From: Jeremy Harris Date: Thu, 30 May 2024 15:20:52 +0000 (+0100) Subject: SMTP WELLKNOWN extension X-Git-Tag: exim-4.98-RC0~8 X-Git-Url: https://git.exim.org/exim.git/commitdiff_plain/e0b3815dc398d7abaa1f81c9f26b1c9b050e94c0 SMTP WELLKNOWN extension --- diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index cea683810..ef0270540 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -11585,6 +11585,19 @@ literal question mark). .cindex "&%utf8_localpart_from_alabel%& expansion item" These convert EAI mail name components between UTF-8 and a-label forms. For information on internationalisation support see &<>&. + + +.new +.vitem &*${xtextd:*&<&'string'&>&*}*& +.cindex "text forcing in strings" +.cindex "string" "xtext decoding" +.cindex "xtext" +.cindex "&%xtextd%& expansion item" +This performs xtext decoding of the string (per RFC 3461 section 4). +.wen + + + .endlist @@ -14836,6 +14849,7 @@ listed in more than one group. .row &%acl_smtp_rcpt%& "ACL for RCPT" .row &%acl_smtp_starttls%& "ACL for STARTTLS" .row &%acl_smtp_vrfy%& "ACL for VRFY" +.row &%acl_smtp_wellknown%& "ACL for WELLKNOWN" .row &%av_scanner%& "specify virus scanner" .row &%check_rfc2047_length%& "check length of RFC 2047 &""encoded &&& words""&" @@ -15002,6 +15016,7 @@ See also the &'Policy controls'& section above. .row &%prdr_enable%& "advertise PRDR to all hosts" .row &%smtputf8_advertise_hosts%& "advertise SMTPUTF8 to these hosts" .row &%tls_advertise_hosts%& "advertise TLS to these hosts" +.row &%wellknown_advertise_hosts%& "advertise WELLKNOWN to these hosts" .endtable @@ -15242,6 +15257,13 @@ received. See chapter &<>& for further details. This option defines the ACL that is run when an SMTP VRFY command is received. See chapter &<>& for further details. +.new +.option acl_smtp_wellknown main string&!! unset +.cindex "WELLKNOWN, ACL for" +This option defines the ACL that is run when an SMTP WELLKNOWN command is +received. See section &<>& for further details. +.wen + .option add_environment main "string list" empty .cindex "environment" "set values" This option adds individual environment variables that the @@ -18913,6 +18935,14 @@ absolute and untainted. See also &%bounce_message_file%&. +.new +.option wellknown_advertise_hosts main boolean unset +.cindex WELLKNOWN advertisement +.cindex "ESMTP extensions" WELLKNOWN +This option enables the advertising of the SMTP WELLKNOWN extension. +See also the &%acl_smtp_wellknown%& ACL (&<>&). +.wen + .option write_rejectlog main boolean true .cindex "reject log" "disabling" If this option is set false, Exim no longer writes anything to the reject log. @@ -30645,6 +30675,7 @@ options in the main part of the configuration. These options are: .cindex "RCPT" "ACL for" .cindex "STARTTLS, ACL for" .cindex "VRFY" "ACL for" +.cindex "WELLKNOWN" "ACL for" .cindex "SMTP" "connection, ACL for" .cindex "non-SMTP messages" "ACLs for" .cindex "MIME content scanning" "ACL for" @@ -30671,6 +30702,7 @@ options in the main part of the configuration. These options are: .irow &%acl_smtp_rcpt%& "ACL for RCPT" .irow &%acl_smtp_starttls%& "ACL for STARTTLS" .irow &%acl_smtp_vrfy%& "ACL for VRFY" +.irow &%acl_smtp_wellknown%& "ACL for WELLKNOWN" .endtable For example, if you set @@ -30853,6 +30885,62 @@ This ACL is evaluated after &%acl_smtp_dkim%& but before &%acl_smtp_data%&. If the ACL is not defined, processing completes as if the feature was not requested by the client. +.new +.subsection "The SMTP WELLKNOWN ACL" SECTWELLKNOWNACL +.cindex "WELLKNOWN" "ACL for" +.oindex "&%acl_smtp_wellknown%&" +The &%acl_smtp_wellknown%& ACL is available only when Exim is compiled +with WELLKNOWN support enabled. + +The ACL determines the response to an SMTP WELLKNOWN command, using the normal +accept/defer/deny verbs for the response code, +and a new &"control=wellknown"& modifier. +This modifier takes a single option, separated by a '/' +character, which must be the name of a file containing the response +cleartext. The modifier is expanded before use in the usual way before +it is used. The configuration is responsible for picking a suitable file +to return and, most importantly, not returning any unexpected file. +The argument for the SMTP verb will be available in the &$smtp_command_argument$& +variable and can be used for building the file path. +If the file path given in the modifier is empty or inacessible, the control will +fail. + +For example: +.code + check_wellknown: + accept control = wellknown/\ + ${lookup {${xtextd:$smtp_command_argument}} \ + dsearch,key=path,filter=file,ret=full \ + {$spooldir/wellknown.d}} +.endd +File content will be encoded in &"xtext"& form, and line-wrapping +for line-length limitation will be done before transmission. +A response summary line will be prepended, with the (pre-encoding) file size. + +The above example uses the expansion operator ${xtextd:} +which is needed to decode the xtext-encoded key from the SMTP verb. + +Under the util directory there is a "mailtest" utility which can be used +to test/retrieve WELLKNOWN items. Syntax is +.code + mailtest -h host.example.com -w security.txt +.endd + +WELLKNOWN is a ESMTP extension providing access to extended +information about the server. It is modelled on the webserver +facilities documented in RFC 8615 and can be used for a security.txt +file and could be used for ACME handshaking (RFC 8555). + +Exim will advertise WELLKNOWN support in the EHLO response +.oindex &%wellknown_advertise_hosts%& +(conditional on a new option &%wellknown_advertise_hosts%&) +and service WELLKNOWN smtp verbs having a single parameter +giving a key for an item of "site-wide metadata". +The verb and key are separated by whitespace, +and the key is xtext-encoded (per RFC 3461 section 4). +.wen + + .subsection "The QUIT ACL" SECTQUITACL .cindex "QUIT, ACL for" The ACL for the SMTP QUIT command is anomalous, in that the outcome of the ACL @@ -31023,12 +31111,15 @@ For &%acl_not_smtp%&, &%acl_smtp_auth%&, &%acl_smtp_connect%&, &%acl_smtp_mime%&, &%acl_smtp_predata%&, and &%acl_smtp_starttls%&, the action when the ACL is not defined is &"accept"&. -For the others (&%acl_smtp_etrn%&, &%acl_smtp_expn%&, &%acl_smtp_rcpt%&, and -&%acl_smtp_vrfy%&), the action when the ACL is not defined is &"deny"&. -This means that &%acl_smtp_rcpt%& must be defined in order to receive any -messages over an SMTP connection. For an example, see the ACL in the default -configuration file. - +For the others (&%acl_smtp_etrn%&, &%acl_smtp_expn%&, &%acl_smtp_rcpt%&, +&%acl_smtp_vrfy%& +.new +and &%acl_smtp_wellknown%&), +.wen +the action when the ACL +is not defined is &"deny"&. This means that &%acl_smtp_rcpt%& must be +defined in order to receive any messages over an SMTP connection. +For an example, see the ACL in the default configuration file. @@ -32133,6 +32224,13 @@ that are being submitted at the same time using &%-bs%& or &%-bS%&. This control enables conversion of UTF-8 in message envelope addresses to a-label form. For details see section &<>&. + +.new +.vitem &*control&~=&~wellknown*& +This control sets up a response data file for a WELLKNOWN SMTP command. +It may only be used in an ACL servicing that command. +For details see section &<>&. +.wen .endlist vlist diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index b0702eea2..3253b90aa 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -24,6 +24,10 @@ Version 4.98 7. The dsearch lookup supports search for a sub-path. + 8. Include mailtest utility for simple connection checking. + + 9. Add SMTP WELLKNOWN extension. + Version 4.97 ------------ diff --git a/doc/doc-txt/experimental-spec.txt b/doc/doc-txt/experimental-spec.txt index aa3a278fd..56ee10f82 100644 --- a/doc/doc-txt/experimental-spec.txt +++ b/doc/doc-txt/experimental-spec.txt @@ -658,6 +658,9 @@ After a success: $proxy_external_address, $proxy_external_port have the proxy "outside" values $sender_host_address, $sender_host_port have the remot client values + + + -------------------------------------------------------------- End of file -------------------------------------------------------------- diff --git a/doc/doc-txt/id-wellknown.txt b/doc/doc-txt/id-wellknown.txt new file mode 100644 index 000000000..14d89cbb1 --- /dev/null +++ b/doc/doc-txt/id-wellknown.txt @@ -0,0 +1,145 @@ +Internet Draft + +Stream: Independent Submission +Category: +Date: 2024/05/26 +Author: J.Harris +Author: B.Quatermass + +-- + + Mailmaint Working Group J. Harris + Internet Draft Independent + Category: Experimental B. Quatermass + Independent + May 2024 + +The WELLKNOWN SMTP Service Extension + +Abstract +-------- + +This document defines a WELLKNOWN extension for the Simple Mail Transfer Protocol +(SMTP). The extension provides the means for an SMTP server to inform a client +of information relating to the server which is intended to be public. + +Status of this Memo +------------------- + +This document is published for examination, experimental implementation, and +evaluation. + +This document defines an Experimental Protocol for the Internet community. + +This is a contribution to the RFC Series, independently of any other RFC +stream. The RFC Editor has chosen to publish this document at its discretion +and makes no statement about its value for implementation or deployment. + +1. Introduction +--------------- + +The Simple Mail Transfer Protocol [SMTP] provides the ability to transfer email +messages from a sending system to a recieving one. + +Senders may on occasion wish to discover additional information, not directly +related to a specific email message, about the receiving system. An example +is a contact point for discussing problems in communications. + +The WELLKNOWN extension provides a means for delivering such information, by an +SMTP server on request from an SMTP client. + +2. The WELLKNOW SMTP Extension +------------------------------ + +The extension mechanism for SMTP is defined in Section 2.2 of the current SMTP +specification [RFC5321a]. + +The name of the extension is WELKNOWN. Servers implementing this extension +advertise a WELLKNOWN as a keyword in the response to EHLO. The keyword has no +parameters. + +A new SMTP verb, "WELLKNOWN" is defined. + +3. The WELLNOWN verb +-------------------- + +The format for the WELLKNOWN verb is: + + WELLKNOWN + +The parameter identifies the specific type of information being +requested. It is separated from the verb by whitespace, and is xtext-encoded +per RFC 3461 Section 4 [RFC3461]. + +After the client gives the WELLKNOWN command, the server responds with one of +the 2xx, 4xx or 5xx response codes. + +A success response MUST be a 250 response code, and MUST be multi-line. + +The first line of a success response will be a response summary; the following +lines are the information data requested, xtext-encoded [RFC3461]. The encoded +information data MAY be split over multiple response lines. + +A response summary MAY be empty. In this case the first line of the response +will be only "250-". + +A response summary MAY contain a size parameter, giving the number of bytes +of data. This parameter is expressed as "SIZE=" followed by a decimal number. +The size value does not include the xtext-encoding overheader, the "250-" or +"250 " response code prefixing each line, nor the CR,LF bytes between lines. + +4. Example +---------- + +S: 220 ESMTP spoken here + +C: EHLO test + +S: 250-Hi there, mate +S: 250-SIZE +S: 250-LIMITS +S: 250-8BITMIME +S: 250-PIPELINING +S: 250-WELLKNOWN +S: 250 HELP + +C: WELLKNOWN security.txt + +S: 250-SIZE=285 +S: 250-Contact:+20mailto:security@example.com+0A +S: 250-+0A +S: 250-Canonical:+20https://www.example.com/.well-known/security.txt+0A +S: 250-Canonical:+20mailserver://mx1.example.com/WELLKNOWN/security.txt+0A +S: 250-Canonical:+20mailserver://mx2.example.com/WELLKNOWN/security.txt+0A +S: 250-+0A +S: 250-Preferred-Languages:+20en+0A +S: 250-+0A +S: 250-Expires:+202025-02-01T00:00:00.000Z+0A +S: 250 +0A + +C: QUIT + +S: 221 + + +5. Use Cases +------------ + +5.1 security.txt +--- +It is common for a website to provide public-access information via the HTTP +protocol. One such item, a "security.txt" file, is descibed in RFC 9116. + +The WELLKNOWN extension provides a method for publishing similar information +for an SMTP host, without the need to operate an HTTP server. + +It is RECOMMENDED that the request-key for this usage be "security.txt". + +5.2 ACME handshake +--- +ACME [RFC8555] provides for obtaining a certificate, needed for encrpted +communications using TLS. It defines handshake methods using the DNS and using +HTTP, for verifying ownership of the domain being certified. + +The WELLKNOWN extension provides a method for operating a similar handshake, +without the need to operate an HTTP server or manipulate the DNS. diff --git a/src/src/EDITME b/src/src/EDITME index 4a33677d5..1440b4b44 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -591,6 +591,9 @@ DISABLE_MAL_MKS=yes # using only native facilities. # SUPPORT_SRS=yes +# Uncomment the following to remove support for the ESMTP extension "WELLKNOWN" +# DISABLE_WELLKNOWN=yes + #------------------------------------------------------------------------------ # Compiling Exim with experimental features. These are documented in diff --git a/src/src/acl.c b/src/src/acl.c index 4e88fc1ac..bfb32df6f 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -57,9 +57,7 @@ static int msgcond[] = { #endif -/* ACL condition and modifier codes - keep in step with the table that -follows. -down. */ +/* ACL condition and modifier codes */ enum { ACLC_ACL, ACLC_ADD_HEADER, @@ -119,7 +117,8 @@ enum { ACLC_ACL, ACLC_SPF_GUESS, #endif ACLC_UDPSEND, - ACLC_VERIFY }; + ACLC_VERIFY, +}; /* ACL conditions/modifiers: "delay", "control", "continue", "endpass", "message", "log_message", "log_reject_target", "logwrite", "queue" and "set" are @@ -149,7 +148,7 @@ static condition_def conditions[] = { [ACLC_ACL] = { US"acl", FALSE, FALSE, 0 }, [ACLC_ADD_HEADER] = { US"add_header", TRUE, TRUE, - (unsigned int) + (unsigned) ~(ACL_BIT_MAIL | ACL_BIT_RCPT | ACL_BIT_PREDATA | ACL_BIT_DATA | #ifndef DISABLE_PRDR @@ -188,7 +187,7 @@ static condition_def conditions[] = { #ifdef EXPERIMENTAL_DCC [ACLC_DCC] = { US"dcc", TRUE, FALSE, - (unsigned int) + (unsigned) ~(ACL_BIT_DATA | # ifndef DISABLE_PRDR ACL_BIT_PRDR | @@ -204,7 +203,7 @@ static condition_def conditions[] = { #ifndef DISABLE_DKIM [ACLC_DKIM_SIGNER] = { US"dkim_signers", TRUE, FALSE, (unsigned int) ~ACL_BIT_DKIM }, [ACLC_DKIM_STATUS] = { US"dkim_status", TRUE, FALSE, - (unsigned int) + (unsigned) ~(ACL_BIT_DKIM | ACL_BIT_DATA | ACL_BIT_MIME # ifndef DISABLE_PRDR | ACL_BIT_PRDR @@ -221,7 +220,7 @@ static condition_def conditions[] = { [ACLC_DNSLISTS] = { US"dnslists", TRUE, FALSE, 0 }, [ACLC_DOMAINS] = { US"domains", FALSE, FALSE, - (unsigned int) + (unsigned) ~(ACL_BIT_RCPT | ACL_BIT_VRFY #ifndef DISABLE_PRDR |ACL_BIT_PRDR @@ -239,7 +238,7 @@ static condition_def conditions[] = { ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START, }, [ACLC_LOCAL_PARTS] = { US"local_parts", FALSE, FALSE, - (unsigned int) + (unsigned) ~(ACL_BIT_RCPT | ACL_BIT_VRFY #ifndef DISABLE_PRDR | ACL_BIT_PRDR @@ -253,7 +252,7 @@ static condition_def conditions[] = { #ifdef WITH_CONTENT_SCAN [ACLC_MALWARE] = { US"malware", TRUE, FALSE, - (unsigned int) + (unsigned) ~(ACL_BIT_DATA | # ifndef DISABLE_PRDR ACL_BIT_PRDR | @@ -280,7 +279,7 @@ static condition_def conditions[] = { #ifdef WITH_CONTENT_SCAN [ACLC_REGEX] = { US"regex", TRUE, FALSE, - (unsigned int) + (unsigned) ~(ACL_BIT_DATA | # ifndef DISABLE_PRDR ACL_BIT_PRDR | @@ -291,7 +290,7 @@ static condition_def conditions[] = { #endif [ACLC_REMOVE_HEADER] = { US"remove_header", TRUE, TRUE, - (unsigned int) + (unsigned) ~(ACL_BIT_MAIL|ACL_BIT_RCPT | ACL_BIT_PREDATA | ACL_BIT_DATA | #ifndef DISABLE_PRDR @@ -320,7 +319,7 @@ static condition_def conditions[] = { #ifdef WITH_CONTENT_SCAN [ACLC_SPAM] = { US"spam", TRUE, FALSE, - (unsigned int) ~(ACL_BIT_DATA | + (unsigned) ~(ACL_BIT_DATA | # ifndef DISABLE_PRDR ACL_BIT_PRDR | # endif @@ -370,8 +369,7 @@ for (condition_def * c = conditions; c < conditions + nelem(conditions); c++) #ifndef MACRO_PREDEF -/* Return values from decode_control(); used as index so keep in step -with the controls_list table that follows! */ +/* Return values from decode_control() */ enum { CONTROL_AUTH_UNADVERTISED, @@ -411,6 +409,9 @@ enum { #ifdef SUPPORT_I18N CONTROL_UTF8_DOWNCONVERT, #endif +#ifndef DISABLE_WELLKNOWN + CONTROL_WELLKNOWN, +#endif }; @@ -564,7 +565,12 @@ static control_def controls_list[] = { #ifdef SUPPORT_I18N [CONTROL_UTF8_DOWNCONVERT] = { US"utf8_downconvert", TRUE, (unsigned) ~(ACL_BIT_RCPT | ACL_BIT_VRFY) - } + }, +#endif +#ifndef DISABLE_WELLKNOWN +[CONTROL_WELLKNOWN] = + { US"wellknown", TRUE, (unsigned) ~ACL_BIT_WELLKNOWN + }, #endif }; @@ -806,7 +812,7 @@ if (*s++ != '=') { *error = string_sprintf("\"=\" missing after ACL \"%s\" %s", name, conditions[cond->type].is_modifier ? US"modifier" : US"condition"); - return FALSE;; + return FALSE; } Uskip_whitespace(&s); cond->arg = taint ? string_copy_taint(s, GET_TAINTED) : string_copy(s); @@ -3122,6 +3128,80 @@ return DEFER; +#ifndef DISABLE_WELLKNOWN +/************************************************* +* The "wellknown" ACL modifier * +*************************************************/ + +/* Called by acl_check_condition() below. + +Retrieve the given file and encode content as xtext. +Prefix with a summary line giving the length of plaintext. +Leave a global pointer to the whole, for output by +the smtp verb handler code (smtp_in.c). + +Arguments: + arg the option string for wellknown= + log_msgptr for error messages + +Returns: OK/FAIL +*/ + +static int +wellknown_process(const uschar * arg, uschar ** log_msgptr) +{ +struct stat statbuf; +FILE * rf; +gstring * g; + +wellknown_response = NULL; +if (f.no_multiline_responses) return FAIL; + +/* Check for file existence */ + +if (!*arg) return FAIL; +if (Ustat(arg, &statbuf) != 0) + { *log_msgptr = US"stat"; goto fail; } + +/*XXX perhaps refuse to serve a group- or world-writeable file? */ + +if (!(rf = Ufopen(arg, "r"))) + { *log_msgptr = US"open"; goto fail; } + +/* Set up summary line for output */ + +g = string_fmt_append(NULL, "SIZE=%lu\n", (long) statbuf.st_size); + +#define LINE_LIM 75 +for (int n = 0, ch; (ch = fgetc(rf)) != EOF; ) + { + /* Xtext-encode, adding output linebreaks for input linebreaks + or when the line gets long enough */ + + if (ch == '\n') + { g = string_fmt_append(g, "+%02X", ch); n = LINE_LIM; } + else if (ch < 33 || ch > 126 || ch == '+' || ch == '=') + { g = string_fmt_append(g, "+%02X", ch); n += 3; } + else + { g = string_fmt_append(g, "%c", ch); n++; } + + if (n >= LINE_LIM) + { g = string_catn(g, US"\n", 1); n = 0; } + } +#undef LINE_LIM + +gstring_release_unused(g); +wellknown_response = string_from_gstring(g); +return OK; + +fail: + *log_msgptr = string_sprintf("wellknown: failed to %s file \"%s\": %s", + *log_msgptr, arg, strerror(errno)); + return FAIL; +} +#endif + + /************************************************* * Handle conditions/modifiers on an ACL item * *************************************************/ @@ -3311,7 +3391,7 @@ for (; cb; cb = cb->next) case ACLC_CONTROL: { - const uschar *p = NULL; + const uschar * p = NULL; control_type = decode_control(arg, &p, where, log_msgptr); /* Check if this control makes sense at this time */ @@ -3323,6 +3403,7 @@ for (; cb; cb = cb->next) return ERROR; } + /*XXX ought to sort these, just for sanity */ switch(control_type) { case CONTROL_AUTH_UNADVERTISED: @@ -3668,8 +3749,13 @@ for (; cb; cb = cb->next) break; } return ERROR; -#endif +#endif /*I18N*/ +#ifndef DISABLE_WELLKNOWN + case CONTROL_WELLKNOWN: + rc = *p == '/' ? wellknown_process(p+1, log_msgptr) : FAIL; + break; +#endif } break; } diff --git a/src/src/auths/xtextencode.c b/src/src/auths/xtextencode.c index 75be18161..00f36e544 100644 --- a/src/src/auths/xtextencode.c +++ b/src/src/auths/xtextencode.c @@ -40,3 +40,5 @@ return string_from_gstring(g); /* End of xtextencode.c */ +/* vi: aw ai sw=2 +*/ diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index c08f1874d..22749b174 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -59,6 +59,7 @@ Do not put spaces between # and the 'define'. #define DISABLE_QUEUE_RAMP #define DISABLE_TLS #define DISABLE_TLS_RESUME +#define DISABLE_WELLKNOWN #define ENABLE_DISABLE_FSYNC diff --git a/src/src/exim.c b/src/src/exim.c index 040df2cd0..b78839b57 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -1108,6 +1108,9 @@ g = string_cat(g, US"Support for:"); #ifndef DISABLE_ESMTP_LIMITS g = string_cat(g, US" ESMTP_Limits"); #endif +#ifndef DISABLE_WELLKNOWN + g = string_cat(g, US" ESMTP_Wellknown"); +#endif #ifndef DISABLE_EVENT g = string_cat(g, US" Event"); #endif diff --git a/src/src/expand.c b/src/src/expand.c index 1d121756d..4bc680544 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -259,7 +259,9 @@ static uschar *op_table_main[] = { US"strlen", US"substr", US"uc", - US"utf8clean" }; + US"utf8clean", + US"xtextd", + }; enum { EOP_ADDRESS = nelem(op_table_underscore), @@ -307,7 +309,9 @@ enum { EOP_STRLEN, EOP_SUBSTR, EOP_UC, - EOP_UTF8CLEAN }; + EOP_UTF8CLEAN, + EOP_XTEXTD, + }; /* Table of condition names, and corresponding switch numbers. The names must @@ -7326,19 +7330,20 @@ NOT_ITEM: ; case EOP_LC: { - int count = 0; - uschar *t = sub - 1; - while (*(++t) != 0) { *t = tolower(*t); count++; } - yield = string_catn(yield, sub, count); + uschar * t = sub - 1; + while (*++t) *t = tolower(*t); + yield = string_catn(yield, sub, t-sub); break; } + { + uschar * s = sub; + } case EOP_UC: { - int count = 0; - uschar *t = sub - 1; - while (*(++t) != 0) { *t = toupper(*t); count++; } - yield = string_catn(yield, sub, count); + uschar * t = sub - 1; + while (*++t) *t = toupper(*t); + yield = string_catn(yield, sub, t-sub); break; } @@ -7774,7 +7779,6 @@ NOT_ITEM: ; } else yield = string_cat(yield, sub); - break; } /* quote_lookuptype does lookup-specific quoting */ @@ -7806,526 +7810,533 @@ NOT_ITEM: ; } yield = string_cat(yield, sub); - break; } + break; - /* rx quote sticks in \ before any non-alphameric character so that - the insertion works in a regular expression. */ + /* rx quote sticks in \ before any non-alphameric character so that + the insertion works in a regular expression. */ - case EOP_RXQUOTE: + case EOP_RXQUOTE: + { + uschar *t = sub - 1; + while (*(++t) != 0) { - uschar *t = sub - 1; - while (*(++t) != 0) - { - if (!isalnum(*t)) - yield = string_catn(yield, US"\\", 1); - yield = string_catn(yield, t, 1); - } - break; + if (!isalnum(*t)) + yield = string_catn(yield, US"\\", 1); + yield = string_catn(yield, t, 1); } + break; + } - /* RFC 2047 encodes, assuming headers_charset (default ISO 8859-1) as - prescribed by the RFC, if there are characters that need to be encoded */ + /* RFC 2047 encodes, assuming headers_charset (default ISO 8859-1) as + prescribed by the RFC, if there are characters that need to be encoded */ - case EOP_RFC2047: - yield = string_cat(yield, - parse_quote_2047(sub, Ustrlen(sub), headers_charset, - FALSE)); - break; + case EOP_RFC2047: + yield = string_cat(yield, + parse_quote_2047(sub, Ustrlen(sub), headers_charset, + FALSE)); + break; - /* RFC 2047 decode */ + /* RFC 2047 decode */ - case EOP_RFC2047D: + case EOP_RFC2047D: + { + int len; + uschar *error; + uschar *decoded = rfc2047_decode(sub, check_rfc2047_length, + headers_charset, '?', &len, &error); + if (error) { - int len; - uschar *error; - uschar *decoded = rfc2047_decode(sub, check_rfc2047_length, - headers_charset, '?', &len, &error); - if (error) - { - expand_string_message = error; - goto EXPAND_FAILED; - } - yield = string_catn(yield, decoded, len); - break; + expand_string_message = error; + goto EXPAND_FAILED; } + yield = string_catn(yield, decoded, len); + break; + } - /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into - underscores */ + /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into + underscores */ - case EOP_FROM_UTF8: + case EOP_FROM_UTF8: + { + uschar * buff = store_get(4, sub); + while (*sub) { - uschar * buff = store_get(4, sub); - while (*sub) - { - int c; - GETUTF8INC(c, sub); - if (c > 255) c = '_'; - buff[0] = c; - yield = string_catn(yield, buff, 1); - } - break; + int c; + GETUTF8INC(c, sub); + if (c > 255) c = '_'; + buff[0] = c; + yield = string_catn(yield, buff, 1); } + break; + } - /* replace illegal UTF-8 sequences by replacement character */ + /* replace illegal UTF-8 sequences by replacement character */ - #define UTF8_REPLACEMENT_CHAR US"?" + #define UTF8_REPLACEMENT_CHAR US"?" + + case EOP_UTF8CLEAN: + { + int seq_len = 0, index = 0, bytes_left = 0, complete; + u_long codepoint = (u_long)-1; + uschar seq_buff[4]; /* accumulate utf-8 here */ - case EOP_UTF8CLEAN: + /* Manually track tainting, as we deal in individual chars below */ + + if (!yield) + yield = string_get_tainted(Ustrlen(sub), sub); + else if (!yield->s || !yield->ptr) { - int seq_len = 0, index = 0, bytes_left = 0, complete; - u_long codepoint = (u_long)-1; - uschar seq_buff[4]; /* accumulate utf-8 here */ + yield->s = store_get(yield->size = Ustrlen(sub), sub); + gstring_reset(yield); + } + else if (is_incompatible(yield->s, sub)) + gstring_rebuffer(yield, sub); - /* Manually track tainting, as we deal in individual chars below */ + /* Check the UTF-8, byte-by-byte */ + + while (*sub) + { + complete = 0; + uschar c = *sub++; - if (!yield) - yield = string_get_tainted(Ustrlen(sub), sub); - else if (!yield->s || !yield->ptr) + if (bytes_left) { - yield->s = store_get(yield->size = Ustrlen(sub), sub); - gstring_reset(yield); + if ((c & 0xc0) != 0x80) + /* wrong continuation byte; invalidate all bytes */ + complete = 1; /* error */ + else + { + codepoint = (codepoint << 6) | (c & 0x3f); + seq_buff[index++] = c; + if (--bytes_left == 0) /* codepoint complete */ + if(codepoint > 0x10FFFF) /* is it too large? */ + complete = -1; /* error (RFC3629 limit) */ + else if ( (codepoint & 0x1FF800 ) == 0xD800 ) /* surrogate */ + /* A UTF-16 surrogate (which should be one of a pair that + encode a Unicode codepoint that is outside the Basic + Multilingual Plane). Error, not UTF8. + RFC2279.2 is slightly unclear on this, but + https://unicodebook.readthedocs.io/issues.html#strict-utf8-decoder + says "Surrogates characters are also invalid in UTF-8: + characters in U+D800—U+DFFF have to be rejected." */ + complete = -1; + else + { /* finished; output utf-8 sequence */ + yield = string_catn(yield, seq_buff, seq_len); + index = 0; + } + } } - else if (is_incompatible(yield->s, sub)) - gstring_rebuffer(yield, sub); - - /* Check the UTF-8, byte-by-byte */ - - while (*sub) + else /* no bytes left: new sequence */ { - complete = 0; - uschar c = *sub++; - - if (bytes_left) + if (!(c & 0x80)) /* 1-byte sequence, US-ASCII, keep it */ { - if ((c & 0xc0) != 0x80) - /* wrong continuation byte; invalidate all bytes */ - complete = 1; /* error */ + yield = string_catn(yield, &c, 1); + continue; + } + if ((c & 0xe0) == 0xc0) /* 2-byte sequence */ + if (c == 0xc0 || c == 0xc1) /* 0xc0 and 0xc1 are illegal */ + complete = -1; else { - codepoint = (codepoint << 6) | (c & 0x3f); - seq_buff[index++] = c; - if (--bytes_left == 0) /* codepoint complete */ - if(codepoint > 0x10FFFF) /* is it too large? */ - complete = -1; /* error (RFC3629 limit) */ - else if ( (codepoint & 0x1FF800 ) == 0xD800 ) /* surrogate */ - /* A UTF-16 surrogate (which should be one of a pair that - encode a Unicode codepoint that is outside the Basic - Multilingual Plane). Error, not UTF8. - RFC2279.2 is slightly unclear on this, but - https://unicodebook.readthedocs.io/issues.html#strict-utf8-decoder - says "Surrogates characters are also invalid in UTF-8: - characters in U+D800—U+DFFF have to be rejected." */ - complete = -1; - else - { /* finished; output utf-8 sequence */ - yield = string_catn(yield, seq_buff, seq_len); - index = 0; - } + bytes_left = 1; + codepoint = c & 0x1f; } + else if ((c & 0xf0) == 0xe0) /* 3-byte sequence */ + { + bytes_left = 2; + codepoint = c & 0x0f; } - else /* no bytes left: new sequence */ + else if ((c & 0xf8) == 0xf0) /* 4-byte sequence */ { - if (!(c & 0x80)) /* 1-byte sequence, US-ASCII, keep it */ - { - yield = string_catn(yield, &c, 1); - continue; - } - if ((c & 0xe0) == 0xc0) /* 2-byte sequence */ - if (c == 0xc0 || c == 0xc1) /* 0xc0 and 0xc1 are illegal */ - complete = -1; - else - { - bytes_left = 1; - codepoint = c & 0x1f; - } - else if ((c & 0xf0) == 0xe0) /* 3-byte sequence */ - { - bytes_left = 2; - codepoint = c & 0x0f; - } - else if ((c & 0xf8) == 0xf0) /* 4-byte sequence */ - { - bytes_left = 3; - codepoint = c & 0x07; - } - else /* invalid or too long (RFC3629 allows only 4 bytes) */ - complete = -1; + bytes_left = 3; + codepoint = c & 0x07; + } + else /* invalid or too long (RFC3629 allows only 4 bytes) */ + complete = -1; - seq_buff[index++] = c; - seq_len = bytes_left + 1; - } /* if(bytes_left) */ + seq_buff[index++] = c; + seq_len = bytes_left + 1; + } /* if(bytes_left) */ - if (complete != 0) - { - bytes_left = index = 0; - yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1); - } - if ((complete == 1) && ((c & 0x80) == 0)) - /* ASCII character follows incomplete sequence */ - yield = string_catn(yield, &c, 1); + if (complete != 0) + { + bytes_left = index = 0; + yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1); } - /* If given a sequence truncated mid-character, we also want to report ? - Eg, ${length_1:フィル} is one byte, not one character, so we expect - ${utf8clean:${length_1:フィル}} to yield '?' */ + if ((complete == 1) && ((c & 0x80) == 0)) + /* ASCII character follows incomplete sequence */ + yield = string_catn(yield, &c, 1); + } + /* If given a sequence truncated mid-character, we also want to report ? + Eg, ${length_1:フィル} is one byte, not one character, so we expect + ${utf8clean:${length_1:フィル}} to yield '?' */ - if (bytes_left != 0) - yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1); + if (bytes_left != 0) + yield = string_catn(yield, UTF8_REPLACEMENT_CHAR, 1); - break; - } + break; + } #ifdef SUPPORT_I18N - case EOP_UTF8_DOMAIN_TO_ALABEL: + case EOP_UTF8_DOMAIN_TO_ALABEL: + { + uschar * error = NULL; + uschar * s = string_domain_utf8_to_alabel(sub, &error); + if (error) { - uschar * error = NULL; - uschar * s = string_domain_utf8_to_alabel(sub, &error); - if (error) - { - expand_string_message = string_sprintf( - "error converting utf8 (%s) to alabel: %s", - string_printing(sub), error); - goto EXPAND_FAILED; - } - yield = string_cat(yield, s); - break; + expand_string_message = string_sprintf( + "error converting utf8 (%s) to alabel: %s", + string_printing(sub), error); + goto EXPAND_FAILED; } + yield = string_cat(yield, s); + break; + } - case EOP_UTF8_DOMAIN_FROM_ALABEL: + case EOP_UTF8_DOMAIN_FROM_ALABEL: + { + uschar * error = NULL; + uschar * s = string_domain_alabel_to_utf8(sub, &error); + if (error) { - uschar * error = NULL; - uschar * s = string_domain_alabel_to_utf8(sub, &error); - if (error) - { - expand_string_message = string_sprintf( - "error converting alabel (%s) to utf8: %s", - string_printing(sub), error); - goto EXPAND_FAILED; - } - yield = string_cat(yield, s); - break; + expand_string_message = string_sprintf( + "error converting alabel (%s) to utf8: %s", + string_printing(sub), error); + goto EXPAND_FAILED; } + yield = string_cat(yield, s); + break; + } - case EOP_UTF8_LOCALPART_TO_ALABEL: + case EOP_UTF8_LOCALPART_TO_ALABEL: + { + uschar * error = NULL; + uschar * s = string_localpart_utf8_to_alabel(sub, &error); + if (error) { - uschar * error = NULL; - uschar * s = string_localpart_utf8_to_alabel(sub, &error); - if (error) - { - expand_string_message = string_sprintf( - "error converting utf8 (%s) to alabel: %s", - string_printing(sub), error); - goto EXPAND_FAILED; - } - yield = string_cat(yield, s); - DEBUG(D_expand) debug_printf_indent("yield: '%Y'\n", yield); - break; + expand_string_message = string_sprintf( + "error converting utf8 (%s) to alabel: %s", + string_printing(sub), error); + goto EXPAND_FAILED; } + yield = string_cat(yield, s); + DEBUG(D_expand) debug_printf_indent("yield: '%Y'\n", yield); + break; + } - case EOP_UTF8_LOCALPART_FROM_ALABEL: + case EOP_UTF8_LOCALPART_FROM_ALABEL: + { + uschar * error = NULL; + uschar * s = string_localpart_alabel_to_utf8(sub, &error); + if (error) { - uschar * error = NULL; - uschar * s = string_localpart_alabel_to_utf8(sub, &error); - if (error) - { - expand_string_message = string_sprintf( - "error converting alabel (%s) to utf8: %s", - string_printing(sub), error); - goto EXPAND_FAILED; - } - yield = string_cat(yield, s); - break; + expand_string_message = string_sprintf( + "error converting alabel (%s) to utf8: %s", + string_printing(sub), error); + goto EXPAND_FAILED; } + yield = string_cat(yield, s); + break; + } #endif /* EXPERIMENTAL_INTERNATIONAL */ - /* escape turns all non-printing characters into escape sequences. */ + /* escape turns all non-printing characters into escape sequences. */ - case EOP_ESCAPE: - { - const uschar * t = string_printing(sub); - yield = string_cat(yield, t); - break; - } + case EOP_ESCAPE: + { + const uschar * t = string_printing(sub); + yield = string_cat(yield, t); + break; + } - case EOP_ESCAPE8BIT: - { - uschar c; + case EOP_ESCAPE8BIT: + { + uschar c; - for (const uschar * s = sub; (c = *s); s++) - yield = c < 127 && c != '\\' - ? string_catn(yield, s, 1) - : string_fmt_append(yield, "\\%03o", c); - break; - } + for (const uschar * s = sub; (c = *s); s++) + yield = c < 127 && c != '\\' + ? string_catn(yield, s, 1) + : string_fmt_append(yield, "\\%03o", c); + break; + } - /* Handle numeric expression evaluation */ + /* Handle numeric expression evaluation */ - case EOP_EVAL: - case EOP_EVAL10: + case EOP_EVAL: + case EOP_EVAL10: + { + uschar *save_sub = sub; + uschar *error = NULL; + int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE); + if (error) { - uschar *save_sub = sub; - uschar *error = NULL; - int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE); - if (error) - { - expand_string_message = string_sprintf("error in expression " - "evaluation: %s (after processing \"%.*s\")", error, - (int)(sub-save_sub), save_sub); - goto EXPAND_FAILED; - } - yield = string_fmt_append(yield, PR_EXIM_ARITH, n); - break; + expand_string_message = string_sprintf("error in expression " + "evaluation: %s (after processing \"%.*s\")", error, + (int)(sub-save_sub), save_sub); + goto EXPAND_FAILED; } + yield = string_fmt_append(yield, PR_EXIM_ARITH, n); + break; + } - /* Handle time period formatting */ + /* Handle time period formatting */ - case EOP_TIME_EVAL: + case EOP_TIME_EVAL: + { + int n = readconf_readtime(sub, 0, FALSE); + if (n < 0) { - int n = readconf_readtime(sub, 0, FALSE); - if (n < 0) - { - expand_string_message = string_sprintf("string \"%s\" is not an " - "Exim time interval in \"%s\" operator", sub, name); - goto EXPAND_FAILED; - } - yield = string_fmt_append(yield, "%d", n); - break; + expand_string_message = string_sprintf("string \"%s\" is not an " + "Exim time interval in \"%s\" operator", sub, name); + goto EXPAND_FAILED; } + yield = string_fmt_append(yield, "%d", n); + break; + } - case EOP_TIME_INTERVAL: + case EOP_TIME_INTERVAL: + { + int n; + uschar *t = read_number(&n, sub); + if (*t != 0) /* Not A Number*/ { - int n; - uschar *t = read_number(&n, sub); - if (*t != 0) /* Not A Number*/ - { - expand_string_message = string_sprintf("string \"%s\" is not a " - "positive number in \"%s\" operator", sub, name); - goto EXPAND_FAILED; - } - t = readconf_printtime(n); - yield = string_cat(yield, t); - break; + expand_string_message = string_sprintf("string \"%s\" is not a " + "positive number in \"%s\" operator", sub, name); + goto EXPAND_FAILED; } + t = readconf_printtime(n); + yield = string_cat(yield, t); + break; + } - /* Convert string to base64 encoding */ + /* Convert string to base64 encoding */ - case EOP_STR2B64: - case EOP_BASE64: - { + case EOP_STR2B64: + case EOP_BASE64: + { #ifndef DISABLE_TLS - uschar * s = vp && *(void **)vp->value - ? tls_cert_der_b64(*(void **)vp->value) - : b64encode(CUS sub, Ustrlen(sub)); + uschar * s = vp && *(void **)vp->value + ? tls_cert_der_b64(*(void **)vp->value) + : b64encode(CUS sub, Ustrlen(sub)); #else - uschar * s = b64encode(CUS sub, Ustrlen(sub)); + uschar * s = b64encode(CUS sub, Ustrlen(sub)); #endif - yield = string_cat(yield, s); - break; - } + yield = string_cat(yield, s); + break; + } - case EOP_BASE64D: + case EOP_BASE64D: + { + uschar * s; + int len = b64decode(sub, &s, sub); + if (len < 0) { - uschar * s; - int len = b64decode(sub, &s, sub); - if (len < 0) - { - expand_string_message = string_sprintf("string \"%s\" is not " - "well-formed for \"%s\" operator", sub, name); - goto EXPAND_FAILED; - } - yield = string_cat(yield, s); - break; + expand_string_message = string_sprintf("string \"%s\" is not " + "well-formed for \"%s\" operator", sub, name); + goto EXPAND_FAILED; } + yield = string_cat(yield, s); + break; + } - /* strlen returns the length of the string */ + /* strlen returns the length of the string */ - case EOP_STRLEN: - yield = string_fmt_append(yield, "%d", Ustrlen(sub)); - break; + case EOP_STRLEN: + yield = string_fmt_append(yield, "%d", Ustrlen(sub)); + break; + + /* length_n or l_n takes just the first n characters or the whole string, + whichever is the shorter; + + substr_m_n, and s_m_n take n characters from offset m; negative m take + from the end; l_n is synonymous with s_0_n. If n is omitted in substr it + takes the rest, either to the right or to the left. + + hash_n or h_n makes a hash of length n from the string, yielding n + characters from the set a-z; hash_n_m makes a hash of length n, but + uses m characters from the set a-zA-Z0-9. + + nhash_n returns a single number between 0 and n-1 (in text form), while + nhash_n_m returns a div/mod hash as two numbers "a/b". The first lies + between 0 and n-1 and the second between 0 and m-1. */ + + case EOP_LENGTH: + case EOP_L: + case EOP_SUBSTR: + case EOP_S: + case EOP_HASH: + case EOP_H: + case EOP_NHASH: + case EOP_NH: + { + int sign = 1; + int value1 = 0; + int value2 = -1; + int *pn; + int len; + uschar *ret; - /* length_n or l_n takes just the first n characters or the whole string, - whichever is the shorter; - - substr_m_n, and s_m_n take n characters from offset m; negative m take - from the end; l_n is synonymous with s_0_n. If n is omitted in substr it - takes the rest, either to the right or to the left. - - hash_n or h_n makes a hash of length n from the string, yielding n - characters from the set a-z; hash_n_m makes a hash of length n, but - uses m characters from the set a-zA-Z0-9. - - nhash_n returns a single number between 0 and n-1 (in text form), while - nhash_n_m returns a div/mod hash as two numbers "a/b". The first lies - between 0 and n-1 and the second between 0 and m-1. */ - - case EOP_LENGTH: - case EOP_L: - case EOP_SUBSTR: - case EOP_S: - case EOP_HASH: - case EOP_H: - case EOP_NHASH: - case EOP_NH: + if (!arg) { - int sign = 1; - int value1 = 0; - int value2 = -1; - int *pn; - int len; - uschar *ret; + expand_string_message = string_sprintf("missing values after %s", + name); + goto EXPAND_FAILED; + } - if (!arg) - { - expand_string_message = string_sprintf("missing values after %s", - name); - goto EXPAND_FAILED; - } + /* "length" has only one argument, effectively being synonymous with + substr_0_n. */ - /* "length" has only one argument, effectively being synonymous with - substr_0_n. */ + if (c == EOP_LENGTH || c == EOP_L) + { + pn = &value2; + value2 = 0; + } - if (c == EOP_LENGTH || c == EOP_L) + /* The others have one or two arguments; for "substr" the first may be + negative. The second being negative means "not supplied". */ + + else + { + pn = &value1; + if (name[0] == 's' && *arg == '-') { sign = -1; arg++; } + } + + /* Read up to two numbers, separated by underscores */ + + ret = arg; + while (*arg != 0) + { + if (arg != ret && *arg == '_' && pn == &value1) { pn = &value2; value2 = 0; + if (arg[1] != 0) arg++; } - - /* The others have one or two arguments; for "substr" the first may be - negative. The second being negative means "not supplied". */ - - else + else if (!isdigit(*arg)) { - pn = &value1; - if (name[0] == 's' && *arg == '-') { sign = -1; arg++; } + expand_string_message = + string_sprintf("non-digit after underscore in \"%s\"", name); + goto EXPAND_FAILED; } + else *pn = (*pn)*10 + *arg++ - '0'; + } + value1 *= sign; - /* Read up to two numbers, separated by underscores */ - - ret = arg; - while (*arg != 0) - { - if (arg != ret && *arg == '_' && pn == &value1) - { - pn = &value2; - value2 = 0; - if (arg[1] != 0) arg++; - } - else if (!isdigit(*arg)) - { - expand_string_message = - string_sprintf("non-digit after underscore in \"%s\"", name); - goto EXPAND_FAILED; - } - else *pn = (*pn)*10 + *arg++ - '0'; - } - value1 *= sign; + /* Perform the required operation */ - /* Perform the required operation */ + ret = c == EOP_HASH || c == EOP_H + ? compute_hash(sub, value1, value2, &len) + : c == EOP_NHASH || c == EOP_NH + ? compute_nhash(sub, value1, value2, &len) + : extract_substr(sub, value1, value2, &len); + if (!ret) goto EXPAND_FAILED; - ret = c == EOP_HASH || c == EOP_H - ? compute_hash(sub, value1, value2, &len) - : c == EOP_NHASH || c == EOP_NH - ? compute_nhash(sub, value1, value2, &len) - : extract_substr(sub, value1, value2, &len); - if (!ret) goto EXPAND_FAILED; + yield = string_catn(yield, ret, len); + break; + } - yield = string_catn(yield, ret, len); - break; - } + /* Stat a path */ - /* Stat a path */ + case EOP_STAT: + { + uschar smode[12]; + uschar **modetable[3]; + mode_t mode; + struct stat st; - case EOP_STAT: + if (expand_forbid & RDO_EXISTS) { - uschar smode[12]; - uschar **modetable[3]; - mode_t mode; - struct stat st; + expand_string_message = US"Use of the stat() expansion is not permitted"; + goto EXPAND_FAILED; + } - if (expand_forbid & RDO_EXISTS) - { - expand_string_message = US"Use of the stat() expansion is not permitted"; - goto EXPAND_FAILED; - } + if (stat(CS sub, &st) < 0) + { + expand_string_message = string_sprintf("stat(%s) failed: %s", + sub, strerror(errno)); + goto EXPAND_FAILED; + } + mode = st.st_mode; + switch (mode & S_IFMT) + { + case S_IFIFO: smode[0] = 'p'; break; + case S_IFCHR: smode[0] = 'c'; break; + case S_IFDIR: smode[0] = 'd'; break; + case S_IFBLK: smode[0] = 'b'; break; + case S_IFREG: smode[0] = '-'; break; + default: smode[0] = '?'; break; + } - if (stat(CS sub, &st) < 0) - { - expand_string_message = string_sprintf("stat(%s) failed: %s", - sub, strerror(errno)); - goto EXPAND_FAILED; - } - mode = st.st_mode; - switch (mode & S_IFMT) - { - case S_IFIFO: smode[0] = 'p'; break; - case S_IFCHR: smode[0] = 'c'; break; - case S_IFDIR: smode[0] = 'd'; break; - case S_IFBLK: smode[0] = 'b'; break; - case S_IFREG: smode[0] = '-'; break; - default: smode[0] = '?'; break; - } + modetable[0] = ((mode & 01000) == 0)? mtable_normal : mtable_sticky; + modetable[1] = ((mode & 02000) == 0)? mtable_normal : mtable_setid; + modetable[2] = ((mode & 04000) == 0)? mtable_normal : mtable_setid; - modetable[0] = ((mode & 01000) == 0)? mtable_normal : mtable_sticky; - modetable[1] = ((mode & 02000) == 0)? mtable_normal : mtable_setid; - modetable[2] = ((mode & 04000) == 0)? mtable_normal : mtable_setid; + for (int i = 0; i < 3; i++) + { + memcpy(CS(smode + 7 - i*3), CS(modetable[i][mode & 7]), 3); + mode >>= 3; + } - for (int i = 0; i < 3; i++) - { - memcpy(CS(smode + 7 - i*3), CS(modetable[i][mode & 7]), 3); - mode >>= 3; - } + smode[10] = 0; + yield = string_fmt_append(yield, + "mode=%04lo smode=%s inode=%ld device=%ld links=%ld " + "uid=%ld gid=%ld size=" OFF_T_FMT " atime=%ld mtime=%ld ctime=%ld", + (long)(st.st_mode & 077777), smode, (long)st.st_ino, + (long)st.st_dev, (long)st.st_nlink, (long)st.st_uid, + (long)st.st_gid, st.st_size, (long)st.st_atime, + (long)st.st_mtime, (long)st.st_ctime); + break; + } - smode[10] = 0; - yield = string_fmt_append(yield, - "mode=%04lo smode=%s inode=%ld device=%ld links=%ld " - "uid=%ld gid=%ld size=" OFF_T_FMT " atime=%ld mtime=%ld ctime=%ld", - (long)(st.st_mode & 077777), smode, (long)st.st_ino, - (long)st.st_dev, (long)st.st_nlink, (long)st.st_uid, - (long)st.st_gid, st.st_size, (long)st.st_atime, - (long)st.st_mtime, (long)st.st_ctime); - break; - } + /* vaguely random number less than N */ - /* vaguely random number less than N */ + case EOP_RANDINT: + { + int_eximarith_t max = expanded_string_integer(sub, TRUE); - case EOP_RANDINT: - { - int_eximarith_t max = expanded_string_integer(sub, TRUE); + if (expand_string_message) + goto EXPAND_FAILED; + yield = string_fmt_append(yield, "%d", vaguely_random_number((int)max)); + break; + } - if (expand_string_message) - goto EXPAND_FAILED; - yield = string_fmt_append(yield, "%d", vaguely_random_number((int)max)); - break; - } + /* Reverse IP, including IPv6 to dotted-nibble */ - /* Reverse IP, including IPv6 to dotted-nibble */ + case EOP_REVERSE_IP: + { + int family, maskptr; + uschar reversed[128]; - case EOP_REVERSE_IP: + family = string_is_ip_address(sub, &maskptr); + if (family == 0) { - int family, maskptr; - uschar reversed[128]; - - family = string_is_ip_address(sub, &maskptr); - if (family == 0) - { - expand_string_message = string_sprintf( - "reverse_ip() not given an IP address [%s]", sub); - goto EXPAND_FAILED; - } - invert_address(reversed, sub); - yield = string_cat(yield, reversed); - break; + expand_string_message = string_sprintf( + "reverse_ip() not given an IP address [%s]", sub); + goto EXPAND_FAILED; } + invert_address(reversed, sub); + yield = string_cat(yield, reversed); + break; + } - /* Unknown operator */ + case EOP_XTEXTD: + { + uschar * s; + int len = auth_xtextdecode(sub, &s); + yield = string_catn(yield, s, len); + break; + } - default: - expand_string_message = - string_sprintf("unknown expansion operator \"%s\"", name); - goto EXPAND_FAILED; - } /* EOP_* switch */ + /* Unknown operator */ + default: + expand_string_message = + string_sprintf("unknown expansion operator \"%s\"", name); + goto EXPAND_FAILED; + } /* EOP_* switch */ - DEBUG(D_expand) + DEBUG(D_expand) { const uschar * res = string_from_gstring(yield); const uschar * s = res + expansion_start; diff --git a/src/src/globals.c b/src/src/globals.c index 4e5fd2991..8230fe9cb 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -467,7 +467,7 @@ uschar *acl_smtp_quit = NULL; uschar *acl_smtp_rcpt = NULL; uschar *acl_smtp_starttls = NULL; uschar *acl_smtp_vrfy = NULL; -#ifdef EXPERIMENTAL_WELLKNOWN +#ifndef DISABLE_WELLKNOWN uschar *acl_smtp_wellknown = NULL; #endif @@ -500,6 +500,9 @@ uschar *acl_wherenames[] = { [ACL_WHERE_RCPT] = US"RCPT", [ACL_WHERE_QUIT] = US"QUIT", [ACL_WHERE_STARTTLS] = US"STARTTLS", [ACL_WHERE_VRFY] = US"VRFY", +#ifndef DISABLE_WELLKNOWN + [ACL_WHERE_WELLKNOWN] = US"WELLKNOWN", +#endif [ACL_WHERE_DELIVERY] = US"delivery", [ACL_WHERE_UNKNOWN] = US"unknown" }; @@ -519,6 +522,9 @@ uschar *acl_wherecodes[] = { [ACL_WHERE_RCPT] = US"550", [ACL_WHERE_EXPN] = US"550", [ACL_WHERE_HELO] = US"550", [ACL_WHERE_STARTTLS] = US"550", +#ifndef DISABLE_WELLKNOWN + [ACL_WHERE_WELLKNOWN] =US"550", +#endif [ACL_WHERE_VRFY] = US"252", }; @@ -999,9 +1005,6 @@ uschar *hosts_proxy = NULL; #endif uschar *hosts_treat_as_local = NULL; uschar *hosts_require_helo = US"*"; -#ifdef EXPERIMENTAL_WELLKNOWN -uschar *hosts_wellknown = NULL; -#endif #ifdef EXPERIMENTAL_XCLIENT uschar *hosts_xclient = NULL; #endif @@ -1669,6 +1672,11 @@ int warning_count = 0; const uschar *warnmsg_delay = NULL; const uschar *warnmsg_recipients = NULL; +#ifndef DISABLE_WELLKNOWN +uschar *wellknown_advertise_hosts = NULL; +uschar *wellknown_response = NULL; +#endif + /* End of globals.c */ /* vi: aw ai sw=2 */ diff --git a/src/src/globals.h b/src/src/globals.h index 30c8bbad4..febc30c84 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -342,6 +342,9 @@ extern uschar *acl_smtp_quit; /* ACL run for QUIT */ extern uschar *acl_smtp_rcpt; /* ACL run for RCPT */ extern uschar *acl_smtp_starttls; /* ACL run for STARTTLS */ extern uschar *acl_smtp_vrfy; /* ACL run for VRFY */ +#ifndef DISABLE_WELLKNOWN +extern uschar *acl_smtp_wellknown; /* ACL run for WELLKNOWN */ +#endif extern tree_node *acl_var_c; /* ACL connection variables */ extern tree_node *acl_var_m; /* ACL message variables */ extern uschar *acl_verify_message; /* User message for verify failure */ @@ -1134,4 +1137,9 @@ extern uschar *version_string; /* Version string */ extern int warning_count; /* Delay warnings sent for this msg */ +#ifndef DISABLE_WELLKNOWN +extern uschar *wellknown_advertise_hosts;/* Allow WELLKNOWN command for specified hosts */ +extern uschar *wellknown_response; /* SMTP response for WELLKNOWN verb */ +#endif + /* End of globals.h */ diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c index 9b354d345..55401e316 100644 --- a/src/src/macro_predef.c +++ b/src/src/macro_predef.c @@ -214,6 +214,9 @@ due to conflicts with other common macros. */ #ifndef DISABLE_TLS_RESUME builtin_macro_create(US"_HAVE_TLS_RESUME"); #endif +#ifndef DISABLE_WELLKNOWN + builtin_macro_create(US"_HAVE_WELLKNOWN"); +#endif #ifdef EXPERIMENTAL_XCLIENT builtin_macro_create(US"_HAVE_XCLIENT"); #endif diff --git a/src/src/macros.h b/src/src/macros.h index 2938b2523..16d1503f2 100644 --- a/src/src/macros.h +++ b/src/src/macros.h @@ -828,6 +828,9 @@ enum { SCH_NONE, SCH_AUTH, SCH_DATA, SCH_BDAT, SCH_EHLO, SCH_ETRN, SCH_EXPN, SCH_HELO, SCH_HELP, SCH_MAIL, SCH_NOOP, SCH_QUIT, SCH_RCPT, SCH_RSET, SCH_STARTTLS, SCH_VRFY, +#ifndef DISABLE_WELLKNOWN + SCH_WELLKNOWN, +#endif #ifdef EXPERIMENTAL_XCLIENT SCH_XCLIENT, #endif @@ -972,6 +975,9 @@ enum { ACL_WHERE_RCPT, /* Some controls are for RCPT only */ ACL_WHERE_NOTQUIT, ACL_WHERE_QUIT, ACL_WHERE_STARTTLS, +#ifndef DISABLE_WELLKNOWN + ACL_WHERE_WELLKNOWN, +#endif ACL_WHERE_VRFY, ACL_WHERE_DELIVERY, @@ -1001,6 +1007,9 @@ enum { ACL_WHERE_RCPT, /* Some controls are for RCPT only */ #define ACL_BIT_QUIT BIT(ACL_WHERE_QUIT) #define ACL_BIT_STARTTLS BIT(ACL_WHERE_STARTTLS) #define ACL_BIT_VRFY BIT(ACL_WHERE_VRFY) +#ifndef DISABLE_WELLKNOWN +# define ACL_BIT_WELLKNOWN BIT(ACL_WHERE_WELLKNOWN) +#endif #define ACL_BIT_DELIVERY BIT(ACL_WHERE_DELIVERY) #define ACL_BIT_UNKNOWN BIT(ACL_WHERE_UNKNOWN) @@ -1201,3 +1210,5 @@ When doing en extended loop of matching, release store periodically. */ DEBUG(D_expand) debug_printf("try option " name "\n"); /* End of macros.h */ +/* vi: aw ai sw=2 +*/ diff --git a/src/src/readconf.c b/src/src/readconf.c index d87a56f3d..aef07184a 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -66,6 +66,9 @@ static optionlist optionlist_config[] = { { "acl_smtp_starttls", opt_stringptr, {&acl_smtp_starttls} }, #endif { "acl_smtp_vrfy", opt_stringptr, {&acl_smtp_vrfy} }, +#ifndef DISABLE_WELLKNOWN + { "acl_smtp_wellknown", opt_stringptr, {&acl_smtp_wellknown} }, +#endif { "add_environment", opt_stringptr, {&add_environment} }, { "admin_groups", opt_gidlist, {&admin_groups} }, { "allow_domain_literals", opt_bool, {&allow_domain_literals} }, @@ -402,6 +405,9 @@ static optionlist optionlist_config[] = { { "uucp_from_pattern", opt_stringptr, {&uucp_from_pattern} }, { "uucp_from_sender", opt_stringptr, {&uucp_from_sender} }, { "warn_message_file", opt_stringptr, {&warn_message_file} }, +#ifndef DISABLE_WELLKNOWN + { "wellknown_advertise_hosts",opt_stringptr, {&wellknown_advertise_hosts} }, +#endif { "write_rejectlog", opt_bool, {&write_rejectlog} }, }; diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c index ff50c80f9..c941d115c 100644 --- a/src/src/smtp_in.c +++ b/src/src/smtp_in.c @@ -86,6 +86,9 @@ enum { /* These commands need not be synchronized when pipelining */ MAIL_CMD, RCPT_CMD, RSET_CMD, +#ifndef DISABLE_WELLKNOWN + WELLKNOWN_CMD, +#endif /* This is a dummy to identify the non-sync commands when not pipelining */ @@ -121,7 +124,8 @@ enum { /* These are specials that don't correspond to actual commands */ EOF_CMD, OTHER_CMD, BADARG_CMD, BADCHAR_CMD, BADSYN_CMD, - TOO_MANY_NONMAIL_CMD }; + TOO_MANY_NONMAIL_CMD +}; /* This is a convenience macro for adding the identity of an SMTP command @@ -230,7 +234,10 @@ static smtp_cmd_list cmd_list[] = { { "etrn", sizeof("etrn")-1, ETRN_CMD, TRUE, FALSE }, { "vrfy", sizeof("vrfy")-1, VRFY_CMD, TRUE, FALSE }, { "expn", sizeof("expn")-1, EXPN_CMD, TRUE, FALSE }, - { "help", sizeof("help")-1, HELP_CMD, TRUE, FALSE } + { "help", sizeof("help")-1, HELP_CMD, TRUE, FALSE }, +#ifndef DISABLE_WELLKNOWN + { "wellknown", sizeof("wellknown")-1, WELLKNOWN_CMD, TRUE, FALSE }, +#endif }; /* This list of names is used for performing the smtp_no_mail logging action. */ @@ -253,6 +260,9 @@ uschar * smtp_names[] = [SCH_RSET] = US"RSET", [SCH_STARTTLS] = US"STARTTLS", [SCH_VRFY] = US"VRFY", +#ifndef DISABLE_WELLKNOWN + [SCH_WELLKNOWN] = US"WELLKNOWN", +#endif #ifdef EXPERIMENTAL_XCLIENT [SCH_XCLIENT] = US"XCLIENT", #endif @@ -1946,7 +1956,7 @@ while (done <= 0) case HELP_CMD: case NOOP_CMD: case ETRN_CMD: -#ifdef EXPERIMENTAL_WELLKNOWN +#ifndef DISABLE_WELLKNOWN case WELLKNOWN_CMD: #endif bsmtp_transaction_linecount = receive_linecount; @@ -2832,10 +2842,8 @@ if (fl.rcpt_in_progress) We only handle pipelining these responses as far as nonfinal/final groups, not the whole MAIL/RCPT/DATA response set. */ -for (;;) - { - uschar *nl = Ustrchr(msg, '\n'); - if (!nl) +for (uschar * nl;;) + if (!(nl = Ustrchr(msg, '\n'))) { smtp_printf("%.3s%c%.*s%s\r\n", !final, code, final ? ' ':'-', esclen, esc, msg); return; @@ -2852,7 +2860,6 @@ for (;;) msg = nl + 1; Uskip_whitespace(&msg); } - } } @@ -2971,7 +2978,8 @@ smtp_code = rc == FAIL ? acl_wherecodes[where] : US"451"; smtp_message_code(&smtp_code, &codelen, &user_msg, &log_msg, where != ACL_WHERE_VRFY); -/* We used to have sender_address here; however, there was a bug that was not +/* Get info for logging. +We used to have sender_address here; however, there was a bug that was not updating sender_address after a rewrite during a verify. When this bug was fixed, sender_address at this point became the rewritten address. I'm not sure this is what should be logged, so I've changed to logging the unrewritten @@ -2996,7 +3004,7 @@ switch (where) { uschar * s = smtp_cmd_data; Uskip_nonwhite(&s); - lim = s - smtp_cmd_data; /* atop after method */ + lim = s - smtp_cmd_data; /* stop after method */ } what = string_sprintf("%s %.*s", acl_wherenames[where], lim, place); } @@ -3375,7 +3383,7 @@ Returns: nothing */ static void -smtp_user_msg(uschar *code, uschar *user_msg) +smtp_user_msg(uschar * code, uschar * user_msg) { int len = 3; smtp_message_code(&code, &len, &user_msg, NULL, TRUE); @@ -3581,6 +3589,36 @@ if (chunking_state > CHUNKING_OFFERED) } +#ifndef DISABLE_WELLKNOWN +static int +smtp_wellknown_handler(void) +{ +if (verify_check_host(&wellknown_advertise_hosts) != FAIL) + { + GET_OPTION("acl_smtp_wellknown"); + if (acl_smtp_wellknown) + { + uschar * user_msg = NULL, * log_msg; + int rc; + + if ((rc = acl_check(ACL_WHERE_WELLKNOWN, NULL, acl_smtp_wellknown, + &user_msg, &log_msg)) != OK) + return smtp_handle_acl_fail(ACL_WHERE_WELLKNOWN, rc, user_msg, log_msg); + else if (!wellknown_response) + return smtp_handle_acl_fail(ACL_WHERE_WELLKNOWN, ERROR, user_msg, log_msg); + smtp_user_msg(US"250", wellknown_response); + return 0; + } + } + +smtp_printf("554 not permitted\r\n", SP_NO_MORE); +log_write(0, LOG_MAIN|LOG_REJECT, "rejected \"%s\" from %s", + smtp_cmd_buffer, sender_helo_name, host_and_ident(FALSE)); +return 0; +} +#endif + + static int expand_mailmax(const uschar * s) { @@ -3686,9 +3724,8 @@ while (done <= 0) void (*oldsignal)(int); pid_t pid; int start, end, sender_domain, recipient_domain; - int rc; - int c; - uschar *orcpt = NULL; + int rc, c; + uschar * orcpt = NULL; int dsn_flags; gstring * g; @@ -4213,12 +4250,12 @@ while (done <= 0) chunking_state = CHUNKING_OFFERED; } +#ifndef DISABLE_TLS /* Advertise TLS (Transport Level Security) aka SSL (Secure Socket Layer) if it has been included in the binary, and the host matches tls_advertise_hosts. We must *not* advertise if we are already in a secure connection. */ -#ifndef DISABLE_TLS if (tls_in.active.sock < 0 && verify_check_host(&tls_advertise_hosts) != FAIL) { @@ -4252,6 +4289,13 @@ while (done <= 0) fl.smtputf8_advertised = TRUE; } #endif +#ifndef DISABLE_WELLKNOWN + if (verify_check_host(&wellknown_advertise_hosts) != FAIL) + { + g = string_catn(g, smtp_code, 3); + g = string_catn(g, US"-WELLKNOWN\r\n", 12); + } +#endif /* Finish off the multiline reply with one that is always available. */ @@ -4299,6 +4343,14 @@ while (done <= 0) toomany = FALSE; break; /* HELO/EHLO */ +#ifndef DISABLE_WELLKNOWN + case WELLKNOWN_CMD: + HAD(SCH_WELLKNOWN); + smtp_mailcmd_count++; + smtp_wellknown_handler(); + break; +#endif + #ifdef EXPERIMENTAL_XCLIENT case XCLIENT_CMD: { @@ -5455,6 +5507,10 @@ while (done <= 0) if (acl_smtp_etrn) smtp_printf(" ETRN", SP_MORE); if (acl_smtp_expn) smtp_printf(" EXPN", SP_MORE); if (acl_smtp_vrfy) smtp_printf(" VRFY", SP_MORE); +#ifndef DISABLE_WELLKNOWN + if (verify_check_host(&wellknown_advertise_hosts) != FAIL) + smtp_printf(" WELLKNOWN", SP_MORE); +#endif #ifdef EXPERIMENTAL_XCLIENT if (proxy_session || verify_check_host(&hosts_xclient) != FAIL) smtp_printf(" XCLIENT", SP_MORE); diff --git a/src/util/mailtest b/src/util/mailtest new file mode 100755 index 000000000..0c50d93f5 --- /dev/null +++ b/src/util/mailtest @@ -0,0 +1,486 @@ +#!/usr/bin/perl +# +############################################################### +############################################################### + +use strict; + +use Net::SMTP; +#use IO::Socket::SSL qw( SSL_ERROR ); +use Net::Domain qw(hostfqdn); +use Getopt::Long qw(:config default bundling no_ignore_case auto_version); +use Pod::Usage; +use Net::Cmd; +use Data::Dumper; + +our @ISA = qw(Net::Cmd); + +############################################################### +############################################################### + +my ($smtp,$optsin,$opt,$mess,$rcpt,@headers,$finished_header,$ofh); +$main::VERSION = '1.2.2'; + +$optsin = { + 'body|b' => \&optset, + 'debug|d' => \&optset, + 'ehlo|helo|m=s' => \&optset, + 'rcptto|recipient|r=s' => \&optset, + 'host|h=s' => \&optset, + 'from822|u=s' => \&optset, + 'vrfy|v' => \&optset, + 'expn|e' => \&optset, + 'mailfrom|from821|from|f=s' => \&optset, + 'port|p=i' => \&optset, + 'wellknown|w=s' => \&optset, + 'output|W=s' => \&optset, + 'include_options|O' => \&optset, + 'include_headers|H' => \&optset, + 'bounce|B' => \&optset, + 'tls|S' => \&optset, + 'nostarttls|s' => \&optset, + 'stricttls|strict_tls' => \&optset, + 'sslargs|tlsargs=s' => \&optset, + 'verbose' => \&optset, + 'help' => \&optset, + 'man' => \&optset, +}; +map { my $t = $_; $t =~ s/\|.*//; $opt->{$t} = undef; } keys %$optsin; +GetOptions( %$optsin ) or pod2usage(2); +pod2usage(1) if $opt->{'help'}; +pod2usage(-exitval => 0, -verbose => 2) if $opt->{'man'}; + +print _Dumper($opt, 'Options') + if $opt->{'debug'}; + +############################################################### +############################################################### +## +## parameter checking +## +############################################################### +############################################################### + +bail( 1, "Host(--host) must be provided" ) + if !$opt->{'host'}; + +$opt->{'port'} = $opt->{'tls'} ? 465 : 25 + if ! $opt->{'port'}; + +if (!$opt->{'ehlo'}) +{ + $opt->{'ehlo'} = hostfqdn(); + fret( "Machine set to $opt->{'ehlo'}" ) if $opt->{'debug'}; +} +if (!$opt->{'mailfrom'} && !$opt->{'bounce'}) +{ + $opt->{'mailfrom'} = $ENV{USER}. "@". $opt->{'ehlo'}; + fret( "MAIL FROM set to $opt->{'mailfrom'}" ) if $opt->{'debug'}; +} +if (!$opt->{'from822'}) +{ + $opt->{'from822'} = $opt->{'mailfrom'}; + fret( "From: set to $opt->{'from822'}" ) if $opt->{'debug'}; +} +if ($opt->{'bounce'}) +{ + $opt->{'mailfrom'} = ""; + $opt->{'from822'} = 'mailer-daemon@'. hostfqdn(); + fret( "MAIL FROM set to $opt->{'mailfrom'}", "From: set to $opt->{'from822'}" ) if $opt->{'debug'}; +} + +bail( 1, "EXPN or VRFY cannot be used without a recipient" ) + if ($opt->{'expn'} || $opt->{'vrfy'}) && ! $opt->{'rcptto'}; + +bail( 1, "Either a recipient or well-known resource must be specified" ) + if ! $opt->{'wellknown'} && ! $opt->{'rcptto'}; + +bail( 1, "Only one of recipient or well-known resource can be specified" ) + if $opt->{'wellknown'} && $opt->{'rcptto'}; + +if ( $opt->{'sslargs'} ) +{ + my @p = split /[=,]/, $opt->{'sslargs'}; + $opt->{'sslparams'} = \@p; +} +else +{ + $opt->{'sslparams'} = [ 'SSL_verify_mode', $opt->{'stricttls'} ? 1 : 0 ]; +} +fret( _Dumper( $opt->{'sslparams'}, 'sslparams' ) ) + if $opt->{'debug'} && ( $opt->{'tls'} || ! $opt->{'nostarttls'} ); + +############################################################### +############################################################### +## +## parameter checking complete. now onto operations +## +## +############################################################### +############################################################### + + + +$smtp= Net::SMTP->new( $opt->{'host'}, + Hello => $opt->{'ehlo'}, + Debug => $opt->{'debug'}, + ( $opt->{'tls'} ? ( 'SSL' => $opt->{'sslargs'} || 1 ) : () ), + Port => $opt->{'port'}, + ); +bail( 1, "Connection Failed: $@" ) + if !$smtp; + +if (!$opt->{'nostarttls'}) +{ + bail( $smtp, 1, "Failed to STARTTLS - $@" ) + if ! $smtp->starttls( @{$opt->{'sslparams'}} ); + + fret( $smtp->message() ) + if $opt->{'verbose'}; +} + +if ($opt->{'wellknown'}) +{ + bail( $smtp, 1, "Server does not support WELLKNOWN" ) + if ! $smtp->supports('WELLKNOWN'); + + my $e_wk = encode_xtext( $opt->{'wellknown'} ); + + bail( $smtp, 1, "Failed to WELLKNOWN - $e_wk", $smtp->message() ) + if ! ( $smtp->command( 'WELLKNOWN', $e_wk )->response() == CMD_OK ); + + fret( "Protocol violation. Code was OK, but not 250", $smtp->code. " - ". $smtp->message ) + if $smtp->code ne '250'; + + $mess = $smtp->message; + my ($info,$size); + ($info,$mess) = split( /\n/, $mess, 2 ); + $info =~ /size=(\d+)/i; + $size = $1 + 0; + $mess = decode_xtext( $mess ); + fret( "Size mismatch on wellknown fetch", "Expected: ". $size, "Received: ". length($mess) ) + if length($mess) != $size; + + if ( $opt->{'output'} ) + { + # Output to named file + # + bail( $smtp, 1, "Unable to open file $opt->{'output'} for WELLKNOWN output - $!" ) + if ! ( $ofh = IO::File->new("> $opt->{'output'}") ); + + print $ofh $mess; + $ofh->close; + } + else + { + # might be hazardous, output via pager + print STDERR "$mess\n"; + } +} + +if ($opt->{'vrfy'}) +{ + $smtp->verify($opt->{'vrfy'}); + fret( $smtp->message() ); +} + +if ($opt->{'expn'}) +{ + $smtp->expand($opt->{'expn'}); + fret( $smtp->message() ); +} + +if ($opt->{'rcptto'}) +{ + bail( $smtp, 1, "MAIL FROM $opt->{'mailfrom'} failed", $@ ) + if ! $smtp->mail($opt->{'mailfrom'}); + + bail( $smtp, 1, "RCPT TO $opt->{'rcptto'} failed", $@ ) + if ! $smtp->to($opt->{'rcptto'}); + + # handle any recipients on command line + while( $rcpt = shift @ARGV ) + { + last if $rcpt eq '--'; + fret( "RCPT TO $rcpt failed", $@ ) + if ! $smtp->to($rcpt); + } + + bail( $smtp, 1, "Unable to set data mode", @_ ) + if ! $smtp->data(); + + if ($opt->{'body'}) + { + push @headers, "Subject: Test Message\n"; + $smtp->datasend("From: $opt->{'from822'}\n"); + $smtp->datasend("To: $opt->{'rcptto'}\n"); + $smtp->datasend("Subject: Test Message\n"); + $smtp->datasend("\n"); + $smtp->datasend("This is a test message\n"); + $smtp->datasend("generated with mailtest\n"); + }else + { + while(<>) + { + if($finished_header==0) + { + if (length($_)<=1) + { + $finished_header = 1; + }else + { + push @headers," ".$_; + } + } + $smtp->datasend("$_"); + } + } + if($opt->{'include_headers'} && @headers) + { + $smtp->datasend("\n Copy of headers follow....\n"); + foreach(@headers) + { + $smtp->datasend("$_"); + } + $smtp->datasend("\n"); + } + if($opt->{'include_options'}) + { + $smtp->datasend("\n Copy of options follow....\n"); + $smtp->datasend(" SMTP HOST $opt->{'host'}\n"); + $smtp->datasend(" HELO $opt->{'ehlo'}\n"); + $smtp->datasend(" MAIL FROM: $opt->{'mailfrom'}\n"); + $smtp->datasend(" RCPT TO: $opt->{'rcptto'}\n\n"); + } + fret( "dataend failed", $@ ) + if ! $smtp->dataend(); +} +$smtp->quit(); + +exit; + +############################################################## +############################################################## + +sub +optset +{ + my $n = shift; + my $v = shift; + #print STDERR "Setting $n to $v\n"; + $opt->{$n->{'name'}} = $v; +} + +sub +decode_xtext +{ + my $mess = shift; + $mess =~ s/[\n\r]//g; + $mess =~ s/\+([0-9a-fA-F]{2})/chr(hex($1))/ge; + return $mess; +} + +sub +encode_xtext +{ + my $mess = shift; + $mess =~ s/([^!-*,-<>-~])/'+'.uc(unpack('H*', $1))/eg; + return $mess; +} + +sub +_Dumper +{ + return Data::Dumper->Dump( [$_[0]], [$_[1] || 'VAR1'] ); +} + +sub +fret +{ + map { print STDERR $_,"\n"; } @_; +} + +sub +bail +{ + shift->quit + if ref($_[0]); + my $rc = shift; + fret( @_ ); + exit $rc; +} + +############################################################## +############################################################## + +__END__ + +=head1 NAME + +mailtest - Simple SMTP sending for diagnostics + +=head1 SYNOPSIS + +mailtest --host host.example.com --rcptto recipient@example.com [ send_options ... ] [ additional recipients ...] + + +Options: + --help + brief help message + --debug + enable debugging + + --host host + host to connect to + --rcptto recipient + recipient for message + + --helo machine + machine name for EHLO + + --vrfy request VRFY of recipient + --expn request EXPN of recipient + + --mailfrom from + use as MAIL FROM value + --from822 from + content From: + + --port port + port to connect to + + --body generate body + --include_options + include Options in body + --include_headers + include generated headers in body + + --tls perform TLS on connect + --nostarttls do no attempt STARTTLS + --stricttls Enable strict verification on TLS connection + + --tlsargs arg=value[,arg=value] + Explicitly define TLS options. + + --bounce sending as bounce (<>) + + --wellknown path + well-known path + --output file + Output file to receive well-known data + +=head1 OPTIONS + +=over 8 + + +=item B<--help> + +Print a brief help message and exits. + +=item B<-d, --debug> + +Enables debugging, outpus additional information whilst processing requests. + +=item B<-h, --host>=I + +Specifies the host to connect to. Must be specified and must be IP-resolvable. + +=item B<-m, --ehlo>=I + +Specified the machine name to use as the B value. Defaults to the fully-qualified name of the host running the command. + +=item B<-r, --rcptto>=I + +Specifies the recipient of message. This is used as the B value. + +=item B<-v, --vrfy> + +Uses the I parameter for the value in a B request. This disables the sending of an email. + +=item B<-e, --expn> + +Uses the I parameter for the value in an B request. This disables the sending of an email. + +=item B<-f, --mailfrom>=I + +Specified the value to use in the B command. Defaults to the current username at the FQDN of the machine B<-m> unless the B<-B> option is used. + +=item B<-u, --from822>=I + +Specified the value to use in the message headers. Defaults to the B<-f> I value unless the B<-B> option is used. + +=item B<-B, --bounce> + +Replace the B<--mailfrom> I with B<\<\>> and the B<--from833> I with B where the host is the value of B<--ehlo> I + +=item B<-p, --port>=I + +Specified the port to connect to on the specified host. Defaults to port 25 unless B<-S> is given in which case it defaults to 465. + +=item B<-S, --tls> + +Specifies that TLS be used directly on the connection prior to any SMTP commands. Changes the connection port to 465 unless it has been explicitly provided. Disables any attempts at B. + +=item B<-s, --nostarttls> + +Disables attempting STARTTLS if offered. Disabled by use of B<-S>. + +=item B<--stricttls> + +Enables strict verification of the TLS connection. Sets the underlying SSL option B to 1/SSL_VERIFY_PEER rather than 0/SSL_VERIFY_NONE. Since the aim of this tool is to test the SMTP protocol behaviour and not the TLS behaviour the decision was made to default the B to 0/SSL_VERIFY_NONE. + +=item B<--sslargs>=argname=argvalue[,argname=argvalue...] + +Allow full control over underlying SSL options. Overrides B<--stricttls>. See the documentation for B for further details. + + --sslargs SSL_verifycn_name=certname.example.com + +=item B<-b, --body> + +Generate a body for the message being sent. + +=item B<-O, --include-options> + +Include details of options used in the message body. + +=item B<-H, --include-headers> + +Include a copy of the generated headers in the message body. + +=item B<-w, --wellknown>=I + +Provides the path value for a B command. + +=item B<-W, --output>=I + +Provides the output file where the B data should be stored. + +=back + +=head1 DESCRIPTION + +B is a simple utility for testing SMTP connections. +It is designed to debug endpoints and not for full email generation. + +It support a number of basic operations, SEND, VRFY, EXPN, WELLKNOWN. + +=head1 COMPATIBILITY + +C only requires modules that should be in all normal distributions. + +=head1 AUTHOR + +Bernard Quatermass + +=head1 COPYRIGHT AND LICENSE + +This software is Copyright (c) 2008,2020,2024 by Bernard Quatermass. + + +=cut + +############################################################### +# vi: sw=4 et +# End of File +############################################################### diff --git a/test/aux-fixed/4040/acme-response b/test/aux-fixed/4040/acme-response new file mode 100644 index 000000000..d3f618d8c --- /dev/null +++ b/test/aux-fixed/4040/acme-response @@ -0,0 +1,3 @@ +line 1 +line 2 +last line diff --git a/test/aux-fixed/4040/sub/acme-response b/test/aux-fixed/4040/sub/acme-response new file mode 100644 index 000000000..d3f618d8c --- /dev/null +++ b/test/aux-fixed/4040/sub/acme-response @@ -0,0 +1,3 @@ +line 1 +line 2 +last line diff --git a/test/confs/4040 b/test/confs/4040 new file mode 100644 index 000000000..c5c6c3c0e --- /dev/null +++ b/test/confs/4040 @@ -0,0 +1,29 @@ +# Exim test configuration 4040 + +SERVER= +OPT= + +.include DIR/aux-var/std_conf_prefix + +primary_hostname = myhost.test.ex + +# ----- Main settings ----- + +wellknown_advertise_hosts = 127.0.0.1 +acl_smtp_wellknown = check_wellknown + +# ----- ACL ----- + +begin acl + +check_wellknown: + accept + logwrite = [$sender_host_address] $smtp_command + condition = ${if == {${received_port}}{PORT_D}} + set acl_c_wellknown = ${lookup {${xtextd:$smtp_command_argument}} \ + dsearch,ret=full,filter=fileOPT \ + {DIR/aux-fixed/TESTNUM}} + logwrite = [$sender_host_address] -> '$acl_c_wellknown' + control = wellknown/$acl_c_wellknown + +# End diff --git a/test/log/4040 b/test/log/4040 new file mode 100644 index 000000000..4274a7a75 --- /dev/null +++ b/test/log/4040 @@ -0,0 +1,21 @@ + +******** SERVER ******** +1999-03-02 09:44:33 exim x.yz daemon started: pid=p1234, no queue runs, listening for SMTP on port PORT_D port PORT_D2 +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN acme-response +1999-03-02 09:44:33 [127.0.0.1] -> 'TESTSUITE/aux-fixed/4040/acme-response' +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN acme-response +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN acme-response +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN badfile +1999-03-02 09:44:33 [127.0.0.1] -> '' +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN badfile +1999-03-02 09:44:33 exim x.yz daemon started: pid=p1235, no queue runs, listening for SMTP on port PORT_D port PORT_D2 +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN acme-response +1999-03-02 09:44:33 [127.0.0.1] -> 'TESTSUITE/aux-fixed/4040/acme-response' +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN sub/acme-response +1999-03-02 09:44:33 [127.0.0.1] -> 'TESTSUITE/aux-fixed/4040/sub/acme-response' +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN sub/badfile +1999-03-02 09:44:33 [127.0.0.1] -> '' +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN sub/badfile +1999-03-02 09:44:33 [127.0.0.1] WELLKNOWN ../badfile +1999-03-02 09:44:33 [127.0.0.1] -> '' +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN ../badfile diff --git a/test/rejectlog/4040 b/test/rejectlog/4040 new file mode 100644 index 000000000..9d4ca3aa6 --- /dev/null +++ b/test/rejectlog/4040 @@ -0,0 +1,6 @@ + +******** SERVER ******** +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN acme-response +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN badfile +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN sub/badfile +1999-03-02 09:44:33 H=(test) [127.0.0.1] rejected WELLKNOWN ../badfile diff --git a/test/runtest b/test/runtest index b959107c1..683dee2d1 100755 --- a/test/runtest +++ b/test/runtest @@ -1430,6 +1430,9 @@ RESET_AFTER_EXTRA_LINE_READ: # DISABLE_OCSP next if /in hosts_requ(est|ire)_ocsp\? (no|yes)/; + # WELLKNOWN + next if / in wellknown_advertise_hosts\?/; + # SUPPORT_PROXY next if /host in hosts_proxy\?/; @@ -1458,7 +1461,10 @@ RESET_AFTER_EXTRA_LINE_READ: next if / in limits_advertise_hosts?\? no \(matched "!\*"\)/; # Experimental_XCLIENT - next if / in hosts_xclient?\? no \(option unset\)/; + next if / in hosts_xclient\? no \(option unset\)/; + + # Experimental_WELLKNOWN + next if / in hosts_wellknown\? no \(option unset\)/; # TCP Fast Open next if /^(ppppp )?setsockopt FASTOPEN: Network Error/; @@ -3663,7 +3669,7 @@ while () } elsif (/^Support for: (.*)/) - { + { # Compile-time features - exim -bV print; @temp = split /(\s+)/, $1; push(@temp, ' '); @@ -4230,7 +4236,7 @@ DIR: for (my $i = 0; $i < @test_dirs; $i++) if (!defined $parm_malware{$1}) { $wantthis = 0; last; } } elsif (/^(not )?feature (.*)$/) - { + { #a macro name, or lack thereof - -bP macros # move to a subroutine? my $eximinfo = "$parm_exim -C $parm_cwd/test-config -DDIR=$parm_cwd -bP macro $2"; diff --git a/test/scripts/4040-wellknown/4040 b/test/scripts/4040-wellknown/4040 new file mode 100644 index 000000000..8ca40306f --- /dev/null +++ b/test/scripts/4040-wellknown/4040 @@ -0,0 +1,157 @@ +# ESMTP WELLNOWN server response +# +# when WELLKNOWN leaves EXPERIMENTAL, add standalone tests +# for ${xtextd:str} to 0002 +# +# +exim -DSERVER=server -bd -oX PORT_D:PORT_D2 +**** +# +client 127.0.0.1 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN acme-response +??? 250-SIZE +??? 250- +??? 250- +??? 250 +QUIT +??? 221 +**** +# +# not advertised conditional on hosts_wellknown +client HOSTIPV4 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250 HELP +QUIT +??? 221 +**** +# +# deny by acl +client 127.0.0.1 PORT_D2 +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN acme-response +??? 550 +QUIT +??? 221 +**** +# +# nonexistent file +client 127.0.0.1 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN badfile +??? 550 +QUIT +??? 221 +**** +# +killdaemon +# +exim -DSERVER=server -DOPT=,key=path -bd -oX PORT_D:PORT_D2 +**** +# +# dsearch with key=path permission +# basic good file +client 127.0.0.1 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN acme-response +??? 250-SIZE +??? 250- +??? 250- +??? 250 +QUIT +??? 221 +**** +# +# subdir/good file +client 127.0.0.1 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN sub/acme-response +??? 250-SIZE +??? 250- +??? 250- +??? 250 +QUIT +??? 221 +**** +# +# nonexistent file +client 127.0.0.1 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN sub/badfile +??? 550 +QUIT +??? 221 +**** +# +# dotdot trap +client 127.0.0.1 PORT_D +??? 220 +EHLO test +??? 250- +??? 250-SIZE +??? 250-LIMITS +??? 250-8BITMIME +??? 250-PIPELINING +??? 250-WELLKNOWN +??? 250 HELP +WELLKNOWN ../badfile +??? 550 +QUIT +??? 221 +**** +# +killdaemon diff --git a/test/scripts/4040-wellknown/REQUIRES b/test/scripts/4040-wellknown/REQUIRES new file mode 100644 index 000000000..457fc5f74 --- /dev/null +++ b/test/scripts/4040-wellknown/REQUIRES @@ -0,0 +1 @@ +support ESMTP_Wellknown