1// Package http provides HTTP listeners/servers, for
2// autoconfiguration/autodiscovery, the account and admin web interface and
27 "golang.org/x/net/http2"
29 "github.com/prometheus/client_golang/prometheus"
30 "github.com/prometheus/client_golang/prometheus/promauto"
31 "github.com/prometheus/client_golang/prometheus/promhttp"
33 "github.com/mjl-/mox/autotls"
34 "github.com/mjl-/mox/config"
35 "github.com/mjl-/mox/dns"
36 "github.com/mjl-/mox/imapserver"
37 "github.com/mjl-/mox/mlog"
38 "github.com/mjl-/mox/mox-"
39 "github.com/mjl-/mox/ratelimit"
40 "github.com/mjl-/mox/smtpserver"
41 "github.com/mjl-/mox/webaccount"
42 "github.com/mjl-/mox/webadmin"
43 "github.com/mjl-/mox/webapisrv"
44 "github.com/mjl-/mox/webmail"
47var pkglog = mlog.New("http", nil)
50 // metricRequest tracks performance (time to write response header) of server.
51 metricRequest = promauto.NewHistogramVec(
52 prometheus.HistogramOpts{
53 Name: "mox_httpserver_request_duration_seconds",
54 Help: "HTTP(s) server request with handler name, protocol, method, result codes, and duration until response status code is written, in seconds.",
55 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
58 "handler", // Name from webhandler, can be empty.
59 "proto", // "http", "https", "ws", "wss"
60 "method", // "(unknown)" and otherwise only common verbs
64 // metricResponse tracks performance of entire request as experienced by users,
65 // which also depends on their connection speed, so not necessarily something you
67 metricResponse = promauto.NewHistogramVec(
68 prometheus.HistogramOpts{
69 Name: "mox_httpserver_response_duration_seconds",
70 Help: "HTTP(s) server response with handler name, protocol, method, result codes, and duration of entire response, in seconds.",
71 Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.100, 0.5, 1, 5, 10, 20, 30, 60, 120},
74 "handler", // Name from webhandler, can be empty.
75 "proto", // "http", "https", "ws", "wss"
76 "method", // "(unknown)" and otherwise only common verbs
82// We serve a favicon when webaccount/webmail/webadmin/webapi for account-related
83// domains. They are configured as "service handler", which have a lower priority
84// than web handler. Admins can configure a custom /favicon.ico route to override
85// the builtin favicon. In the future, we may want to make it easier to customize
86// the favicon, possibly per client settings domain.
90var faviconModTime = time.Now()
93 p, err := os.Executable()
95 if st, err := os.Stat(p); err == nil {
96 faviconModTime = st.ModTime()
101func faviconHandle(w http.ResponseWriter, r *http.Request) {
102 http.ServeContent(w, r, "favicon.ico", faviconModTime, strings.NewReader(faviconIco))
105type responseWriterFlusher interface {
110// http.ResponseWriter that writes access log and tracks metrics at end of response.
111type loggingWriter struct {
112 W responseWriterFlusher // Calls are forwarded.
115 WebsocketRequest bool // Whether request from was websocket.
123 Size int64 // Of data served to client, for non-websocket responses.
124 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
125 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
127 WebsocketResponse bool // If this was a successful websocket connection with backend.
128 SizeFromClient, SizeToClient int64 // Websocket data.
129 Attrs []slog.Attr // Additional fields to log.
132func (w *loggingWriter) AddAttr(a slog.Attr) {
133 w.Attrs = append(w.Attrs, a)
136func (w *loggingWriter) Flush() {
140func (w *loggingWriter) Header() http.Header {
144// protocol, for logging.
145func (w *loggingWriter) proto(websocket bool) string {
156func (w *loggingWriter) Write(buf []byte) (int, error) {
157 if w.StatusCode == 0 {
158 w.WriteHeader(http.StatusOK)
164 n, err = w.W.Write(buf)
169 // We flush after each write. Probably takes a few more bytes, but prevents any
170 // issues due to buffering.
171 // w.Gzip.Write updates w.Size with the compressed byte count.
172 n, err = w.Gzip.Write(buf)
177 w.UncompressedSize += int64(n)
186func (w *loggingWriter) setStatusCode(statusCode int) {
187 if w.StatusCode != 0 {
191 w.StatusCode = statusCode
192 method := metricHTTPMethod(w.R.Method)
193 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
196// SetUncompressedSize is used through an interface by
197// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
198func (w *loggingWriter) SetUncompressedSize(origSize int64) {
199 w.UncompressedSize = origSize
202func (w *loggingWriter) WriteHeader(statusCode int) {
203 if w.StatusCode != 0 {
207 w.setStatusCode(statusCode)
209 // We transparently gzip-compress responses for requests under these conditions, all must apply:
211 // - Enabled for handler (static handlers make their own decisions).
212 // - Not a websocket request.
213 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
214 // - Not already compressed, or any other Content-Encoding header (including "identity").
215 // - Client accepts gzip encoded responses.
216 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
217 if w.Compress && !w.WebsocketRequest && statusCode == http.StatusOK && w.W.Header().Values("Content-Encoding") == nil && acceptsGzip(w.R) && compressibleContentType(w.W.Header().Get("Content-Type")) {
218 // todo: we should gather the first kb of data, see if it is compressible. if not, just return original. should set timer so we flush if it takes too long to gather 1kb. for smaller data we shouldn't compress at all.
220 // We track the gzipped output for the access log.
221 cw := countWriter{Writer: w.W, Size: &w.Size}
222 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
223 w.W.Header().Set("Content-Encoding", "gzip")
224 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
226 w.W.WriteHeader(statusCode)
229func acceptsGzip(r *http.Request) bool {
230 s := r.Header.Get("Accept-Encoding")
231 t := strings.Split(s, ",")
232 for _, e := range t {
233 e = strings.TrimSpace(e)
234 tt := strings.Split(e, ";")
235 if len(tt) > 1 && t[1] == "q=0" {
245var compressibleTypes = map[string]bool{
246 "application/csv": true,
247 "application/javascript": true,
248 "application/json": true,
249 "application/x-javascript": true,
250 "application/xml": true,
251 "image/vnd.microsoft.icon": true,
252 "image/x-icon": true,
256 "font/opentype": true,
259func compressibleContentType(ct string) bool {
260 ct = strings.SplitN(ct, ";", 2)[0]
261 ct = strings.TrimSpace(ct)
262 ct = strings.ToLower(ct)
263 if compressibleTypes[ct] {
266 t, st, _ := strings.Cut(ct, "/")
267 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
270func compressibleContent(f *os.File) bool {
271 // We don't want to store many small files. They take up too much disk overhead.
272 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
276 buf := make([]byte, 512)
277 n, err := f.ReadAt(buf, 0)
278 if err != nil && err != io.EOF {
281 ct := http.DetectContentType(buf[:n])
282 return compressibleContentType(ct)
285type countWriter struct {
290func (w countWriter) Write(buf []byte) (int, error) {
291 n, err := w.Writer.Write(buf)
298var tlsVersions = map[uint16]string{
299 tls.VersionTLS10: "tls1.0",
300 tls.VersionTLS11: "tls1.1",
301 tls.VersionTLS12: "tls1.2",
302 tls.VersionTLS13: "tls1.3",
305func metricHTTPMethod(method string) string {
306 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
307 method = strings.ToLower(method)
309 case "acl", "baseline-control", "bind", "checkin", "checkout", "connect", "copy", "delete", "get", "head", "label", "link", "lock", "merge", "mkactivity", "mkcalendar", "mkcol", "mkredirectref", "mkworkspace", "move", "options", "orderpatch", "patch", "post", "pri", "propfind", "proppatch", "put", "rebind", "report", "search", "trace", "unbind", "uncheckout", "unlink", "unlock", "update", "updateredirectref", "version-control":
315func (w *loggingWriter) error(err error) {
321func (w *loggingWriter) Done() {
322 if w.Err == nil && w.Gzip != nil {
323 if err := w.Gzip.Close(); err != nil {
328 method := metricHTTPMethod(w.R.Method)
329 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
333 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
341 err = w.R.Context().Err()
343 attrs := []slog.Attr{
344 slog.String("httpaccess", ""),
345 slog.String("handler", w.Handler),
346 slog.String("method", method),
347 slog.Any("url", w.R.URL),
348 slog.String("host", w.R.Host),
349 slog.Duration("duration", time.Since(w.Start)),
350 slog.Int("statuscode", w.StatusCode),
351 slog.String("proto", strings.ToLower(w.R.Proto)),
352 slog.Any("remoteaddr", w.R.RemoteAddr),
353 slog.String("tlsinfo", tlsinfo),
354 slog.String("useragent", w.R.Header.Get("User-Agent")),
355 slog.String("referrr", w.R.Header.Get("Referrer")),
357 if w.WebsocketRequest {
358 attrs = append(attrs,
359 slog.Bool("websocketrequest", true),
362 if w.WebsocketResponse {
363 attrs = append(attrs,
364 slog.Bool("websocket", true),
365 slog.Int64("sizetoclient", w.SizeToClient),
366 slog.Int64("sizefromclient", w.SizeFromClient),
368 } else if w.UncompressedSize > 0 {
369 attrs = append(attrs,
370 slog.Int64("size", w.Size),
371 slog.Int64("uncompressedsize", w.UncompressedSize),
374 attrs = append(attrs,
375 slog.Int64("size", w.Size),
378 attrs = append(attrs, w.Attrs...)
379 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
382// Built-in handlers, e.g. mta-sts and autoconfig.
383type pathHandler struct {
384 Name string // For logging/metrics.
385 HostMatch func(host dns.IPDomain) bool // If not nil, called to see if domain of requests matches. Host can be zero value for invalid domain/ip.
386 Path string // Path to register, like on http.ServeMux.
390 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
391 TLSConfig *tls.Config
392 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
395 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
396 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
397 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
398 // interfaces) last. WebHandlers can also pass requests to the internal servers.
399 // This order allows admins to serve other content on domains serving the mox.conf
400 // internal services.
401 SystemHandlers []pathHandler // Sorted, longest first.
403 ServiceHandlers []pathHandler // Sorted, longest first.
406// SystemHandle registers a named system handler for a path and optional host. If
407// path ends with a slash, it is used as prefix match, otherwise a full path match
408// is required. If hostOpt is set, only requests to those host are handled by this
410func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
411 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
414// Like SystemHandle, but for internal services "admin", "account", "webmail",
415// "webapi" configured in the mox.conf Listener.
416func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
417 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
421 limiterConnectionrate = &ratelimit.Limiter{
422 WindowLimits: []ratelimit.WindowLimit{
425 Limits: [...]int64{1000, 3000, 9000},
429 Limits: [...]int64{5000, 15000, 45000},
435// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
436// right pathHandler or WebHandler, and it generates access logs and tracks
438func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
440 // Rate limiting as early as possible.
441 ipstr, _, err := net.SplitHostPort(r.RemoteAddr)
443 pkglog.Debugx("split host:port client remoteaddr", err, slog.Any("remoteaddr", r.RemoteAddr))
444 } else if ip := net.ParseIP(ipstr); ip == nil {
445 pkglog.Debug("parsing ip for client remoteaddr", slog.Any("remoteaddr", r.RemoteAddr))
446 } else if !limiterConnectionrate.Add(ip, now, 1) {
447 method := metricHTTPMethod(r.Method)
452 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
453 // No logging, that's just noise.
455 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
459 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
460 r = r.WithContext(ctx)
462 wf, ok := xw.(responseWriterFlusher)
464 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
468 nw := &loggingWriter{
475 // Cleanup path, removing ".." and ".". Keep any trailing slash.
476 trailingPath := strings.HasSuffix(r.URL.Path, "/")
477 if r.URL.Path == "" {
480 r.URL.Path = path.Clean(r.URL.Path)
481 if r.URL.Path == "." {
484 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
489 nhost, _, err := net.SplitHostPort(host)
493 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
495 dom, domErr := dns.ParseDomain(host)
497 ipdom = dns.IPDomain{Domain: dom}
501 handle := func(h pathHandler) bool {
502 if h.HostMatch != nil && !h.HostMatch(ipdom) {
505 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
508 h.Handler.ServeHTTP(nw, r)
514 for _, h := range s.SystemHandlers {
520 if WebHandle(nw, r, ipdom) {
524 for _, h := range s.ServiceHandlers {
529 nw.Handler = "(nomatch)"
533func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
534 // Helpfully redirect user to version with ending slash.
535 if path != "/" && strings.HasSuffix(path, "/") {
536 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
537 http.Redirect(w, r, path, http.StatusSeeOther)
539 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
543// Listen binds to sockets for HTTP listeners, including those required for ACME to
544// generate TLS certificates. It stores the listeners so Serve can start serving them.
546 // Initialize listeners in deterministic order for the same potential error
548 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
549 for _, name := range names {
550 l := mox.Conf.Static.Listeners[name]
551 portServe := portServes(name, l)
553 ports := slices.Sorted(maps.Keys(portServe))
554 for _, port := range ports {
555 srv := portServe[port]
556 for _, ip := range l.IPs {
557 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, srv.NextProto)
563func portServes(name string, l config.Listener) map[int]*serve {
564 portServe := map[int]*serve{}
566 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
567 localhost := dns.Domain{ASCII: "localhost"}
569 ldom := l.HostnameDomain
570 if l.Hostname == "" {
571 ldom = mox.Conf.Static.HostnameDomain
573 listenerHostMatch := func(host dns.IPDomain) bool {
577 return host.Domain == ldom || host.Domain == localhost
579 accountHostMatch := func(host dns.IPDomain) bool {
580 if listenerHostMatch(host) {
583 return mox.Conf.IsClientSettingsDomain(host.Domain)
586 var ensureServe func(https bool, port int, kind string, favicon bool) *serve
587 ensureServe = func(https bool, port int, kind string, favicon bool) *serve {
590 s = &serve{nil, nil, tlsNextProtoMap{}, false, nil, false, nil}
593 s.Kinds = append(s.Kinds, kind)
594 if favicon && !s.Favicon {
595 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
599 // We clone TLS configs because we may modify it later on for this server, for
600 // ALPN. And we need copies because multiple listeners on http.Server where the
601 // config is used will try to modify it concurrently.
602 if https && l.TLS.ACME != "" {
603 s.TLSConfig = l.TLS.ACMEConfig.Clone()
605 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
606 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
607 ensureServe(true, tlsport, "acme-tls-alpn-01", false)
610 s.TLSConfig = l.TLS.Config.Clone()
615 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
616 // yet, add http-01 validation mechanism handler to server.
617 ensureACMEHTTP01 := func(srv *serve) {
618 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
619 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
620 srv.Kinds = append(srv.Kinds, "acme-http-01")
621 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
625 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
626 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
627 ensureServe(true, port, "acme-tls-alpn-01", false)
629 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
630 s := ensureServe(true, 443, "smtp-https", false)
631 hostname := mox.Conf.Static.HostnameDomain
632 if l.Hostname != "" {
633 hostname = l.HostnameDomain
636 maxMsgSize := l.SMTPMaxMessageSize
638 maxMsgSize = config.DefaultMaxMsgSize
640 requireTLS := !l.SMTP.NoRequireTLS
642 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
643 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
646 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
647 s := ensureServe(true, 443, "imap-https", false)
648 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
649 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
652 if l.AccountHTTP.Enabled {
653 port := config.Port(l.AccountHTTP.Port, 80)
655 if l.AccountHTTP.Path != "" {
656 path = l.AccountHTTP.Path
658 srv := ensureServe(false, port, "account-http at "+path, true)
659 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
660 srv.ServiceHandle("account", accountHostMatch, path, handler)
661 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
662 ensureACMEHTTP01(srv)
664 if l.AccountHTTPS.Enabled {
665 port := config.Port(l.AccountHTTPS.Port, 443)
667 if l.AccountHTTPS.Path != "" {
668 path = l.AccountHTTPS.Path
670 srv := ensureServe(true, port, "account-https at "+path, true)
671 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
672 srv.ServiceHandle("account", accountHostMatch, path, handler)
673 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
676 if l.AdminHTTP.Enabled {
677 port := config.Port(l.AdminHTTP.Port, 80)
679 if l.AdminHTTP.Path != "" {
680 path = l.AdminHTTP.Path
682 srv := ensureServe(false, port, "admin-http at "+path, true)
683 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
684 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
685 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
686 ensureACMEHTTP01(srv)
688 if l.AdminHTTPS.Enabled {
689 port := config.Port(l.AdminHTTPS.Port, 443)
691 if l.AdminHTTPS.Path != "" {
692 path = l.AdminHTTPS.Path
694 srv := ensureServe(true, port, "admin-https at "+path, true)
695 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
696 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
697 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
700 maxMsgSize := l.SMTPMaxMessageSize
702 maxMsgSize = config.DefaultMaxMsgSize
705 if l.WebAPIHTTP.Enabled {
706 port := config.Port(l.WebAPIHTTP.Port, 80)
708 if l.WebAPIHTTP.Path != "" {
709 path = l.WebAPIHTTP.Path
711 srv := ensureServe(false, port, "webapi-http at "+path, true)
712 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
713 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
714 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
715 ensureACMEHTTP01(srv)
717 if l.WebAPIHTTPS.Enabled {
718 port := config.Port(l.WebAPIHTTPS.Port, 443)
720 if l.WebAPIHTTPS.Path != "" {
721 path = l.WebAPIHTTPS.Path
723 srv := ensureServe(true, port, "webapi-https at "+path, true)
724 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
725 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
726 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
729 if l.WebmailHTTP.Enabled {
730 port := config.Port(l.WebmailHTTP.Port, 80)
732 if l.WebmailHTTP.Path != "" {
733 path = l.WebmailHTTP.Path
735 srv := ensureServe(false, port, "webmail-http at "+path, true)
736 var accountPath string
737 if l.AccountHTTP.Enabled {
739 if l.AccountHTTP.Path != "" {
740 accountPath = l.AccountHTTP.Path
743 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
744 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
745 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
746 ensureACMEHTTP01(srv)
748 if l.WebmailHTTPS.Enabled {
749 port := config.Port(l.WebmailHTTPS.Port, 443)
751 if l.WebmailHTTPS.Path != "" {
752 path = l.WebmailHTTPS.Path
754 srv := ensureServe(true, port, "webmail-https at "+path, true)
755 var accountPath string
756 if l.AccountHTTPS.Enabled {
758 if l.AccountHTTPS.Path != "" {
759 accountPath = l.AccountHTTPS.Path
762 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
763 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
764 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
767 if l.MetricsHTTP.Enabled {
768 port := config.Port(l.MetricsHTTP.Port, 8010)
769 srv := ensureServe(false, port, "metrics-http", false)
770 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
771 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
772 if r.URL.Path != "/" {
775 } else if r.Method != "GET" {
776 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
779 w.Header().Set("Content-Type", "text/html")
780 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
783 if l.AutoconfigHTTPS.Enabled {
784 port := config.Port(l.AutoconfigHTTPS.Port, 443)
785 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, port, "autoconfig-https", false)
786 if l.AutoconfigHTTPS.NonTLS {
787 ensureACMEHTTP01(srv)
789 autoconfigMatch := func(ipdom dns.IPDomain) bool {
794 // Thunderbird requests an autodiscovery URL at the email address domain name, so
795 // autoconfig prefix is optional.
796 if strings.HasPrefix(dom.ASCII, "autoconfig.") {
797 dom.ASCII = strings.TrimPrefix(dom.ASCII, "autoconfig.")
798 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
800 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
801 // use the mail server's host name.
802 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
805 dc, ok := mox.Conf.Domain(dom)
806 return ok && !dc.ReportsOnly
808 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
809 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
810 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
811 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
813 if l.MTASTSHTTPS.Enabled {
814 port := config.Port(l.MTASTSHTTPS.Port, 443)
815 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, port, "mtasts-https", false)
816 if l.MTASTSHTTPS.NonTLS {
817 ensureACMEHTTP01(srv)
819 mtastsMatch := func(ipdom dns.IPDomain) bool {
820 // todo: may want to check this against the configured domains, could in theory be just a webserver.
825 return strings.HasPrefix(dom.ASCII, "mta-sts.")
827 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
829 if l.PprofHTTP.Enabled {
830 // Importing net/http/pprof registers handlers on the default serve mux.
831 port := config.Port(l.PprofHTTP.Port, 8011)
832 if _, ok := portServe[port]; ok {
833 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
835 srv := &serve{[]string{"pprof-http"}, nil, nil, false, nil, false, nil}
836 portServe[port] = srv
837 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
839 if l.WebserverHTTP.Enabled {
840 port := config.Port(l.WebserverHTTP.Port, 80)
841 srv := ensureServe(false, port, "webserver-http", false)
843 ensureACMEHTTP01(srv)
845 if l.WebserverHTTPS.Enabled {
846 port := config.Port(l.WebserverHTTPS.Port, 443)
847 srv := ensureServe(true, port, "webserver-https", false)
851 if l.TLS != nil && l.TLS.ACME != "" {
852 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
853 if ensureManagerHosts[m] == nil {
854 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
856 hosts := ensureManagerHosts[m]
857 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
859 if l.HostnameDomain.ASCII != "" {
860 hosts[l.HostnameDomain] = struct{}{}
863 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
864 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
865 // for the timeouts of clients doing autoconfig.
867 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
868 for _, name := range mox.Conf.Domains() {
869 if dom, err := dns.ParseDomain(name); err != nil {
870 pkglog.Errorx("parsing domain from config", err)
871 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
872 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
876 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
878 pkglog.Errorx("parsing domain from config for autoconfig", err)
880 hosts[autoconfdom] = struct{}{}
886 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
887 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
890 for _, srv := range portServe {
891 sortPathHandlers(srv.SystemHandlers)
892 sortPathHandlers(srv.ServiceHandlers)
898func sortPathHandlers(l []pathHandler) {
899 sort.Slice(l, func(i, j int) bool {
902 if len(a) == len(b) {
903 // For consistent order.
906 // Longest paths first.
907 return len(a) > len(b)
911// functions to be launched in goroutine that will serve on a listener.
914// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
915// immediately after startup. We only do so for our explicit listener hostnames,
916// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
917// do request autoconfig, otherwise clients may run into their timeouts waiting for
918// the certificate to be given during the first https connection.
919var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
921type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
923// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
924func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
925 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
930 if tlsConfig == nil {
932 if os.Getuid() == 0 {
933 pkglog.Print("http listener",
934 slog.String("name", name),
935 slog.String("kinds", strings.Join(kinds, ",")),
936 slog.String("address", addr))
938 ln, err = mox.Listen(mox.Network(ip), addr)
940 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
944 if os.Getuid() == 0 {
945 pkglog.Print("https listener",
946 slog.String("name", name),
947 slog.String("kinds", strings.Join(kinds, ",")),
948 slog.String("address", addr))
950 ln, err = mox.Listen(mox.Network(ip), addr)
952 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
954 ln = tls.NewListener(ln, tlsConfig)
957 server := &http.Server{
959 TLSConfig: tlsConfig,
960 ReadHeaderTimeout: 30 * time.Second,
961 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
962 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
963 TLSNextProto: nextProto,
965 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
966 // However, HTTP2 is negotiated via ALPN. Because we are configuring
967 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
968 // and calling ConfigureServer.
969 err = http2.ConfigureServer(server, nil)
971 pkglog.Fatalx("https: unable to configure http2", err)
974 err := server.Serve(ln)
975 pkglog.Fatalx(protocol+": serve", err)
977 servers = append(servers, serve)
980// Serve starts serving on the initialized listeners.
982 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
984 go webaccount.ImportManage()
986 for _, serve := range servers {
992 time.Sleep(1 * time.Second)
994 for m, hosts := range ensureManagerHosts {
995 for host := range hosts {
996 // Check if certificate is already available. If so, we don't print as much after a
997 // restart, and finish more quickly if only a few certificates are missing/old.
998 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
999 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1005 // Just in case someone adds quite some domains to their config. We don't want to
1006 // hit any ACME rate limits.
1010 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1011 time.Sleep(10 * time.Second)
1015 hello := &tls.ClientHelloInfo{
1016 ServerName: host.ASCII,
1018 // Make us fetch an ECDSA P256 cert.
1019 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1020 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1021 SupportedCurves: []tls.CurveID{tls.CurveP256},
1022 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1023 SupportedVersions: []uint16{tls.VersionTLS13},
1025 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1026 if _, err := m.Manager.GetCertificate(hello); err != nil {
1027 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))