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))
105// healthHandle returns 200 when the server is healthy, 503 when shutting down.
106func healthHandle(w http.ResponseWriter, r *http.Request) {
107 if r.Method != "GET" && r.Method != "HEAD" {
108 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
112 case <-mox.Shutdown.Done():
113 http.Error(w, "shutting down", http.StatusServiceUnavailable)
115 w.Header().Set("Content-Type", "text/plain")
116 w.WriteHeader(http.StatusOK)
121type responseWriterFlusher interface {
126// http.ResponseWriter that writes access log and tracks metrics at end of response.
127type loggingWriter struct {
128 W responseWriterFlusher // Calls are forwarded.
132 WebsocketRequest bool // Whether request from was websocket.
140 Size int64 // Of data served to client, for non-websocket responses.
141 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
142 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
144 WebsocketResponse bool // If this was a successful websocket connection with backend.
145 SizeFromClient, SizeToClient int64 // Websocket data.
146 Attrs []slog.Attr // Additional fields to log.
149func (w *loggingWriter) AddAttr(a slog.Attr) {
150 w.Attrs = append(w.Attrs, a)
153func (w *loggingWriter) Flush() {
157func (w *loggingWriter) Header() http.Header {
161// protocol, for logging.
162func (w *loggingWriter) proto(websocket bool) string {
173func (w *loggingWriter) Write(buf []byte) (int, error) {
174 if w.StatusCode == 0 {
175 w.WriteHeader(http.StatusOK)
181 n, err = w.W.Write(buf)
186 // We flush after each write. Probably takes a few more bytes, but prevents any
187 // issues due to buffering.
188 // w.Gzip.Write updates w.Size with the compressed byte count.
189 n, err = w.Gzip.Write(buf)
194 w.UncompressedSize += int64(n)
203func (w *loggingWriter) setStatusCode(statusCode int) {
204 if w.StatusCode != 0 {
208 w.StatusCode = statusCode
209 method := metricHTTPMethod(w.R.Method)
210 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
213// SetUncompressedSize is used through an interface by
214// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
215func (w *loggingWriter) SetUncompressedSize(origSize int64) {
216 w.UncompressedSize = origSize
219func (w *loggingWriter) WriteHeader(statusCode int) {
220 if w.StatusCode != 0 {
224 w.setStatusCode(statusCode)
226 // We transparently gzip-compress responses for requests under these conditions, all must apply:
228 // - Enabled for handler (static handlers make their own decisions).
229 // - Not a websocket request.
230 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
231 // - Not already compressed, or any other Content-Encoding header (including "identity").
232 // - Client accepts gzip encoded responses.
233 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
234 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")) {
235 // 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.
237 // We track the gzipped output for the access log.
238 cw := countWriter{Writer: w.W, Size: &w.Size}
239 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
240 w.W.Header().Set("Content-Encoding", "gzip")
241 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
243 w.W.WriteHeader(statusCode)
246func acceptsGzip(r *http.Request) bool {
247 s := r.Header.Get("Accept-Encoding")
248 t := strings.Split(s, ",")
249 for _, e := range t {
250 e = strings.TrimSpace(e)
251 tt := strings.Split(e, ";")
252 if len(tt) > 1 && t[1] == "q=0" {
262var compressibleTypes = map[string]bool{
263 "application/csv": true,
264 "application/javascript": true,
265 "application/json": true,
266 "application/x-javascript": true,
267 "application/xml": true,
268 "image/vnd.microsoft.icon": true,
269 "image/x-icon": true,
273 "font/opentype": true,
276func compressibleContentType(ct string) bool {
277 ct = strings.SplitN(ct, ";", 2)[0]
278 ct = strings.TrimSpace(ct)
279 ct = strings.ToLower(ct)
280 if compressibleTypes[ct] {
283 t, st, _ := strings.Cut(ct, "/")
284 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
287func compressibleContent(f *os.File) bool {
288 // We don't want to store many small files. They take up too much disk overhead.
289 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
293 buf := make([]byte, 512)
294 n, err := f.ReadAt(buf, 0)
295 if err != nil && err != io.EOF {
298 ct := http.DetectContentType(buf[:n])
299 return compressibleContentType(ct)
302type countWriter struct {
307func (w countWriter) Write(buf []byte) (int, error) {
308 n, err := w.Writer.Write(buf)
315var tlsVersions = map[uint16]string{
316 tls.VersionTLS10: "tls1.0",
317 tls.VersionTLS11: "tls1.1",
318 tls.VersionTLS12: "tls1.2",
319 tls.VersionTLS13: "tls1.3",
322func metricHTTPMethod(method string) string {
323 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
324 method = strings.ToLower(method)
326 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":
332func (w *loggingWriter) error(err error) {
338func (w *loggingWriter) Done() {
339 if w.Err == nil && w.Gzip != nil {
340 if err := w.Gzip.Close(); err != nil {
345 method := metricHTTPMethod(w.R.Method)
346 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
350 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
358 err = w.R.Context().Err()
360 attrs := []slog.Attr{
361 slog.String("httpaccess", ""),
362 slog.String("handler", w.Handler),
363 slog.String("method", method),
364 slog.Any("url", w.R.URL),
365 slog.String("host", w.R.Host),
366 slog.Duration("duration", time.Since(w.Start)),
367 slog.Int("statuscode", w.StatusCode),
368 slog.String("proto", strings.ToLower(w.R.Proto)),
369 slog.String("remoteaddr", w.R.RemoteAddr),
370 slog.String("tlsinfo", tlsinfo),
371 slog.String("useragent", w.R.Header.Get("User-Agent")),
372 slog.String("referer", w.R.Header.Get("Referer")),
375 s := w.R.Header.Get("X-Forwarded-For")
376 ipstr := strings.TrimSpace(strings.Split(s, ",")[0])
377 attrs = append(attrs,
378 slog.String("clientip", ipstr),
382 if w.WebsocketRequest {
383 attrs = append(attrs,
384 slog.Bool("websocketrequest", true),
387 if w.WebsocketResponse {
388 attrs = append(attrs,
389 slog.Bool("websocket", true),
390 slog.Int64("sizetoclient", w.SizeToClient),
391 slog.Int64("sizefromclient", w.SizeFromClient),
393 } else if w.UncompressedSize > 0 {
394 attrs = append(attrs,
395 slog.Int64("size", w.Size),
396 slog.Int64("uncompressedsize", w.UncompressedSize),
399 attrs = append(attrs,
400 slog.Int64("size", w.Size),
403 attrs = append(attrs, w.Attrs...)
404 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
407// Built-in handlers, e.g. mta-sts and autoconfig.
408type pathHandler struct {
409 Name string // For logging/metrics.
410 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.
411 Path string // Path to register, like on http.ServeMux.
416 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
417 TLSConfig *tls.Config
418 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
420 Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
421 RateLimitDisabled bool // Don't apply ratelimiting.
423 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
424 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
425 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
426 // interfaces) last. WebHandlers can also pass requests to the internal servers.
427 // This order allows admins to serve other content on domains serving the mox.conf
428 // internal services.
429 SystemHandlers []pathHandler // Sorted, longest first.
431 ServiceHandlers []pathHandler // Sorted, longest first.
434// SystemHandle registers a named system handler for a path and optional host. If
435// path ends with a slash, it is used as prefix match, otherwise a full path match
436// is required. If hostOpt is set, only requests to those host are handled by this
438func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
439 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
442// Like SystemHandle, but for internal services "admin", "account", "webmail",
443// "webapi" configured in the mox.conf Listener.
444func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
445 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
449 limiterConnectionrate = &ratelimit.Limiter{
450 WindowLimits: []ratelimit.WindowLimit{
453 Limits: [...]int64{1000, 3000, 9000},
457 Limits: [...]int64{5000, 15000, 45000},
463// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
464// right pathHandler or WebHandler, and it generates access logs and tracks
466func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
469 // Rate limiting as early as possible, if enabled.
470 if !s.RateLimitDisabled {
471 // If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
472 // Otherwise the remote IP for this connection.
475 s := r.Header.Get("X-Forwarded-For")
476 ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
478 pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
482 ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
484 pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
487 ip := net.ParseIP(ipstr)
488 if ip == nil && ipstr != "" {
489 pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
491 if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
492 method := metricHTTPMethod(r.Method)
497 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
498 // No logging, that's just noise.
500 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
505 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
506 r = r.WithContext(ctx)
508 wf, ok := xw.(responseWriterFlusher)
510 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
514 nw := &loggingWriter{
518 Forwarded: s.Forwarded,
522 // Cleanup path, removing ".." and ".". Keep any trailing slash.
523 trailingPath := strings.HasSuffix(r.URL.Path, "/")
524 if r.URL.Path == "" {
527 r.URL.Path = path.Clean(r.URL.Path)
528 if r.URL.Path == "." {
531 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
536 nhost, _, err := net.SplitHostPort(host)
540 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
542 dom, domErr := dns.ParseDomain(host)
544 ipdom = dns.IPDomain{Domain: dom}
548 handle := func(h pathHandler) bool {
549 if h.HostMatch != nil && !h.HostMatch(ipdom) {
552 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
555 h.Handler.ServeHTTP(nw, r)
561 if slices.ContainsFunc(s.SystemHandlers, handle) {
565 if WebHandle(nw, r, ipdom) {
569 if slices.ContainsFunc(s.ServiceHandlers, handle) {
572 nw.Handler = "(nomatch)"
576func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
577 // Helpfully redirect user to version with ending slash.
578 if path != "/" && strings.HasSuffix(path, "/") {
579 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
580 http.Redirect(w, r, path, http.StatusSeeOther)
582 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
586// Listen binds to sockets for HTTP listeners, including those required for ACME to
587// generate TLS certificates. It stores the listeners so Serve can start serving them.
589 // Initialize listeners in deterministic order for the same potential error
591 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
592 for _, name := range names {
593 l := mox.Conf.Static.Listeners[name]
594 portServe := portServes(name, l)
596 ports := slices.Sorted(maps.Keys(portServe))
597 for _, port := range ports {
598 srv := portServe[port]
599 for _, ip := range l.IPs {
600 // Since go1.27, the http.Server.TLSNextProto map is modified during
601 // http.Server.Serve instead of in http2.ConfigureServer. So clone it so it doesn't
602 // get modified concurrently when multiple listeners using the same config start
604 nextProto := maps.Clone(srv.NextProto)
606 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, nextProto)
612func portServes(name string, l config.Listener) map[int]*serve {
613 portServe := map[int]*serve{}
615 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
616 localhost := dns.Domain{ASCII: "localhost"}
618 ldom := l.HostnameDomain
619 if l.Hostname == "" {
620 ldom = mox.Conf.Static.HostnameDomain
622 listenerHostMatch := func(host dns.IPDomain) bool {
626 return host.Domain == ldom || host.Domain == localhost
628 accountHostMatch := func(host dns.IPDomain) bool {
629 if listenerHostMatch(host) {
632 return mox.Conf.IsClientSettingsDomain(host.Domain)
635 var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve
636 ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve {
639 s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil}
642 s.Kinds = append(s.Kinds, kind)
643 if favicon && !s.Favicon {
644 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
647 s.Forwarded = s.Forwarded || forwarded
648 s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
650 // We clone TLS configs because we may modify it later on for this server, for
651 // ALPN. And we need copies because multiple listeners on http.Server where the
652 // config is used will try to modify it concurrently.
653 if https && l.TLS.ACME != "" {
654 s.TLSConfig = l.TLS.ACMEConfig.Clone()
656 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
657 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
658 ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false)
661 s.TLSConfig = l.TLS.Config.Clone()
666 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
667 // yet, add http-01 validation mechanism handler to server.
668 ensureACMEHTTP01 := func(srv *serve) {
669 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
670 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
671 srv.Kinds = append(srv.Kinds, "acme-http-01")
672 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
676 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
677 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
678 ensureServe(true, false, false, port, "acme-tls-alpn-01", false)
680 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
681 s := ensureServe(true, false, false, 443, "smtp-https", false)
682 hostname := mox.Conf.Static.HostnameDomain
683 if l.Hostname != "" {
684 hostname = l.HostnameDomain
687 maxMsgSize := l.SMTPMaxMessageSize
689 maxMsgSize = config.DefaultMaxMsgSize
691 requireTLS := !l.SMTP.NoRequireTLS
693 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
694 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
697 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
698 s := ensureServe(true, false, false, 443, "imap-https", false)
699 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
700 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
703 if l.AccountHTTP.Enabled {
704 port := config.Port(l.AccountHTTP.Port, 80)
706 if l.AccountHTTP.Path != "" {
707 path = l.AccountHTTP.Path
709 srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true)
710 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
711 srv.ServiceHandle("account", accountHostMatch, path, handler)
712 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
713 ensureACMEHTTP01(srv)
715 if l.AccountHTTPS.Enabled {
716 port := config.Port(l.AccountHTTPS.Port, 443)
718 if l.AccountHTTPS.Path != "" {
719 path = l.AccountHTTPS.Path
721 srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true)
722 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
723 srv.ServiceHandle("account", accountHostMatch, path, handler)
724 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
727 if l.AdminHTTP.Enabled {
728 port := config.Port(l.AdminHTTP.Port, 80)
730 if l.AdminHTTP.Path != "" {
731 path = l.AdminHTTP.Path
733 srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true)
734 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
735 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
736 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
737 ensureACMEHTTP01(srv)
739 if l.AdminHTTPS.Enabled {
740 port := config.Port(l.AdminHTTPS.Port, 443)
742 if l.AdminHTTPS.Path != "" {
743 path = l.AdminHTTPS.Path
745 srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true)
746 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
747 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
748 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
751 maxMsgSize := l.SMTPMaxMessageSize
753 maxMsgSize = config.DefaultMaxMsgSize
756 if l.WebAPIHTTP.Enabled {
757 port := config.Port(l.WebAPIHTTP.Port, 80)
759 if l.WebAPIHTTP.Path != "" {
760 path = l.WebAPIHTTP.Path
762 srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true)
763 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
764 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
765 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
766 ensureACMEHTTP01(srv)
768 if l.WebAPIHTTPS.Enabled {
769 port := config.Port(l.WebAPIHTTPS.Port, 443)
771 if l.WebAPIHTTPS.Path != "" {
772 path = l.WebAPIHTTPS.Path
774 srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true)
775 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
776 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
777 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
780 if l.WebmailHTTP.Enabled {
781 port := config.Port(l.WebmailHTTP.Port, 80)
783 if l.WebmailHTTP.Path != "" {
784 path = l.WebmailHTTP.Path
786 srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true)
787 var accountPath string
788 if l.AccountHTTP.Enabled {
790 if l.AccountHTTP.Path != "" {
791 accountPath = l.AccountHTTP.Path
794 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
795 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
796 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
797 ensureACMEHTTP01(srv)
799 if l.WebmailHTTPS.Enabled {
800 port := config.Port(l.WebmailHTTPS.Port, 443)
802 if l.WebmailHTTPS.Path != "" {
803 path = l.WebmailHTTPS.Path
805 srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true)
806 var accountPath string
807 if l.AccountHTTPS.Enabled {
809 if l.AccountHTTPS.Path != "" {
810 accountPath = l.AccountHTTPS.Path
813 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
814 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
815 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
818 if l.MetricsHTTP.Enabled {
819 port := config.Port(l.MetricsHTTP.Port, 8010)
820 srv := ensureServe(false, false, false, port, "metrics-http", false)
821 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
822 srv.SystemHandle("health", nil, "/health", mox.SafeHeaders(http.HandlerFunc(healthHandle)))
823 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
824 if r.URL.Path != "/" {
827 } else if r.Method != "GET" {
828 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
831 w.Header().Set("Content-Type", "text/html")
832 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
835 if l.AutoconfigHTTPS.Enabled {
836 port := config.Port(l.AutoconfigHTTPS.Port, 443)
837 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, l.AutoconfigHTTPS.Forwarded, false, port, "autoconfig-https", false)
838 if l.AutoconfigHTTPS.NonTLS {
839 ensureACMEHTTP01(srv)
841 autoconfigMatch := func(ipdom dns.IPDomain) bool {
846 // Thunderbird requests an autodiscovery URL at the email address domain name, so
847 // autoconfig prefix is optional.
848 if after, ok := strings.CutPrefix(dom.ASCII, "autoconfig."); ok {
850 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
852 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
853 // use the mail server's host name.
854 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
857 dc, ok := mox.Conf.Domain(dom)
858 return ok && !dc.ReportsOnly
860 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
861 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
862 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
863 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
865 if l.MTASTSHTTPS.Enabled {
866 port := config.Port(l.MTASTSHTTPS.Port, 443)
867 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, l.MTASTSHTTPS.Forwarded, false, port, "mtasts-https", false)
868 if l.MTASTSHTTPS.NonTLS {
869 ensureACMEHTTP01(srv)
871 mtastsMatch := func(ipdom dns.IPDomain) bool {
872 // todo: may want to check this against the configured domains, could in theory be just a webserver.
877 return strings.HasPrefix(dom.ASCII, "mta-sts.")
879 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
881 if l.PprofHTTP.Enabled {
882 // Importing net/http/pprof registers handlers on the default serve mux.
883 port := config.Port(l.PprofHTTP.Port, 8011)
884 if _, ok := portServe[port]; ok {
885 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
887 srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil}
888 portServe[port] = srv
889 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
891 if l.WebserverHTTP.Enabled {
892 port := config.Port(l.WebserverHTTP.Port, 80)
893 srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false)
895 ensureACMEHTTP01(srv)
897 if l.WebserverHTTPS.Enabled {
898 port := config.Port(l.WebserverHTTPS.Port, 443)
899 srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false)
903 if l.TLS != nil && l.TLS.ACME != "" {
904 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
905 if ensureManagerHosts[m] == nil {
906 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
908 hosts := ensureManagerHosts[m]
909 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
911 if l.HostnameDomain.ASCII != "" {
912 hosts[l.HostnameDomain] = struct{}{}
915 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
916 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
917 // for the timeouts of clients doing autoconfig.
919 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
920 for _, name := range mox.Conf.Domains() {
921 if dom, err := dns.ParseDomain(name); err != nil {
922 pkglog.Errorx("parsing domain from config", err)
923 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
924 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
928 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
930 pkglog.Errorx("parsing domain from config for autoconfig", err)
932 hosts[autoconfdom] = struct{}{}
938 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
939 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
942 for _, srv := range portServe {
943 sortPathHandlers(srv.SystemHandlers)
944 sortPathHandlers(srv.ServiceHandlers)
950func sortPathHandlers(l []pathHandler) {
951 sort.Slice(l, func(i, j int) bool {
954 if len(a) == len(b) {
955 // For consistent order.
958 // Longest paths first.
959 return len(a) > len(b)
963// functions to be launched in goroutine that will serve on a listener.
966// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
967// immediately after startup. We only do so for our explicit listener hostnames,
968// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
969// do request autoconfig, otherwise clients may run into their timeouts waiting for
970// the certificate to be given during the first https connection.
971var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
973type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
975// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
976func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
977 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
982 if tlsConfig == nil {
984 if os.Getuid() == 0 {
985 pkglog.Print("http listener",
986 slog.String("name", name),
987 slog.String("kinds", strings.Join(kinds, ",")),
988 slog.String("address", addr))
990 ln, err = mox.Listen(mox.Network(ip), addr)
992 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
996 if os.Getuid() == 0 {
997 pkglog.Print("https listener",
998 slog.String("name", name),
999 slog.String("kinds", strings.Join(kinds, ",")),
1000 slog.String("address", addr))
1002 ln, err = mox.Listen(mox.Network(ip), addr)
1004 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
1006 ln = tls.NewListener(ln, tlsConfig)
1009 server := &http.Server{
1011 TLSConfig: tlsConfig,
1012 ReadHeaderTimeout: 30 * time.Second,
1013 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
1014 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
1015 TLSNextProto: nextProto,
1018 // Set server.Protocols with http2 enabled or http2 won't work with go1.27 with
1019 // http2.ConfigureServer and non-nil server.TLSNextProto.
1020 if tlsConfig != nil {
1021 var p http.Protocols
1024 server.Protocols = &p
1027 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
1028 // However, HTTP2 is negotiated via ALPN. Because we are configuring
1029 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
1030 // and calling ConfigureServer.
1031 err = http2.ConfigureServer(server, nil)
1033 pkglog.Fatalx("https: unable to configure http2", err)
1037 err := server.Serve(ln)
1038 pkglog.Fatalx(protocol+": serve", err)
1040 servers = append(servers, serve)
1043// Serve starts serving on the initialized listeners.
1045 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
1047 go webaccount.ImportManage()
1049 for _, serve := range servers {
1055 time.Sleep(1 * time.Second)
1057 for m, hosts := range ensureManagerHosts {
1058 for host := range hosts {
1059 // Check if certificate is already available. If so, we don't print as much after a
1060 // restart, and finish more quickly if only a few certificates are missing/old.
1061 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1062 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1068 // Just in case someone adds quite some domains to their config. We don't want to
1069 // hit any ACME rate limits.
1073 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1074 time.Sleep(10 * time.Second)
1078 hello := &tls.ClientHelloInfo{
1079 ServerName: host.ASCII,
1081 // Make us fetch an ECDSA P256 cert.
1082 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1083 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1084 SupportedCurves: []tls.CurveID{tls.CurveP256},
1085 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1086 SupportedVersions: []uint16{tls.VersionTLS13},
1088 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1089 if _, err := m.Manager.GetCertificate(hello); err != nil {
1090 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))