mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:04:39 +03:00
make setting up apple mail clients easier by providing .mobileconfig device management profiles
including showing a qr code to easily get the file on iphones. the profile is currently in the "account" page. idea by x8x in issue #65
This commit is contained in:
227
http/autoconf.go
227
http/autoconf.go
@ -4,11 +4,12 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"rsc.io/qr"
|
||||
|
||||
"github.com/mjl-/mox/config"
|
||||
"github.com/mjl-/mox/mlog"
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/smtp"
|
||||
@ -36,7 +37,9 @@ var (
|
||||
// - Thunderbird will request an "autoconfig" xml file.
|
||||
// - Microsoft tools will request an "autodiscovery" xml file.
|
||||
// - In my tests on an internal domain, iOS mail only talks to Apple servers, then
|
||||
// does not attempt autoconfiguration. Possibly due to them being private DNS names.
|
||||
// does not attempt autoconfiguration. Possibly due to them being private DNS
|
||||
// names. Apple software can be provisioned with "mobileconfig" profile files,
|
||||
// which users can download after logging in.
|
||||
//
|
||||
// DNS records seem optional, but autoconfig.<domain> and autodiscover.<domain>
|
||||
// (both CNAME or A) are useful, and so is SRV _autodiscovery._tcp.<domain> 0 0 443
|
||||
@ -67,13 +70,31 @@ func autoconfHandle(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := mox.Conf.Domain(addr.Domain); !ok {
|
||||
http.Error(w, "400 - bad request - unknown domain", http.StatusBadRequest)
|
||||
socketType := func(tlsMode mox.TLSMode) (string, error) {
|
||||
switch tlsMode {
|
||||
case mox.TLSModeImmediate:
|
||||
return "SSL", nil
|
||||
case mox.TLSModeSTARTTLS:
|
||||
return "STARTTLS", nil
|
||||
case mox.TLSModeNone:
|
||||
return "plain", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown tls mode %v", tlsMode)
|
||||
}
|
||||
}
|
||||
|
||||
var imapTLS, submissionTLS string
|
||||
config, err := mox.ClientConfigDomain(addr.Domain)
|
||||
if err == nil {
|
||||
imapTLS, err = socketType(config.IMAP.TLSMode)
|
||||
}
|
||||
if err == nil {
|
||||
submissionTLS, err = socketType(config.Submission.TLSMode)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
addrDom = addr.Domain.Name()
|
||||
|
||||
hostname := mox.Conf.Static.HostnameDomain
|
||||
|
||||
// Thunderbird doesn't seem to allow U-labels, always return ASCII names.
|
||||
var resp autoconfigResponse
|
||||
@ -83,64 +104,24 @@ func autoconfHandle(w http.ResponseWriter, r *http.Request) {
|
||||
resp.EmailProvider.DisplayName = email
|
||||
resp.EmailProvider.DisplayShortName = addr.Domain.ASCII
|
||||
|
||||
var imapPort int
|
||||
var imapSocket string
|
||||
for _, l := range mox.Conf.Static.Listeners {
|
||||
if l.IMAPS.Enabled {
|
||||
imapSocket = "SSL"
|
||||
imapPort = config.Port(l.IMAPS.Port, 993)
|
||||
} else if l.IMAP.Enabled {
|
||||
if l.TLS != nil && imapSocket != "SSL" {
|
||||
imapSocket = "STARTTLS"
|
||||
imapPort = config.Port(l.IMAP.Port, 143)
|
||||
} else if imapSocket == "" {
|
||||
imapSocket = "plain"
|
||||
imapPort = config.Port(l.IMAP.Port, 143)
|
||||
}
|
||||
}
|
||||
}
|
||||
if imapPort == 0 {
|
||||
log.Error("autoconfig: no imap configured?")
|
||||
}
|
||||
|
||||
// todo: specify SCRAM-SHA-256 once thunderbird and autoconfig supports it. or perhaps that will fall under "password-encrypted" by then.
|
||||
|
||||
resp.EmailProvider.IncomingServer.Type = "imap"
|
||||
resp.EmailProvider.IncomingServer.Hostname = hostname.ASCII
|
||||
resp.EmailProvider.IncomingServer.Port = imapPort
|
||||
resp.EmailProvider.IncomingServer.SocketType = imapSocket
|
||||
resp.EmailProvider.IncomingServer.Hostname = config.IMAP.Host.ASCII
|
||||
resp.EmailProvider.IncomingServer.Port = config.IMAP.Port
|
||||
resp.EmailProvider.IncomingServer.SocketType = imapTLS
|
||||
resp.EmailProvider.IncomingServer.Username = email
|
||||
resp.EmailProvider.IncomingServer.Authentication = "password-encrypted"
|
||||
|
||||
var smtpPort int
|
||||
var smtpSocket string
|
||||
for _, l := range mox.Conf.Static.Listeners {
|
||||
if l.Submissions.Enabled {
|
||||
smtpSocket = "SSL"
|
||||
smtpPort = config.Port(l.Submissions.Port, 465)
|
||||
} else if l.Submission.Enabled {
|
||||
if l.TLS != nil && smtpSocket != "SSL" {
|
||||
smtpSocket = "STARTTLS"
|
||||
smtpPort = config.Port(l.Submission.Port, 587)
|
||||
} else if smtpSocket == "" {
|
||||
smtpSocket = "plain"
|
||||
smtpPort = config.Port(l.Submission.Port, 587)
|
||||
}
|
||||
}
|
||||
}
|
||||
if smtpPort == 0 {
|
||||
log.Error("autoconfig: no smtp submission configured?")
|
||||
}
|
||||
|
||||
resp.EmailProvider.OutgoingServer.Type = "smtp"
|
||||
resp.EmailProvider.OutgoingServer.Hostname = hostname.ASCII
|
||||
resp.EmailProvider.OutgoingServer.Port = smtpPort
|
||||
resp.EmailProvider.OutgoingServer.SocketType = smtpSocket
|
||||
resp.EmailProvider.OutgoingServer.Hostname = config.Submission.Host.ASCII
|
||||
resp.EmailProvider.OutgoingServer.Port = config.Submission.Port
|
||||
resp.EmailProvider.OutgoingServer.SocketType = submissionTLS
|
||||
resp.EmailProvider.OutgoingServer.Username = email
|
||||
resp.EmailProvider.OutgoingServer.Authentication = "password-encrypted"
|
||||
|
||||
// todo: should we put the email address in the URL?
|
||||
resp.ClientConfigUpdate.URL = fmt.Sprintf("https://%s/mail/config-v1.1.xml", hostname.ASCII)
|
||||
resp.ClientConfigUpdate.URL = fmt.Sprintf("https://autoconfig.%s/mail/config-v1.1.xml", addr.Domain.ASCII)
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
enc := xml.NewEncoder(w)
|
||||
@ -188,13 +169,33 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := mox.Conf.Domain(addr.Domain); !ok {
|
||||
http.Error(w, "400 - bad request - unknown domain", http.StatusBadRequest)
|
||||
// tlsmode returns the "ssl" and "encryption" fields.
|
||||
tlsmode := func(tlsMode mox.TLSMode) (string, string, error) {
|
||||
switch tlsMode {
|
||||
case mox.TLSModeImmediate:
|
||||
return "on", "TLS", nil
|
||||
case mox.TLSModeSTARTTLS:
|
||||
return "on", "", nil
|
||||
case mox.TLSModeNone:
|
||||
return "off", "", nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("unknown tls mode %v", tlsMode)
|
||||
}
|
||||
}
|
||||
|
||||
var imapSSL, imapEncryption string
|
||||
var submissionSSL, submissionEncryption string
|
||||
config, err := mox.ClientConfigDomain(addr.Domain)
|
||||
if err == nil {
|
||||
imapSSL, imapEncryption, err = tlsmode(config.IMAP.TLSMode)
|
||||
}
|
||||
if err == nil {
|
||||
submissionSSL, submissionEncryption, err = tlsmode(config.Submission.TLSMode)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
addrDom = addr.Domain.Name()
|
||||
|
||||
hostname := mox.Conf.Static.HostnameDomain
|
||||
|
||||
// The docs are generated and fragmented in many tiny pages, hard to follow.
|
||||
// High-level starting point, https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/78530279-d042-4eb0-a1f4-03b18143cd19
|
||||
@ -205,47 +206,6 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||
// use. See
|
||||
// https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726
|
||||
|
||||
var imapPort int
|
||||
imapSSL := "off"
|
||||
var imapEncryption string
|
||||
|
||||
var smtpPort int
|
||||
smtpSSL := "off"
|
||||
var smtpEncryption string
|
||||
for _, l := range mox.Conf.Static.Listeners {
|
||||
if l.IMAPS.Enabled {
|
||||
imapPort = config.Port(l.IMAPS.Port, 993)
|
||||
imapSSL = "on"
|
||||
imapEncryption = "TLS" // Assuming this means direct TLS.
|
||||
} else if l.IMAP.Enabled {
|
||||
if l.TLS != nil && imapEncryption != "TLS" {
|
||||
imapSSL = "on"
|
||||
imapPort = config.Port(l.IMAP.Port, 143)
|
||||
} else if imapSSL == "" {
|
||||
imapPort = config.Port(l.IMAP.Port, 143)
|
||||
}
|
||||
}
|
||||
|
||||
if l.Submissions.Enabled {
|
||||
smtpPort = config.Port(l.Submissions.Port, 465)
|
||||
smtpSSL = "on"
|
||||
smtpEncryption = "TLS" // Assuming this means direct TLS.
|
||||
} else if l.Submission.Enabled {
|
||||
if l.TLS != nil && smtpEncryption != "TLS" {
|
||||
smtpSSL = "on"
|
||||
smtpPort = config.Port(l.Submission.Port, 587)
|
||||
} else if smtpSSL == "" {
|
||||
smtpPort = config.Port(l.Submission.Port, 587)
|
||||
}
|
||||
}
|
||||
}
|
||||
if imapPort == 0 {
|
||||
log.Error("autoconfig: no smtp submission configured?")
|
||||
}
|
||||
if smtpPort == 0 {
|
||||
log.Error("autoconfig: no imap configured?")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||
|
||||
resp := autodiscoverResponse{}
|
||||
@ -259,8 +219,8 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||
Protocol: []autodiscoverProtocol{
|
||||
{
|
||||
Type: "IMAP",
|
||||
Server: hostname.ASCII,
|
||||
Port: imapPort,
|
||||
Server: config.IMAP.Host.ASCII,
|
||||
Port: config.IMAP.Port,
|
||||
LoginName: req.Request.EmailAddress,
|
||||
SSL: imapSSL,
|
||||
Encryption: imapEncryption,
|
||||
@ -269,11 +229,11 @@ func autodiscoverHandle(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
{
|
||||
Type: "SMTP",
|
||||
Server: hostname.ASCII,
|
||||
Port: smtpPort,
|
||||
Server: config.Submission.Host.ASCII,
|
||||
Port: config.Submission.Port,
|
||||
LoginName: req.Request.EmailAddress,
|
||||
SSL: smtpSSL,
|
||||
Encryption: smtpEncryption,
|
||||
SSL: submissionSSL,
|
||||
Encryption: submissionEncryption,
|
||||
SPA: "off", // Override default "on", this is Microsofts proprietary authentication protocol.
|
||||
AuthRequired: "on",
|
||||
},
|
||||
@ -360,3 +320,58 @@ type autodiscoverProtocol struct {
|
||||
SPA string
|
||||
AuthRequired string
|
||||
}
|
||||
|
||||
// Serve a .mobileconfig file. This endpoint is not a standard place where Apple
|
||||
// devices look. We point to it from the account page.
|
||||
func mobileconfigHandle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
address := r.FormValue("address")
|
||||
fullName := r.FormValue("name")
|
||||
buf, err := MobileConfig(address, fullName)
|
||||
if err != nil {
|
||||
http.Error(w, "400 - bad request - "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
h := w.Header()
|
||||
filename := address
|
||||
filename = strings.ReplaceAll(filename, ".", "-")
|
||||
filename = strings.ReplaceAll(filename, "@", "-at-")
|
||||
filename = "email-account-" + filename + ".mobileconfig"
|
||||
h.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
// Serve a png file with qrcode with the link to the .mobileconfig file, should be
|
||||
// helpful for mobile devices.
|
||||
func mobileconfigQRCodeHandle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "405 - method not allowed - get required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(r.URL.Path, ".qrcode.png") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Compose URL, scheme and host are not set.
|
||||
u := *r.URL
|
||||
if r.TLS == nil {
|
||||
u.Scheme = "http"
|
||||
} else {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
u.Host = r.Host
|
||||
u.Path = strings.TrimSuffix(u.Path, ".qrcode.png")
|
||||
|
||||
code, err := qr.Encode(u.String(), qr.L)
|
||||
if err != nil {
|
||||
http.Error(w, "500 - internal server error - generating qr-code: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "image/png")
|
||||
w.Write(code.PNG())
|
||||
}
|
||||
|
200
http/mobileconfig.go
Normal file
200
http/mobileconfig.go
Normal file
@ -0,0 +1,200 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/mjl-/mox/mox-"
|
||||
"github.com/mjl-/mox/smtp"
|
||||
)
|
||||
|
||||
// Apple software isn't good at autoconfig/autodiscovery, but it can import a
|
||||
// device management profile containing account settings.
|
||||
//
|
||||
// See https://developer.apple.com/documentation/devicemanagement/mail.
|
||||
type deviceManagementProfile struct {
|
||||
XMLName xml.Name `xml:"plist"`
|
||||
Version string `xml:"version,attr"`
|
||||
Dict dict `xml:"dict"`
|
||||
}
|
||||
|
||||
type array []dict
|
||||
|
||||
type dict map[string]any
|
||||
|
||||
// MarshalXML marshals as <dict> with multiple pairs of <key> and a value of various types.
|
||||
func (m dict) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
// The plist format isn't that easy to generate with Go's xml package, it's leaving
|
||||
// out reasonable structure, instead just concatenating key/value pairs. Perhaps
|
||||
// there is a better way?
|
||||
|
||||
if err := e.EncodeToken(xml.StartElement{Name: xml.Name{Local: "dict"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
l := maps.Keys(m)
|
||||
sort.Strings(l)
|
||||
for _, k := range l {
|
||||
tokens := []xml.Token{
|
||||
xml.StartElement{Name: xml.Name{Local: "key"}},
|
||||
xml.CharData([]byte(k)),
|
||||
xml.EndElement{Name: xml.Name{Local: "key"}},
|
||||
}
|
||||
for _, t := range tokens {
|
||||
if err := e.EncodeToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tokens = nil
|
||||
|
||||
switch v := m[k].(type) {
|
||||
case string:
|
||||
tokens = []xml.Token{
|
||||
xml.StartElement{Name: xml.Name{Local: "string"}},
|
||||
xml.CharData([]byte(v)),
|
||||
xml.EndElement{Name: xml.Name{Local: "string"}},
|
||||
}
|
||||
case int:
|
||||
tokens = []xml.Token{
|
||||
xml.StartElement{Name: xml.Name{Local: "integer"}},
|
||||
xml.CharData([]byte(fmt.Sprintf("%d", v))),
|
||||
xml.EndElement{Name: xml.Name{Local: "integer"}},
|
||||
}
|
||||
case bool:
|
||||
tag := "false"
|
||||
if v {
|
||||
tag = "true"
|
||||
}
|
||||
tokens = []xml.Token{
|
||||
xml.StartElement{Name: xml.Name{Local: tag}},
|
||||
xml.EndElement{Name: xml.Name{Local: tag}},
|
||||
}
|
||||
case array:
|
||||
if err := e.EncodeToken(xml.StartElement{Name: xml.Name{Local: "array"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range v {
|
||||
if err := d.MarshalXML(e, xml.StartElement{Name: xml.Name{Local: "array"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "array"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unexpected dict value of type %T", v)
|
||||
}
|
||||
for _, t := range tokens {
|
||||
if err := e.EncodeToken(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := e.EncodeToken(xml.EndElement{Name: xml.Name{Local: "dict"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MobileConfig returns a device profile for a macOS Mail email account. The file
|
||||
// should have a .mobileconfig extension. Opening the file adds it to Profiles in
|
||||
// System Preferences, where it can be installed. This profile does not contain a
|
||||
// password because sending opaque files containing passwords around to users seems
|
||||
// like bad security practice.
|
||||
//
|
||||
// The config is not signed, so users must ignore warnings about unsigned profiles.
|
||||
func MobileConfig(address, fullName string) ([]byte, error) {
|
||||
addr, err := smtp.ParseAddress(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing address: %v", err)
|
||||
}
|
||||
|
||||
config, err := mox.ClientConfigDomain(addr.Domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting config for domain: %v", err)
|
||||
}
|
||||
|
||||
// Apple software wants identifiers...
|
||||
t := strings.Split(addr.Domain.Name(), ".")
|
||||
slices.Reverse(t)
|
||||
reverseAddr := strings.Join(t, ".") + "." + addr.Localpart.String()
|
||||
|
||||
// Apple software wants UUIDs... We generate them deterministically based on address
|
||||
// and our code (through key, which we must change if code changes).
|
||||
const key = "mox0"
|
||||
uuid := func(prefix string) string {
|
||||
mac := hmac.New(sha256.New, []byte(key))
|
||||
mac.Write([]byte(prefix + "\n" + "\n" + address))
|
||||
sum := mac.Sum(nil)
|
||||
uuid := fmt.Sprintf("%x-%x-%x-%x-%x", sum[0:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
|
||||
return uuid
|
||||
}
|
||||
|
||||
uuidConfig := uuid("config")
|
||||
uuidAccount := uuid("account")
|
||||
|
||||
// The "UseSSL" fields are underspecified in Apple's format. They say "If true,
|
||||
// enables SSL for authentication on the incoming mail server.". I'm assuming they
|
||||
// want to know if they should start immediately with a handshake, instead of
|
||||
// starting out plain. There is no way to require STARTTLS though. You could even
|
||||
// interpret their wording as this field enable authentication through client-side
|
||||
// TLS certificates, given their "on the incoming mail server", instead of "of the
|
||||
// incoming mail server".
|
||||
|
||||
var w bytes.Buffer
|
||||
p := deviceManagementProfile{
|
||||
Version: "1.0",
|
||||
Dict: dict(map[string]any{
|
||||
"PayloadDisplayName": fmt.Sprintf("%s email account", address),
|
||||
"PayloadIdentifier": reverseAddr + ".email",
|
||||
"PayloadType": "Configuration",
|
||||
"PayloadUUID": uuidConfig,
|
||||
"PayloadVersion": 1,
|
||||
"PayloadContent": array{
|
||||
dict(map[string]any{
|
||||
"EmailAccountDescription": address,
|
||||
"EmailAccountName": fullName,
|
||||
"EmailAccountType": "EmailTypeIMAP",
|
||||
"EmailAddress": address,
|
||||
"IncomingMailServerAuthentication": "EmailAuthCRAMMD5", // SCRAM not an option at time of writing..
|
||||
"IncomingMailServerUsername": address,
|
||||
"IncomingMailServerHostName": config.IMAP.Host.ASCII,
|
||||
"IncomingMailServerPortNumber": config.IMAP.Port,
|
||||
"IncomingMailServerUseSSL": config.IMAP.TLSMode == mox.TLSModeImmediate,
|
||||
"OutgoingMailServerAuthentication": "EmailAuthCRAMMD5", // SCRAM not an option at time of writing...
|
||||
"OutgoingMailServerHostName": config.Submission.Host.ASCII,
|
||||
"OutgoingMailServerPortNumber": config.Submission.Port,
|
||||
"OutgoingMailServerUsername": address,
|
||||
"OutgoingMailServerUseSSL": config.Submission.TLSMode == mox.TLSModeImmediate,
|
||||
"OutgoingPasswordSameAsIncomingPassword": true,
|
||||
"PayloadIdentifier": reverseAddr + ".email.account",
|
||||
"PayloadType": "com.apple.mail.managed",
|
||||
"PayloadUUID": uuidAccount,
|
||||
"PayloadVersion": 1,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}
|
||||
if _, err := fmt.Fprint(&w, xml.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := fmt.Fprint(&w, "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc := xml.NewEncoder(&w)
|
||||
enc.Indent("", "\t")
|
||||
if err := enc.Encode(p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := fmt.Fprintln(&w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w.Bytes(), nil
|
||||
}
|
12
http/web.go
12
http/web.go
@ -617,11 +617,19 @@ func Listen() {
|
||||
port := config.Port(l.AutoconfigHTTPS.Port, 443)
|
||||
srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https")
|
||||
autoconfigMatch := func(dom dns.Domain) bool {
|
||||
// todo: may want to check this against the configured domains, could in theory be just a webserver.
|
||||
return strings.HasPrefix(dom.ASCII, "autoconfig.")
|
||||
// Thunderbird requests an autodiscovery URL at the email address domain name, so
|
||||
// autoconfig prefix is optional.
|
||||
if strings.HasPrefix(dom.ASCII, "autoconfig.") {
|
||||
dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
|
||||
dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
|
||||
}
|
||||
_, ok := mox.Conf.Domain(dom)
|
||||
return ok
|
||||
}
|
||||
srv.Handle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", safeHeaders(http.HandlerFunc(autoconfHandle)))
|
||||
srv.Handle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", safeHeaders(http.HandlerFunc(autodiscoverHandle)))
|
||||
srv.Handle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", safeHeaders(http.HandlerFunc(mobileconfigHandle)))
|
||||
srv.Handle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", safeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
|
||||
}
|
||||
if l.MTASTSHTTPS.Enabled {
|
||||
port := config.Port(l.MTASTSHTTPS.Port, 443)
|
||||
|
Reference in New Issue
Block a user