[exim-cvs] SMTP WELLKNOWN extension

Página Inicial
Delete this message
Reply to this message
Autor: Exim Git Commits Mailing List
Data:  
Para: exim-cvs
Assunto: [exim-cvs] SMTP WELLKNOWN extension
Gitweb: https://git.exim.org/exim.git/commitdiff/e0b3815dc398d7abaa1f81c9f26b1c9b050e94c0
Commit:     e0b3815dc398d7abaa1f81c9f26b1c9b050e94c0
Parent:     b66eb2387fe955529001a76daab428d6dfb50c89
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Thu May 30 16:20:52 2024 +0100
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Thu May 30 16:32:57 2024 +0100

    SMTP WELLKNOWN extension
---
 doc/doc-docbook/spec.xfpt             | 110 ++++-
 doc/doc-txt/NewStuff                  |   4 +
 doc/doc-txt/experimental-spec.txt     |   3 +
 doc/doc-txt/id-wellknown.txt          | 145 ++++++
 src/src/EDITME                        |   3 +
 src/src/acl.c                         | 124 ++++-
 src/src/auths/xtextencode.c           |   2 +
 src/src/config.h.defaults             |   1 +
 src/src/exim.c                        |   3 +
 src/src/expand.c                      | 859 +++++++++++++++++-----------------
 src/src/globals.c                     |  16 +-
 src/src/globals.h                     |   8 +
 src/src/macro_predef.c                |   3 +
 src/src/macros.h                      |  11 +
 src/src/readconf.c                    |   6 +
 src/src/smtp_in.c                     |  86 +++-
 src/util/mailtest                     | 486 +++++++++++++++++++
 test/aux-fixed/4040/acme-response     |   3 +
 test/aux-fixed/4040/sub/acme-response |   3 +
 test/confs/4040                       |  29 ++
 test/log/4040                         |  21 +
 test/rejectlog/4040                   |   6 +
 test/runtest                          |  12 +-
 test/scripts/4040-wellknown/4040      | 157 +++++++
 test/scripts/4040-wellknown/REQUIRES  |   1 +
 25 files changed, 1631 insertions(+), 471 deletions(-)

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 &<<SECTi18nMTA>>&.
+
+
+.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 &<<CHAPACL>>& for further details.
 This option defines the ACL that is run when an SMTP VRFY command is
 received. See chapter &<<CHAPACL>>& 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 &<<SECTWELLKNOWNACL>>& 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 (&<<SECTWELLKNOWNACL>>&).
+.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:<coded-string>}
+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 &<<SECTi18nMTA>>&.
+
+.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 &<<SECTWELLKNOWNACL>>&.
+.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 <request-key>
+
+The <request-key> 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@??? [ 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<host>
+
+Specifies the host to connect to. Must be specified and must be IP-resolvable.
+
+=item B<-m, --ehlo>=I<machine>
+
+Specified the machine name to use as the B<EHLO> value. Defaults to the fully-qualified name of the host running the command.
+
+=item B<-r, --rcptto>=I<recipient>
+
+Specifies the recipient of message. This is used as the B<RCPT TO> value.
+
+=item B<-v, --vrfy>
+
+Uses the I<recipient> parameter for the value in a B<VRFY> request. This disables the sending of an email.
+
+=item B<-e, --expn>
+
+Uses the I<recipient> parameter for the value in an B<EXPN> request. This disables the sending of an email.
+
+=item B<-f, --mailfrom>=I<from_address>
+
+Specified the value to use in the B<MAIL FROM> 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<from_user>
+
+Specified the value to use in the message headers. Defaults to the B<-f> I<from_address> value unless the B<-B> option is used.
+
+=item B<-B, --bounce>
+
+Replace the B<--mailfrom> I<from_address> with B<\<\>> and the B<--from833> I<from_user> with B<mailer-daemon@host> where the host is the value of B<--ehlo> I<machine>
+
+=item B<-p, --port>=I<port>
+
+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<STARTTLS>.
+
+=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<SSL_verify_mode> 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<SSL_verify_mode> 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<IO::Socket::SSL> 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<well-known-path>
+
+Provides the path value for a B<WELLKNOWN> command.
+
+=item B<-W, --output>=I<output_file>
+
+Provides the output file where the B<WELLKNOWN> data should be stored.
+
+=back
+
+=head1 DESCRIPTION
+
+B<mailtest> 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<mailtest> only requires modules that should be in all normal distributions.
+
+=head1 AUTHOR
+
+Bernard Quatermass <bernardq@???>
+
+=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 (<EXIMINFO>)
     }
 
   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


--
## subscription configuration (requires account):
## https://lists.exim.org/mailman3/postorius/lists/exim-cvs.lists.exim.org/
## unsubscribe (doesn't require an account):
## exim-cvs-unsubscribe@???
## Exim details at http://www.exim.org/
## Please use the Wiki with this list - http://wiki.exim.org/