10 "github.com/prometheus/client_golang/prometheus"
11 "github.com/prometheus/client_golang/prometheus/promauto"
14 "github.com/mjl-/mox/admin"
15 "github.com/mjl-/mox/smtp"
19 metricAutoconf = promauto.NewCounterVec(
20 prometheus.CounterOpts{
21 Name: "mox_autoconf_request_total",
22 Help: "Number of autoconf requests.",
26 metricAutodiscover = promauto.NewCounterVec(
27 prometheus.CounterOpts{
28 Name: "mox_autodiscover_request_total",
29 Help: "Number of autodiscover requests.",
35// Autoconfiguration/Autodiscovery:
37// - Thunderbird will request an "autoconfig" xml file.
38// - Microsoft tools will request an "autodiscovery" xml file.
39// - In my tests on an internal domain, iOS mail only talks to Apple servers, then
40// does not attempt autoconfiguration. Possibly due to them being private DNS
41// names. Apple software can be provisioned with "mobileconfig" profile files,
42// which users can download after logging in.
44// DNS records seem optional, but autoconfig.<domain> and autodiscover.<domain>
45// (both CNAME or A) are useful, and so is SRV _autodiscovery._tcp.<domain> 0 0 443
46// autodiscover.<domain> (or just <hostname> directly).
48// Autoconf/discovery only works with valid TLS certificates, not with self-signed
49// certs. So use it on public endpoints with certs signed by common CA's, or run
50// your own (internal) CA and import the CA cert on your devices.
52// Also see https://roll.urown.net/server/mail/autoconfig.html
54// Autoconfiguration for Mozilla Thunderbird.
55// User should create a DNS record: autoconfig.<domain> (CNAME or A).
56// See https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat
57func autoconfHandle(w http.ResponseWriter, r *http.Request) {
58 log := pkglog.WithContext(r.Context())
62 metricAutoconf.WithLabelValues(addrDom).Inc()
65 email := r.FormValue("emailaddress")
66 log.Debug("autoconfig request", slog.String("email", email))
67 addr, err := smtp.ParseAddress(email)
69 http.Error(w, "400 - bad request - invalid parameter emailaddress", http.StatusBadRequest)
73 socketType := func(tlsMode admin.TLSMode) (string, error) {
75 case admin.TLSModeImmediate:
77 case admin.TLSModeSTARTTLS:
78 return "STARTTLS", nil
79 case admin.TLSModeNone:
82 return "", fmt.Errorf("unknown tls mode %v", tlsMode)
86 var imapTLS, submissionTLS string
87 config, err := admin.ClientConfigDomain(addr.Domain)
89 imapTLS, err = socketType(config.IMAP.TLSMode)
92 submissionTLS, err = socketType(config.Submission.TLSMode)
95 http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
99 // Thunderbird doesn't seem to allow U-labels, always return ASCII names.
100 var resp autoconfigResponse
102 resp.EmailProvider.ID = addr.Domain.ASCII
103 resp.EmailProvider.Domain = addr.Domain.ASCII
104 resp.EmailProvider.DisplayName = email
105 resp.EmailProvider.DisplayShortName = addr.Domain.ASCII
107 // todo: specify SCRAM-SHA-256 once thunderbird and autoconfig supports it. or perhaps that will fall under "password-encrypted" by then.
108 // todo: let user configure they prefer or require tls client auth and specify "TLS-client-cert"
110 resp.EmailProvider.IncomingServer.Type = "imap"
111 resp.EmailProvider.IncomingServer.Hostname = config.IMAP.Host.ASCII
112 resp.EmailProvider.IncomingServer.Port = config.IMAP.Port
113 resp.EmailProvider.IncomingServer.SocketType = imapTLS
114 resp.EmailProvider.IncomingServer.Username = email
115 resp.EmailProvider.IncomingServer.Authentication = "password-encrypted"
117 resp.EmailProvider.OutgoingServer.Type = "smtp"
118 resp.EmailProvider.OutgoingServer.Hostname = config.Submission.Host.ASCII
119 resp.EmailProvider.OutgoingServer.Port = config.Submission.Port
120 resp.EmailProvider.OutgoingServer.SocketType = submissionTLS
121 resp.EmailProvider.OutgoingServer.Username = email
122 resp.EmailProvider.OutgoingServer.Authentication = "password-encrypted"
124 // todo: should we put the email address in the URL?
125 resp.ClientConfigUpdate.URL = fmt.Sprintf("https://autoconfig.%s/mail/config-v1.1.xml", addr.Domain.ASCII)
127 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
128 enc := xml.NewEncoder(w)
130 fmt.Fprint(w, xml.Header)
131 if err := enc.Encode(resp); err != nil {
132 log.Errorx("marshal autoconfig response", err)
136// Autodiscover from Microsoft, also used by Thunderbird.
137// User should create a DNS record: _autodiscover._tcp.<domain> SRV 0 0 443 <hostname>
139// In practice, autodiscover does not seem to work wit microsoft clients. A
140// connectivity test tool for outlook is available on
141// https://testconnectivity.microsoft.com/, it has an option to do "Autodiscover to
142// detect server settings". Incoming TLS connections are all failing, with various
145// Thunderbird does understand autodiscover.
146func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
147 log := pkglog.WithContext(r.Context())
151 metricAutodiscover.WithLabelValues(addrDom).Inc()
154 if r.Method != "POST" {
155 http.Error(w, "405 - method not allowed - post required", http.StatusMethodNotAllowed)
159 var req autodiscoverRequest
160 if err := xml.NewDecoder(r.Body).Decode(&req); err != nil {
161 http.Error(w, "400 - bad request - parsing autodiscover request: "+err.Error(), http.StatusMethodNotAllowed)
165 log.Debug("autodiscover request", slog.String("email", req.Request.EmailAddress))
167 addr, err := smtp.ParseAddress(req.Request.EmailAddress)
169 http.Error(w, "400 - bad request - invalid parameter emailaddress", http.StatusBadRequest)
173 // tlsmode returns the "ssl" and "encryption" fields.
174 tlsmode := func(tlsMode admin.TLSMode) (string, string, error) {
176 case admin.TLSModeImmediate:
177 return "on", "TLS", nil
178 case admin.TLSModeSTARTTLS:
180 case admin.TLSModeNone:
181 return "off", "", nil
183 return "", "", fmt.Errorf("unknown tls mode %v", tlsMode)
187 var imapSSL, imapEncryption string
188 var submissionSSL, submissionEncryption string
189 config, err := admin.ClientConfigDomain(addr.Domain)
191 imapSSL, imapEncryption, err = tlsmode(config.IMAP.TLSMode)
194 submissionSSL, submissionEncryption, err = tlsmode(config.Submission.TLSMode)
197 http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
201 // The docs are generated and fragmented in many tiny pages, hard to follow.
202 // High-level starting point, https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/78530279-d042-4eb0-a1f4-03b18143cd19
203 // Request: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/2096fab2-9c3c-40b9-b123-edf6e8d55a9b
204 // Response, protocol: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/f4238db6-a983-435c-807a-b4b4a624c65b
205 // It appears autodiscover does not allow specifying SCRAM-SHA-256 as
206 // authentication method, or any authentication method that real clients actually
208 // https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726
210 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
212 // todo: let user configure they prefer or require tls client auth and add "AuthPackage" with value "certificate" to Protocol? see https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726
214 resp := autodiscoverResponse{}
215 resp.XMLName.Local = "Autodiscover"
216 resp.XMLName.Space = "http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"
217 resp.Response.XMLName.Local = "Response"
218 resp.Response.XMLName.Space = "http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"
219 resp.Response.Account = autodiscoverAccount{
220 AccountType: "email",
222 Protocol: []autodiscoverProtocol{
225 Server: config.IMAP.Host.ASCII,
226 Port: config.IMAP.Port,
227 LoginName: req.Request.EmailAddress,
229 Encryption: imapEncryption,
230 SPA: "off", // Override default "on", this is Microsofts proprietary authentication protocol.
235 Server: config.Submission.Host.ASCII,
236 Port: config.Submission.Port,
237 LoginName: req.Request.EmailAddress,
239 Encryption: submissionEncryption,
240 SPA: "off", // Override default "on", this is Microsofts proprietary authentication protocol.
245 enc := xml.NewEncoder(w)
247 fmt.Fprint(w, xml.Header)
248 if err := enc.Encode(resp); err != nil {
249 log.Errorx("marshal autodiscover response", err)
253// Thunderbird requests these URLs for autoconfig/autodiscover:
254// https://autoconfig.example.org/mail/config-v1.1.xml?emailaddress=user%40example.org
255// https://autodiscover.example.org/autodiscover/autodiscover.xml
256// https://example.org/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=user%40example.org
257// https://example.org/autodiscover/autodiscover.xml
258type autoconfigResponse struct {
259 XMLName xml.Name `xml:"clientConfig"`
260 Version string `xml:"version,attr"`
262 EmailProvider struct {
263 ID string `xml:"id,attr"`
264 Domain string `xml:"domain"`
265 DisplayName string `xml:"displayName"`
266 DisplayShortName string `xml:"displayShortName"`
268 IncomingServer struct {
269 Type string `xml:"type,attr"`
270 Hostname string `xml:"hostname"`
271 Port int `xml:"port"`
272 SocketType string `xml:"socketType"`
273 Username string `xml:"username"`
274 Authentication string `xml:"authentication"`
275 } `xml:"incomingServer"`
277 OutgoingServer struct {
278 Type string `xml:"type,attr"`
279 Hostname string `xml:"hostname"`
280 Port int `xml:"port"`
281 SocketType string `xml:"socketType"`
282 Username string `xml:"username"`
283 Authentication string `xml:"authentication"`
284 } `xml:"outgoingServer"`
285 } `xml:"emailProvider"`
287 ClientConfigUpdate struct {
288 URL string `xml:"url,attr"`
289 } `xml:"clientConfigUpdate"`
292type autodiscoverRequest struct {
293 XMLName xml.Name `xml:"Autodiscover"`
295 EmailAddress string `xml:"EMailAddress"`
296 AcceptableResponseSchema string `xml:"AcceptableResponseSchema"`
300type autodiscoverResponse struct {
304 Account autodiscoverAccount
308type autodiscoverAccount struct {
311 Protocol []autodiscoverProtocol
314type autodiscoverProtocol struct {
322 Encryption string `xml:",omitempty"`
327// Serve a .mobileconfig file. This endpoint is not a standard place where Apple
328// devices look. We point to it from the account page.
329func mobileconfigHandle(w http.ResponseWriter, r *http.Request) {
330 if r.Method != "GET" {
331 http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
334 addresses := r.FormValue("addresses")
335 fullName := r.FormValue("name")
339 err = fmt.Errorf("missing/empty field addresses")
341 l := strings.Split(addresses, ",")
343 buf, err = MobileConfig(l, fullName)
346 http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
351 filename = strings.ReplaceAll(filename, ".", "-")
352 filename = strings.ReplaceAll(filename, "@", "-at-")
353 filename = "email-account-" + filename + ".mobileconfig"
354 h.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
358// Serve a png file with qrcode with the link to the .mobileconfig file, should be
359// helpful for mobile devices.
360func mobileconfigQRCodeHandle(w http.ResponseWriter, r *http.Request) {
361 if r.Method != "GET" {
362 http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
365 if !strings.HasSuffix(r.URL.Path, ".qrcode.png") {
370 // Compose URL, scheme and host are not set.
378 u.Path = strings.TrimSuffix(u.Path, ".qrcode.png")
380 code, err := qr.Encode(u.String(), qr.L)
382 http.Error(w, "500 - internal server error - generating qr-code: "+err.Error(), http.StatusInternalServerError)
386 h.Set("Content-Type", "image/png")