Autodiscover advertises implicit-TLS ports (993/995/465) as <Encryption>TLS</Encryption>, which Outlook treats as STARTTLS: account setup hangs and fails

Issue Description

What happened?

Adding an IMAP account in Outlook for Windows (classic) via Autodiscover never completes. Outlook retrieves the settings from Stalwart (POST /autodiscover/autodiscover.xml → HTTP 200). The setup wizard then stalls for a long time and finally reports that the account could not be configured.

For the implicit-TLS ports, the response contains both <SSL>on</SSL> and <Encryption>TLS</Encryption>:

<Protocol>
  <Type>IMAP</Type>
  <Server>mail.example.com</Server>
  <Port>993</Port>
  <LoginName>[email protected]</LoginName>
  <AuthRequired>on</AuthRequired>
  <DirectoryPort>0</DirectoryPort>
  <ReferralPort>0</ReferralPort>
  <SSL>on</SSL>
  <Encryption>TLS</Encryption>
  <SPA>off</SPA>
</Protocol>

[MS-OXDSCLI] states: “If the Encryption element is present, it overrides the SSL element.” Its allowed values are None, SSL, TLS and Auto. These are the same four options as the encryption drop-down in Outlook’s manual IMAP/SMTP settings, where current builds label them SSL/TLS (implicit TLS) and STARTTLS. So Encryption=TLS tells Outlook to use STARTTLS on 993/995/465.

Outlook’s Test E-mail AutoConfiguration tool shows the IMAP entry for port 993 as TLS: Yes.

Outlook therefore opens a plaintext connection to 993/465 and waits for the server greeting before sending STARTTLS. Stalwart, meanwhile, waits for a TLS ClientHello. Neither side sends anything until Outlook times out.

Confirmed: with a reverse proxy rewriting <Encryption>TLS</Encryption> to <Encryption>SSL</Encryption> in this response, Outlook completes the account setup automatically.

How can we reproduce the problem?

  1. Run Stalwart with implicit-TLS listeners for IMAP (993) and submission (465), with cleartext disabled for these services.
  2. Request the Autodiscover settings:
   curl -s -X POST https://autodiscover.example.com/autodiscover/autodiscover.xml \
     -H 'Content-Type: text/xml' \
     --data '<?xml version="1.0" encoding="utf-8"?><Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>[email protected]</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>'

Every <Protocol> block for an implicit-TLS port (993, 995, 465) contains <SSL>on</SSL> and <Encryption>TLS</Encryption>.
3. Emulate what Outlook does with that value (a plaintext connection that waits for a greeting):

   nc -v mail.example.com 993                              # connects; no greeting ever arrives
   openssl s_client -connect mail.example.com:993 -quiet   # implicit TLS: "* OK ..." greeting arrives immediately
  1. In Outlook (classic, Windows), go to File → Add Account and enter the address. The wizard hangs, then fails. Ctrl + right-click the Outlook tray icon → Test E-mail AutoConfiguration shows IMAP port 993 with “TLS: Yes”.

With the same credentials, implicit TLS on the same ports works: IMAP LOGIN/LIST/SELECT and SMTP AUTH/MAIL FROM/RCPT TO succeed. A plaintext connection to 993/465, which is what Outlook opens for Encryption=TLS, receives no greeting within 20 s.

Root cause

crates/common/src/network/autoconfig/autodiscover.rs hardcodes Encryption=TLS for the implicit-TLS port and emits no <Encryption> for the plaintext port (lines 58–89 at commit 9d1c75a):

There is no configuration workaround: Service (in crates/registry/src/schema/structs.rs) only exposes hostname and cleartext.

Enabling cleartext makes it worse. The plaintext ports (143/110/587) are listed first, with <SSL>off</SSL> and no <Encryption>, which means no encryption at all for Outlook.

The Mozilla autoconfig endpoint already maps this correctly: SSL for the TLS port and STARTTLS for the plaintext port (crates/common/src/network/autoconfig/legacy_autoconfig.rs, lines 49–58 at the same commit). Only Autodiscover is affected.

Proposed fix

--- a/crates/common/src/network/autoconfig/autodiscover.rs
+++ b/crates/common/src/network/autoconfig/autodiscover.rs
@@ -77,8 +77,9 @@
-                    let _ = writeln!(
-                        &mut config,
-                        "\t\t\t\t<SSL>{}</SSL>",
-                        if is_tls == 1 { "on" } else { "off" }
-                    );
-                    if is_tls == 1 {
-                        let _ = writeln!(&mut config, "\t\t\t\t<Encryption>TLS</Encryption>");
-                    }
+                    // MS-OXDSCLI: <Encryption> overrides <SSL>. For Outlook, "SSL" means
+                    // implicit TLS and "TLS" means STARTTLS.
+                    let (ssl, encryption) = if is_tls == 1 {
+                        ("on", "SSL")
+                    } else {
+                        ("off", "TLS")
+                    };
+                    let _ = writeln!(&mut config, "\t\t\t\t<SSL>{ssl}</SSL>");
+                    let _ = writeln!(&mut config, "\t\t\t\t<Encryption>{encryption}</Encryption>");

Suggestion: when cleartext is enabled, emit the implicit-TLS block before the STARTTLS one, so that clients taking the first block prefer implicit TLS (RFC 8314).

Workaround

Rewrite the response in a reverse proxy. Tested in production with Caddy and the caddyserver/replace-response plugin:

@autodiscover_xml path /autodiscover/autodiscover.xml /autodiscover.xml
handle {
	request_header @autodiscover_xml Accept-Encoding identity
	replace @autodiscover_xml "<Encryption>TLS</Encryption>" "<Encryption>SSL</Encryption>"
	reverse_proxy 127.0.0.1:8080
}
  • Accept-Encoding identity keeps the body uncompressed so the text can be rewritten.
  • The matcher keeps the rewrite away from other paths, such as WebDAV file content.
  • Apply it to both autodiscover.<domain> and the server hostname, because the Autodiscover V2 JSON response points clients to https://<server-hostname>/autodiscover/autodiscover.xml.

Alternatively, set up accounts manually in Outlook with SSL/TLS on 993 and 465.

Environment

  • Stalwart: v0.16.22 (Docker image stalwartlabs/stalwart:v0.16.22); the code above is unchanged on main at 9d1c75a
  • Outlook (classic), Microsoft 365 Apps Click-to-Run x64, build 16.0.17932.21000, Windows 11 (build 26100)
  • Caddy in front of Stalwart for HTTPS. The problem was reproduced with the XML passed through unchanged; the workaround above is the only rewrite.

References

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

S3-compatible

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

This confirms the code matches the report exactly, current behavior hardcodes <Encryption>TLS</Encryption> for implicit-TLS ports with no way to configure otherwise. This is a well-documented, well-diagnosed bug report rather than a usage question, so there isn’t a docs answer to give beyond acknowledging it.

Thanks for the extremely detailed writeup, root cause, and patch. This matches the current source exactly: crates/common/src/network/autoconfig/autodiscover.rs unconditionally emits <Encryption>TLS</Encryption> whenever is_tls == 1 (i.e., for the implicit-TLS listener), which per MS-OXDSCLI tells Outlook to use STARTTLS on a port that only speaks implicit TLS, causing the hang you describe. There’s no config knob in Service (hostname/cleartext only) to work around it from the config side, so your Caddy response-rewrite workaround is a reasonable stopgap in the meantime.

A human maintainer will need to review and apply the fix (or an equivalent), since this is a code-level defect rather than a documentation or configuration gap.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

This has been fixed in v0.16.24 (yet to be released).