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.
116 WebsocketRequest bool // Whether request from was websocket.
124 Size int64 // Of data served to client, for non-websocket responses.
125 UncompressedSize int64 // Can be set by a handler that already serves compressed data, and we update it while compressing.
126 Gzip *gzip.Writer // Only set if we transparently compress within loggingWriter (static handlers handle compression themselves, with a cache).
128 WebsocketResponse bool // If this was a successful websocket connection with backend.
129 SizeFromClient, SizeToClient int64 // Websocket data.
130 Attrs []slog.Attr // Additional fields to log.
133func (w *loggingWriter) AddAttr(a slog.Attr) {
134 w.Attrs = append(w.Attrs, a)
137func (w *loggingWriter) Flush() {
141func (w *loggingWriter) Header() http.Header {
145// protocol, for logging.
146func (w *loggingWriter) proto(websocket bool) string {
157func (w *loggingWriter) Write(buf []byte) (int, error) {
158 if w.StatusCode == 0 {
159 w.WriteHeader(http.StatusOK)
165 n, err = w.W.Write(buf)
170 // We flush after each write. Probably takes a few more bytes, but prevents any
171 // issues due to buffering.
172 // w.Gzip.Write updates w.Size with the compressed byte count.
173 n, err = w.Gzip.Write(buf)
178 w.UncompressedSize += int64(n)
187func (w *loggingWriter) setStatusCode(statusCode int) {
188 if w.StatusCode != 0 {
192 w.StatusCode = statusCode
193 method := metricHTTPMethod(w.R.Method)
194 metricRequest.WithLabelValues(w.Handler, w.proto(w.WebsocketRequest), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
197// SetUncompressedSize is used through an interface by
198// ../webmail/webmail.go:/WriteHeader, preventing an import cycle.
199func (w *loggingWriter) SetUncompressedSize(origSize int64) {
200 w.UncompressedSize = origSize
203func (w *loggingWriter) WriteHeader(statusCode int) {
204 if w.StatusCode != 0 {
208 w.setStatusCode(statusCode)
210 // We transparently gzip-compress responses for requests under these conditions, all must apply:
212 // - Enabled for handler (static handlers make their own decisions).
213 // - Not a websocket request.
214 // - Regular success responses (not errors, or partial content or redirects or "not modified", etc).
215 // - Not already compressed, or any other Content-Encoding header (including "identity").
216 // - Client accepts gzip encoded responses.
217 // - The response has a content-type that is compressible (text/*, */*+{json,xml}, and a few common files (e.g. json, xml, javascript).
218 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")) {
219 // 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.
221 // We track the gzipped output for the access log.
222 cw := countWriter{Writer: w.W, Size: &w.Size}
223 w.Gzip, _ = gzip.NewWriterLevel(cw, gzip.BestSpeed)
224 w.W.Header().Set("Content-Encoding", "gzip")
225 w.W.Header().Del("Content-Length") // No longer valid, set again for small responses by net/http.
227 w.W.WriteHeader(statusCode)
230func acceptsGzip(r *http.Request) bool {
231 s := r.Header.Get("Accept-Encoding")
232 t := strings.Split(s, ",")
233 for _, e := range t {
234 e = strings.TrimSpace(e)
235 tt := strings.Split(e, ";")
236 if len(tt) > 1 && t[1] == "q=0" {
246var compressibleTypes = map[string]bool{
247 "application/csv": true,
248 "application/javascript": true,
249 "application/json": true,
250 "application/x-javascript": true,
251 "application/xml": true,
252 "image/vnd.microsoft.icon": true,
253 "image/x-icon": true,
257 "font/opentype": true,
260func compressibleContentType(ct string) bool {
261 ct = strings.SplitN(ct, ";", 2)[0]
262 ct = strings.TrimSpace(ct)
263 ct = strings.ToLower(ct)
264 if compressibleTypes[ct] {
267 t, st, _ := strings.Cut(ct, "/")
268 return t == "text" || strings.HasSuffix(st, "+json") || strings.HasSuffix(st, "+xml")
271func compressibleContent(f *os.File) bool {
272 // We don't want to store many small files. They take up too much disk overhead.
273 if fi, err := f.Stat(); err != nil || fi.Size() < 1024 || fi.Size() > 10*1024*1024 {
277 buf := make([]byte, 512)
278 n, err := f.ReadAt(buf, 0)
279 if err != nil && err != io.EOF {
282 ct := http.DetectContentType(buf[:n])
283 return compressibleContentType(ct)
286type countWriter struct {
291func (w countWriter) Write(buf []byte) (int, error) {
292 n, err := w.Writer.Write(buf)
299var tlsVersions = map[uint16]string{
300 tls.VersionTLS10: "tls1.0",
301 tls.VersionTLS11: "tls1.1",
302 tls.VersionTLS12: "tls1.2",
303 tls.VersionTLS13: "tls1.3",
306func metricHTTPMethod(method string) string {
307 // https://www.iana.org/assignments/http-methods/http-methods.xhtml
308 method = strings.ToLower(method)
310 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":
316func (w *loggingWriter) error(err error) {
322func (w *loggingWriter) Done() {
323 if w.Err == nil && w.Gzip != nil {
324 if err := w.Gzip.Close(); err != nil {
329 method := metricHTTPMethod(w.R.Method)
330 metricResponse.WithLabelValues(w.Handler, w.proto(w.WebsocketResponse), method, fmt.Sprintf("%d", w.StatusCode)).Observe(float64(time.Since(w.Start)) / float64(time.Second))
334 if v, ok := tlsVersions[w.R.TLS.Version]; ok {
342 err = w.R.Context().Err()
344 attrs := []slog.Attr{
345 slog.String("httpaccess", ""),
346 slog.String("handler", w.Handler),
347 slog.String("method", method),
348 slog.Any("url", w.R.URL),
349 slog.String("host", w.R.Host),
350 slog.Duration("duration", time.Since(w.Start)),
351 slog.Int("statuscode", w.StatusCode),
352 slog.String("proto", strings.ToLower(w.R.Proto)),
353 slog.String("remoteaddr", w.R.RemoteAddr),
354 slog.String("tlsinfo", tlsinfo),
355 slog.String("useragent", w.R.Header.Get("User-Agent")),
356 slog.String("referer", w.R.Header.Get("Referer")),
359 s := w.R.Header.Get("X-Forwarded-For")
360 ipstr := strings.TrimSpace(strings.Split(s, ",")[0])
361 attrs = append(attrs,
362 slog.String("clientip", ipstr),
366 if w.WebsocketRequest {
367 attrs = append(attrs,
368 slog.Bool("websocketrequest", true),
371 if w.WebsocketResponse {
372 attrs = append(attrs,
373 slog.Bool("websocket", true),
374 slog.Int64("sizetoclient", w.SizeToClient),
375 slog.Int64("sizefromclient", w.SizeFromClient),
377 } else if w.UncompressedSize > 0 {
378 attrs = append(attrs,
379 slog.Int64("size", w.Size),
380 slog.Int64("uncompressedsize", w.UncompressedSize),
383 attrs = append(attrs,
384 slog.Int64("size", w.Size),
387 attrs = append(attrs, w.Attrs...)
388 pkglog.WithContext(w.R.Context()).Debugx("http request", err, attrs...)
391// Built-in handlers, e.g. mta-sts and autoconfig.
392type pathHandler struct {
393 Name string // For logging/metrics.
394 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.
395 Path string // Path to register, like on http.ServeMux.
400 Kinds []string // Type of handler and protocol (e.g. acme-tls-alpn-01, account-http, admin-https, imap-https, smtp-https).
401 TLSConfig *tls.Config
402 NextProto tlsNextProtoMap // For HTTP server, when we do submission/imap with ALPN over the HTTPS port.
404 Forwarded bool // Requests are coming from a reverse proxy, we'll use X-Forwarded-For for the IP address to ratelimit.
405 RateLimitDisabled bool // Don't apply ratelimiting.
407 // SystemHandlers are for MTA-STS, autoconfig, ACME validation. They can't be
408 // overridden by WebHandlers. WebHandlers are evaluated next, and the internal
409 // service handlers from Listeners in mox.conf (for admin, account, webmail, webapi
410 // interfaces) last. WebHandlers can also pass requests to the internal servers.
411 // This order allows admins to serve other content on domains serving the mox.conf
412 // internal services.
413 SystemHandlers []pathHandler // Sorted, longest first.
415 ServiceHandlers []pathHandler // Sorted, longest first.
418// SystemHandle registers a named system handler for a path and optional host. If
419// path ends with a slash, it is used as prefix match, otherwise a full path match
420// is required. If hostOpt is set, only requests to those host are handled by this
422func (s *serve) SystemHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
423 s.SystemHandlers = append(s.SystemHandlers, pathHandler{name, hostMatch, path, fn})
426// Like SystemHandle, but for internal services "admin", "account", "webmail",
427// "webapi" configured in the mox.conf Listener.
428func (s *serve) ServiceHandle(name string, hostMatch func(dns.IPDomain) bool, path string, fn http.Handler) {
429 s.ServiceHandlers = append(s.ServiceHandlers, pathHandler{name, hostMatch, path, fn})
433 limiterConnectionrate = &ratelimit.Limiter{
434 WindowLimits: []ratelimit.WindowLimit{
437 Limits: [...]int64{1000, 3000, 9000},
441 Limits: [...]int64{5000, 15000, 45000},
447// ServeHTTP is the starting point for serving HTTP requests. It dispatches to the
448// right pathHandler or WebHandler, and it generates access logs and tracks
450func (s *serve) ServeHTTP(xw http.ResponseWriter, r *http.Request) {
453 // Rate limiting as early as possible, if enabled.
454 if !s.RateLimitDisabled {
455 // If requests are coming from a reverse proxy, use the IP from X-Forwarded-For.
456 // Otherwise the remote IP for this connection.
459 s := r.Header.Get("X-Forwarded-For")
460 ipstr = strings.TrimSpace(strings.Split(s, ",")[0])
462 pkglog.Debug("ratelimit: no ip address in X-Forwarded-For header")
466 ipstr, _, err = net.SplitHostPort(r.RemoteAddr)
468 pkglog.Debugx("ratelimit: parsing remote address", err, slog.String("remoteaddr", r.RemoteAddr))
471 ip := net.ParseIP(ipstr)
472 if ip == nil && ipstr != "" {
473 pkglog.Debug("ratelimit: invalid ip", slog.String("ip", ipstr))
475 if ip != nil && !limiterConnectionrate.Add(ip, now, 1) {
476 method := metricHTTPMethod(r.Method)
481 metricRequest.WithLabelValues("(ratelimited)", proto, method, "429").Observe(0)
482 // No logging, that's just noise.
484 http.Error(xw, "429 - too many auth attempts", http.StatusTooManyRequests)
489 ctx := context.WithValue(r.Context(), mlog.CidKey, mox.Cid())
490 r = r.WithContext(ctx)
492 wf, ok := xw.(responseWriterFlusher)
494 http.Error(xw, "500 - internal server error - cannot access underlying connection"+recvid(r), http.StatusInternalServerError)
498 nw := &loggingWriter{
502 Forwarded: s.Forwarded,
506 // Cleanup path, removing ".." and ".". Keep any trailing slash.
507 trailingPath := strings.HasSuffix(r.URL.Path, "/")
508 if r.URL.Path == "" {
511 r.URL.Path = path.Clean(r.URL.Path)
512 if r.URL.Path == "." {
515 if trailingPath && !strings.HasSuffix(r.URL.Path, "/") {
520 nhost, _, err := net.SplitHostPort(host)
524 ipdom := dns.IPDomain{IP: net.ParseIP(host)}
526 dom, domErr := dns.ParseDomain(host)
528 ipdom = dns.IPDomain{Domain: dom}
532 handle := func(h pathHandler) bool {
533 if h.HostMatch != nil && !h.HostMatch(ipdom) {
536 if r.URL.Path == h.Path || strings.HasSuffix(h.Path, "/") && strings.HasPrefix(r.URL.Path, h.Path) {
539 h.Handler.ServeHTTP(nw, r)
545 if slices.ContainsFunc(s.SystemHandlers, handle) {
549 if WebHandle(nw, r, ipdom) {
553 if slices.ContainsFunc(s.ServiceHandlers, handle) {
556 nw.Handler = "(nomatch)"
560func redirectToTrailingSlash(srv *serve, hostMatch func(dns.IPDomain) bool, name, path string) {
561 // Helpfully redirect user to version with ending slash.
562 if path != "/" && strings.HasSuffix(path, "/") {
563 handler := mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
564 http.Redirect(w, r, path, http.StatusSeeOther)
566 srv.ServiceHandle(name, hostMatch, strings.TrimRight(path, "/"), handler)
570// Listen binds to sockets for HTTP listeners, including those required for ACME to
571// generate TLS certificates. It stores the listeners so Serve can start serving them.
573 // Initialize listeners in deterministic order for the same potential error
575 names := slices.Sorted(maps.Keys(mox.Conf.Static.Listeners))
576 for _, name := range names {
577 l := mox.Conf.Static.Listeners[name]
578 portServe := portServes(name, l)
580 ports := slices.Sorted(maps.Keys(portServe))
581 for _, port := range ports {
582 srv := portServe[port]
583 for _, ip := range l.IPs {
584 // Since go1.27, the http.Server.TLSNextProto map is modified during
585 // http.Server.Serve instead of in http2.ConfigureServer. So clone it so it doesn't
586 // get modified concurrently when multiple listeners using the same config start
588 nextProto := maps.Clone(srv.NextProto)
590 listen1(ip, port, srv.TLSConfig, name, srv.Kinds, srv, nextProto)
596func portServes(name string, l config.Listener) map[int]*serve {
597 portServe := map[int]*serve{}
599 // For system/services, we serve on host localhost too, for ssh tunnel scenario's.
600 localhost := dns.Domain{ASCII: "localhost"}
602 ldom := l.HostnameDomain
603 if l.Hostname == "" {
604 ldom = mox.Conf.Static.HostnameDomain
606 listenerHostMatch := func(host dns.IPDomain) bool {
610 return host.Domain == ldom || host.Domain == localhost
612 accountHostMatch := func(host dns.IPDomain) bool {
613 if listenerHostMatch(host) {
616 return mox.Conf.IsClientSettingsDomain(host.Domain)
619 var ensureServe func(https, forwarded, noRateLimiting bool, port int, kind string, favicon bool) *serve
620 ensureServe = func(https, forwarded, rateLimitDisabled bool, port int, kind string, favicon bool) *serve {
623 s = &serve{nil, nil, tlsNextProtoMap{}, false, false, false, nil, false, nil}
626 s.Kinds = append(s.Kinds, kind)
627 if favicon && !s.Favicon {
628 s.ServiceHandle("favicon", accountHostMatch, "/favicon.ico", mox.SafeHeaders(http.HandlerFunc(faviconHandle)))
631 s.Forwarded = s.Forwarded || forwarded
632 s.RateLimitDisabled = s.RateLimitDisabled || rateLimitDisabled
634 // We clone TLS configs because we may modify it later on for this server, for
635 // ALPN. And we need copies because multiple listeners on http.Server where the
636 // config is used will try to modify it concurrently.
637 if https && l.TLS.ACME != "" {
638 s.TLSConfig = l.TLS.ACMEConfig.Clone()
640 tlsport := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
641 if portServe[tlsport] == nil || !slices.Contains(portServe[tlsport].Kinds, "acme-tls-alpn-01") {
642 ensureServe(true, false, false, tlsport, "acme-tls-alpn-01", false)
645 s.TLSConfig = l.TLS.Config.Clone()
650 // If TLS with ACME is enabled on this plain HTTP port, and it hasn't been enabled
651 // yet, add http-01 validation mechanism handler to server.
652 ensureACMEHTTP01 := func(srv *serve) {
653 if l.TLS != nil && l.TLS.ACME != "" && !slices.Contains(srv.Kinds, "acme-http-01") {
654 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
655 srv.Kinds = append(srv.Kinds, "acme-http-01")
656 srv.SystemHandle("acme-http-01", nil, "/.well-known/acme-challenge/", m.Manager.HTTPHandler(nil))
660 if l.TLS != nil && l.TLS.ACME != "" && (l.SMTP.Enabled && !l.SMTP.NoSTARTTLS || l.Submissions.Enabled || l.IMAPS.Enabled) {
661 port := config.Port(mox.Conf.Static.ACME[l.TLS.ACME].Port, 443)
662 ensureServe(true, false, false, port, "acme-tls-alpn-01", false)
664 if l.Submissions.Enabled && l.Submissions.EnabledOnHTTPS {
665 s := ensureServe(true, false, false, 443, "smtp-https", false)
666 hostname := mox.Conf.Static.HostnameDomain
667 if l.Hostname != "" {
668 hostname = l.HostnameDomain
671 maxMsgSize := l.SMTPMaxMessageSize
673 maxMsgSize = config.DefaultMaxMsgSize
675 requireTLS := !l.SMTP.NoRequireTLS
677 s.NextProto["smtp"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
678 smtpserver.ServeTLSConn(name, hostname, conn, s.TLSConfig, true, true, maxMsgSize, requireTLS)
681 if l.IMAPS.Enabled && l.IMAPS.EnabledOnHTTPS {
682 s := ensureServe(true, false, false, 443, "imap-https", false)
683 s.NextProto["imap"] = func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
684 imapserver.ServeTLSConn(name, conn, s.TLSConfig)
687 if l.AccountHTTP.Enabled {
688 port := config.Port(l.AccountHTTP.Port, 80)
690 if l.AccountHTTP.Path != "" {
691 path = l.AccountHTTP.Path
693 srv := ensureServe(false, l.AccountHTTP.Forwarded, false, port, "account-http at "+path, true)
694 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTP.Forwarded))))
695 srv.ServiceHandle("account", accountHostMatch, path, handler)
696 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
697 ensureACMEHTTP01(srv)
699 if l.AccountHTTPS.Enabled {
700 port := config.Port(l.AccountHTTPS.Port, 443)
702 if l.AccountHTTPS.Path != "" {
703 path = l.AccountHTTPS.Path
705 srv := ensureServe(true, l.AccountHTTPS.Forwarded, false, port, "account-https at "+path, true)
706 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webaccount.Handler(path, l.AccountHTTPS.Forwarded))))
707 srv.ServiceHandle("account", accountHostMatch, path, handler)
708 redirectToTrailingSlash(srv, accountHostMatch, "account", path)
711 if l.AdminHTTP.Enabled {
712 port := config.Port(l.AdminHTTP.Port, 80)
714 if l.AdminHTTP.Path != "" {
715 path = l.AdminHTTP.Path
717 srv := ensureServe(false, l.AdminHTTP.Forwarded, false, port, "admin-http at "+path, true)
718 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTP.Forwarded))))
719 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
720 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
721 ensureACMEHTTP01(srv)
723 if l.AdminHTTPS.Enabled {
724 port := config.Port(l.AdminHTTPS.Port, 443)
726 if l.AdminHTTPS.Path != "" {
727 path = l.AdminHTTPS.Path
729 srv := ensureServe(true, l.AdminHTTPS.Forwarded, false, port, "admin-https at "+path, true)
730 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webadmin.Handler(path, l.AdminHTTPS.Forwarded))))
731 srv.ServiceHandle("admin", listenerHostMatch, path, handler)
732 redirectToTrailingSlash(srv, listenerHostMatch, "admin", path)
735 maxMsgSize := l.SMTPMaxMessageSize
737 maxMsgSize = config.DefaultMaxMsgSize
740 if l.WebAPIHTTP.Enabled {
741 port := config.Port(l.WebAPIHTTP.Port, 80)
743 if l.WebAPIHTTP.Path != "" {
744 path = l.WebAPIHTTP.Path
746 srv := ensureServe(false, l.WebAPIHTTP.Forwarded, false, port, "webapi-http at "+path, true)
747 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTP.Forwarded)))
748 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
749 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
750 ensureACMEHTTP01(srv)
752 if l.WebAPIHTTPS.Enabled {
753 port := config.Port(l.WebAPIHTTPS.Port, 443)
755 if l.WebAPIHTTPS.Path != "" {
756 path = l.WebAPIHTTPS.Path
758 srv := ensureServe(true, l.WebAPIHTTPS.Forwarded, false, port, "webapi-https at "+path, true)
759 handler := mox.SafeHeaders(http.StripPrefix(strings.TrimRight(path, "/"), webapisrv.NewServer(maxMsgSize, path, l.WebAPIHTTPS.Forwarded)))
760 srv.ServiceHandle("webapi", accountHostMatch, path, handler)
761 redirectToTrailingSlash(srv, accountHostMatch, "webapi", path)
764 if l.WebmailHTTP.Enabled {
765 port := config.Port(l.WebmailHTTP.Port, 80)
767 if l.WebmailHTTP.Path != "" {
768 path = l.WebmailHTTP.Path
770 srv := ensureServe(false, l.WebmailHTTP.Forwarded, false, port, "webmail-http at "+path, true)
771 var accountPath string
772 if l.AccountHTTP.Enabled {
774 if l.AccountHTTP.Path != "" {
775 accountPath = l.AccountHTTP.Path
778 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTP.Forwarded, accountPath)))
779 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
780 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
781 ensureACMEHTTP01(srv)
783 if l.WebmailHTTPS.Enabled {
784 port := config.Port(l.WebmailHTTPS.Port, 443)
786 if l.WebmailHTTPS.Path != "" {
787 path = l.WebmailHTTPS.Path
789 srv := ensureServe(true, l.WebmailHTTPS.Forwarded, false, port, "webmail-https at "+path, true)
790 var accountPath string
791 if l.AccountHTTPS.Enabled {
793 if l.AccountHTTPS.Path != "" {
794 accountPath = l.AccountHTTPS.Path
797 handler := http.StripPrefix(strings.TrimRight(path, "/"), http.HandlerFunc(webmail.Handler(maxMsgSize, path, l.WebmailHTTPS.Forwarded, accountPath)))
798 srv.ServiceHandle("webmail", accountHostMatch, path, handler)
799 redirectToTrailingSlash(srv, accountHostMatch, "webmail", path)
802 if l.MetricsHTTP.Enabled {
803 port := config.Port(l.MetricsHTTP.Port, 8010)
804 srv := ensureServe(false, false, false, port, "metrics-http", false)
805 srv.SystemHandle("metrics", nil, "/metrics", mox.SafeHeaders(promhttp.Handler()))
806 srv.SystemHandle("metrics", nil, "/", mox.SafeHeaders(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
807 if r.URL.Path != "/" {
810 } else if r.Method != "GET" {
811 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
814 w.Header().Set("Content-Type", "text/html")
815 fmt.Fprint(w, `<html><body>see <a href="metrics">metrics</a></body></html>`)
818 if l.AutoconfigHTTPS.Enabled {
819 port := config.Port(l.AutoconfigHTTPS.Port, 443)
820 srv := ensureServe(!l.AutoconfigHTTPS.NonTLS, l.AutoconfigHTTPS.Forwarded, false, port, "autoconfig-https", false)
821 if l.AutoconfigHTTPS.NonTLS {
822 ensureACMEHTTP01(srv)
824 autoconfigMatch := func(ipdom dns.IPDomain) bool {
829 // Thunderbird requests an autodiscovery URL at the email address domain name, so
830 // autoconfig prefix is optional.
831 if after, ok := strings.CutPrefix(dom.ASCII, "autoconfig."); ok {
833 dom.Unicode = strings.TrimPrefix(dom.Unicode, "autoconfig.")
835 // Autodiscovery uses a SRV record. It shouldn't point to a CNAME. So we directly
836 // use the mail server's host name.
837 if dom == mox.Conf.Static.HostnameDomain || dom == mox.Conf.Static.Listeners["public"].HostnameDomain {
840 dc, ok := mox.Conf.Domain(dom)
841 return ok && !dc.ReportsOnly
843 srv.SystemHandle("autoconfig", autoconfigMatch, "/mail/config-v1.1.xml", mox.SafeHeaders(http.HandlerFunc(autoconfHandle)))
844 srv.SystemHandle("autodiscover", autoconfigMatch, "/autodiscover/autodiscover.xml", mox.SafeHeaders(http.HandlerFunc(autodiscoverHandle)))
845 srv.SystemHandle("mobileconfig", autoconfigMatch, "/profile.mobileconfig", mox.SafeHeaders(http.HandlerFunc(mobileconfigHandle)))
846 srv.SystemHandle("mobileconfigqrcodepng", autoconfigMatch, "/profile.mobileconfig.qrcode.png", mox.SafeHeaders(http.HandlerFunc(mobileconfigQRCodeHandle)))
848 if l.MTASTSHTTPS.Enabled {
849 port := config.Port(l.MTASTSHTTPS.Port, 443)
850 srv := ensureServe(!l.MTASTSHTTPS.NonTLS, l.MTASTSHTTPS.Forwarded, false, port, "mtasts-https", false)
851 if l.MTASTSHTTPS.NonTLS {
852 ensureACMEHTTP01(srv)
854 mtastsMatch := func(ipdom dns.IPDomain) bool {
855 // todo: may want to check this against the configured domains, could in theory be just a webserver.
860 return strings.HasPrefix(dom.ASCII, "mta-sts.")
862 srv.SystemHandle("mtasts", mtastsMatch, "/.well-known/mta-sts.txt", mox.SafeHeaders(http.HandlerFunc(mtastsPolicyHandle)))
864 if l.PprofHTTP.Enabled {
865 // Importing net/http/pprof registers handlers on the default serve mux.
866 port := config.Port(l.PprofHTTP.Port, 8011)
867 if _, ok := portServe[port]; ok {
868 pkglog.Fatal("cannot serve pprof on same endpoint as other http services")
870 srv := &serve{[]string{"pprof-http"}, nil, nil, false, false, false, nil, false, nil}
871 portServe[port] = srv
872 srv.SystemHandle("pprof", nil, "/", http.DefaultServeMux)
874 if l.WebserverHTTP.Enabled {
875 port := config.Port(l.WebserverHTTP.Port, 80)
876 srv := ensureServe(false, false, l.WebserverHTTP.RateLimitDisabled, port, "webserver-http", false)
878 ensureACMEHTTP01(srv)
880 if l.WebserverHTTPS.Enabled {
881 port := config.Port(l.WebserverHTTPS.Port, 443)
882 srv := ensureServe(true, false, l.WebserverHTTPS.RateLimitDisabled, port, "webserver-https", false)
886 if l.TLS != nil && l.TLS.ACME != "" {
887 m := mox.Conf.Static.ACME[l.TLS.ACME].Manager
888 if ensureManagerHosts[m] == nil {
889 ensureManagerHosts[m] = map[dns.Domain]struct{}{}
891 hosts := ensureManagerHosts[m]
892 hosts[mox.Conf.Static.HostnameDomain] = struct{}{}
894 if l.HostnameDomain.ASCII != "" {
895 hosts[l.HostnameDomain] = struct{}{}
898 // All domains are served on all listeners. Gather autoconfig hostnames to ensure
899 // presence of TLS certificates. Fetching a certificate on-demand may be too slow
900 // for the timeouts of clients doing autoconfig.
902 if l.AutoconfigHTTPS.Enabled && !l.AutoconfigHTTPS.NonTLS {
903 for _, name := range mox.Conf.Domains() {
904 if dom, err := dns.ParseDomain(name); err != nil {
905 pkglog.Errorx("parsing domain from config", err)
906 } else if d, _ := mox.Conf.Domain(dom); d.ReportsOnly || d.Disabled {
907 // Do not gather autoconfig name if we aren't accepting email for this domain or when it is disabled.
911 autoconfdom, err := dns.ParseDomain("autoconfig." + name)
913 pkglog.Errorx("parsing domain from config for autoconfig", err)
915 hosts[autoconfdom] = struct{}{}
921 if s := portServe[443]; s != nil && s.TLSConfig != nil && len(s.NextProto) > 0 {
922 s.TLSConfig.NextProtos = append(s.TLSConfig.NextProtos, slices.Collect(maps.Keys(s.NextProto))...)
925 for _, srv := range portServe {
926 sortPathHandlers(srv.SystemHandlers)
927 sortPathHandlers(srv.ServiceHandlers)
933func sortPathHandlers(l []pathHandler) {
934 sort.Slice(l, func(i, j int) bool {
937 if len(a) == len(b) {
938 // For consistent order.
941 // Longest paths first.
942 return len(a) > len(b)
946// functions to be launched in goroutine that will serve on a listener.
949// We'll explicitly ensure these TLS certs exist (e.g. are created with ACME)
950// immediately after startup. We only do so for our explicit listener hostnames,
951// not for mta-sts DNS records, it can be requested on demand (perhaps never). We
952// do request autoconfig, otherwise clients may run into their timeouts waiting for
953// the certificate to be given during the first https connection.
954var ensureManagerHosts = map[*autotls.Manager]map[dns.Domain]struct{}{}
956type tlsNextProtoMap = map[string]func(*http.Server, *tls.Conn, http.Handler)
958// listen prepares a listener, and adds it to "servers", to be launched (if not running as root) through Serve.
959func listen1(ip string, port int, tlsConfig *tls.Config, name string, kinds []string, handler http.Handler, nextProto tlsNextProtoMap) {
960 addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
965 if tlsConfig == nil {
967 if os.Getuid() == 0 {
968 pkglog.Print("http listener",
969 slog.String("name", name),
970 slog.String("kinds", strings.Join(kinds, ",")),
971 slog.String("address", addr))
973 ln, err = mox.Listen(mox.Network(ip), addr)
975 pkglog.Fatalx("http: listen", err, slog.Any("addr", addr))
979 if os.Getuid() == 0 {
980 pkglog.Print("https listener",
981 slog.String("name", name),
982 slog.String("kinds", strings.Join(kinds, ",")),
983 slog.String("address", addr))
985 ln, err = mox.Listen(mox.Network(ip), addr)
987 pkglog.Fatalx("https: listen", err, slog.String("addr", addr))
989 ln = tls.NewListener(ln, tlsConfig)
992 server := &http.Server{
994 TLSConfig: tlsConfig,
995 ReadHeaderTimeout: 30 * time.Second,
996 IdleTimeout: 65 * time.Second, // Chrome closes connections after 60 seconds, firefox after 115 seconds.
997 ErrorLog: golog.New(mlog.LogWriter(pkglog.With(slog.String("pkg", "net/http")), slog.LevelInfo, protocol+" error"), "", 0),
998 TLSNextProto: nextProto,
1001 // Set server.Protocols with http2 enabled or http2 won't work with go1.27 with
1002 // http2.ConfigureServer and non-nil server.TLSNextProto.
1003 if tlsConfig != nil {
1004 var p http.Protocols
1007 server.Protocols = &p
1010 // By default, the Go 1.6 and above http.Server includes support for HTTP2.
1011 // However, HTTP2 is negotiated via ALPN. Because we are configuring
1012 // TLSNextProto above, we have to explicitly enable HTTP2 by importing http2
1013 // and calling ConfigureServer.
1014 err = http2.ConfigureServer(server, nil)
1016 pkglog.Fatalx("https: unable to configure http2", err)
1020 err := server.Serve(ln)
1021 pkglog.Fatalx(protocol+": serve", err)
1023 servers = append(servers, serve)
1026// Serve starts serving on the initialized listeners.
1028 loadStaticGzipCache(mox.DataDirPath("tmp/httpstaticcompresscache"), 512*1024*1024)
1030 go webaccount.ImportManage()
1032 for _, serve := range servers {
1038 time.Sleep(1 * time.Second)
1040 for m, hosts := range ensureManagerHosts {
1041 for host := range hosts {
1042 // Check if certificate is already available. If so, we don't print as much after a
1043 // restart, and finish more quickly if only a few certificates are missing/old.
1044 if avail, err := m.CertAvailable(mox.Shutdown, pkglog, host); err != nil {
1045 pkglog.Errorx("checking acme certificate availability", err, slog.Any("host", host))
1051 // Just in case someone adds quite some domains to their config. We don't want to
1052 // hit any ACME rate limits.
1056 // Sleep just a little. We don't want to hammer our ACME provider, e.g. Let's Encrypt.
1057 time.Sleep(10 * time.Second)
1061 hello := &tls.ClientHelloInfo{
1062 ServerName: host.ASCII,
1064 // Make us fetch an ECDSA P256 cert.
1065 // We add TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 to get around the ecDSA check in autocert.
1066 CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_AES_128_GCM_SHA256},
1067 SupportedCurves: []tls.CurveID{tls.CurveP256},
1068 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
1069 SupportedVersions: []uint16{tls.VersionTLS13},
1071 pkglog.Print("ensuring certificate availability", slog.Any("hostname", host))
1072 if _, err := m.Manager.GetCertificate(hello); err != nil {
1073 pkglog.Errorx("requesting automatic certificate", err, slog.Any("hostname", host))