7 cryptorand "crypto/rand"
22 "github.com/prometheus/client_golang/prometheus"
23 "github.com/prometheus/client_golang/prometheus/promauto"
25 "github.com/mjl-/mox/dns"
26 "github.com/mjl-/mox/dnsbl"
27 "github.com/mjl-/mox/message"
28 "github.com/mjl-/mox/metrics"
29 "github.com/mjl-/mox/mlog"
30 "github.com/mjl-/mox/mox-"
31 "github.com/mjl-/mox/moxvar"
32 "github.com/mjl-/mox/queue"
33 "github.com/mjl-/mox/store"
34 "github.com/mjl-/mox/updates"
37var metricDNSBL = promauto.NewGaugeVec(
39 Name: "mox_dnsbl_ips_success",
40 Help: "DNSBL lookups to configured DNSBLs of our IPs.",
48func monitorDNSBL(log mlog.Log) {
50 // On error, don't bring down the entire server.
53 log.Error("monitordnsbl panic", slog.Any("panic", x))
55 metrics.PanicInc(metrics.Serve)
59 publicListener := mox.Conf.Static.Listeners["public"]
61 // We keep track of the previous metric values, so we can delete those we no longer
67 prevResults := map[key]struct{}{}
69 // Last time we checked, and how many outgoing delivery connections were made at that time.
73 resolver := dns.StrictResolver{Pkg: "dnsblmonitor"}
74 var sleep time.Duration // No sleep on first iteration.
77 // We check more often when we send more. Every 100 messages, and between 5 mins
79 conns := queue.ConnectionCounter()
80 if sleep > 0 && conns < lastConns+100 && time.Since(last) < 3*time.Hour {
83 sleep = 5 * time.Minute
88 zones := append([]dns.Domain{}, publicListener.SMTP.DNSBLZones...)
89 conf := mox.Conf.DynamicConfig()
90 for _, zone := range conf.MonitorDNSBLZones {
91 if !slices.Contains(zones, zone) {
92 zones = append(zones, zone)
96 ips, err := mox.IPs(mox.Context, false)
98 log.Errorx("listing ips for dnsbl monitor", err)
99 // Mark checks as broken.
100 for k := range prevResults {
101 metricDNSBL.WithLabelValues(k.zone.Name(), k.ip).Set(-1)
105 var publicIPs []net.IP
106 var publicIPstrs []string
107 for _, ip := range ips {
108 if ip.IsLoopback() || ip.IsPrivate() {
111 publicIPs = append(publicIPs, ip)
112 publicIPstrs = append(publicIPstrs, ip.String())
115 // Remove labels that no longer exist from metric.
116 for k := range prevResults {
117 if !slices.Contains(zones, k.zone) || !slices.Contains(publicIPstrs, k.ip) {
118 metricDNSBL.DeleteLabelValues(k.zone.Name(), k.ip)
119 delete(prevResults, k)
123 // Do DNSBL checks and update metric.
124 for _, ip := range publicIPs {
125 for _, zone := range zones {
126 status, expl, err := dnsbl.Lookup(mox.Context, log.Logger, resolver, zone, ip)
128 log.Errorx("dnsbl monitor lookup", err,
130 slog.Any("zone", zone),
131 slog.String("expl", expl),
132 slog.Any("status", status))
135 if status == dnsbl.StatusPass {
138 metricDNSBL.WithLabelValues(zone.Name(), ip.String()).Set(v)
139 k := key{zone, ip.String()}
140 prevResults[k] = struct{}{}
142 time.Sleep(time.Second)
148// also see localserve.go, code is similar or even shared.
149func cmdServe(c *cmd) {
150 c.help = `Start mox, serving SMTP/IMAP/HTTPS.
152Incoming email is accepted over SMTP. Email can be retrieved by users using
153IMAP. HTTP listeners are started for the admin/account web interfaces, and for
154automated TLS configuration. Missing essential TLS certificates are immediately
155requested, other TLS certificates are requested on demand.
157Only implemented on unix systems, not Windows.
164 // Set debug logging until config is fully loaded.
166 mox.Conf.Log[""] = mlog.LevelDebug
167 mlog.SetConfig(mox.Conf.Log)
169 checkACMEHosts := os.Getuid() != 0
173 if os.Getuid() == 0 {
174 mox.MustLoadConfig(true, checkACMEHosts)
176 // No need to potentially start and keep multiple processes. As root, we just need
177 // to start the child process.
178 runtime.GOMAXPROCS(1)
180 moxconf, err := filepath.Abs(mox.ConfigStaticPath)
181 log.Check(err, "finding absolute mox.conf path")
182 domainsconf, err := filepath.Abs(mox.ConfigDynamicPath)
183 log.Check(err, "finding absolute domains.conf path")
185 log.Print("starting as root, initializing network listeners",
186 slog.String("version", moxvar.Version),
187 slog.Any("pid", os.Getpid()),
188 slog.String("moxconf", moxconf),
189 slog.String("domainsconf", domainsconf))
190 if os.Getenv("MOX_SOCKETS") != "" {
191 log.Fatal("refusing to start as root with $MOX_SOCKETS set")
193 if os.Getenv("MOX_FILES") != "" {
194 log.Fatal("refusing to start as root with $MOX_FILES set")
197 if !mox.Conf.Static.NoFixPermissions {
198 // Fix permissions now that we have privilege to do so. Useful for update of v0.0.1
199 // that was running directly as mox-user.
200 workdir, err := os.Getwd()
202 log.Printx("get working dir, continuing without potentially fixing up permissions", err)
204 configdir := filepath.Dir(mox.ConfigStaticPath)
205 datadir := mox.DataDirPath(".")
206 err := fixperms(log, workdir, configdir, datadir, mox.Conf.Static.UID, mox.Conf.Static.GID)
208 log.Fatalx("fixing permissions", err)
213 mox.RestorePassedFiles()
214 mox.MustLoadConfig(true, checkACMEHosts)
215 log.Print("starting as unprivileged user",
216 slog.String("user", mox.Conf.Static.User),
217 slog.Any("uid", mox.Conf.Static.UID),
218 slog.Any("gid", mox.Conf.Static.GID),
219 slog.Any("pid", os.Getpid()))
222 syscall.Umask(syscall.Umask(007) | 007)
224 // Initialize key and random buffer for creating opaque SMTP
225 // transaction IDs based on "cid"s.
226 recvidpath := mox.DataDirPath("receivedid.key")
227 recvidbuf, err := os.ReadFile(recvidpath)
228 if err != nil || len(recvidbuf) != 16+8 {
229 recvidbuf = make([]byte, 16+8)
230 if _, err := cryptorand.Read(recvidbuf); err != nil {
231 log.Fatalx("reading random recvid data", err)
233 if err := os.WriteFile(recvidpath, recvidbuf, 0660); err != nil {
234 log.Fatalx("writing recvidpath", err, slog.String("path", recvidpath))
236 err := os.Chown(recvidpath, int(mox.Conf.Static.UID), 0)
237 log.Check(err, "chown receveidid.key",
238 slog.String("path", recvidpath),
239 slog.Any("uid", mox.Conf.Static.UID),
241 err = os.Chmod(recvidpath, 0640)
242 log.Check(err, "chmod receveidid.key to 0640", slog.String("path", recvidpath))
244 if err := mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:]); err != nil {
245 log.Fatalx("init receivedid", err)
248 // Start mox. If running as root, this will bind/listen on network sockets, and
249 // fork and exec itself as unprivileged user, then waits for the child to stop and
250 // exit. When running as root, this function never returns. But the new
251 // unprivileged user will get here again, with network sockets prepared.
253 // We listen to the unix domain ctl socket afterwards, which we always remove
254 // before listening. We need to do that because we may not have cleaned up our
255 // control socket during unexpected shutdown. We don't want to remove and listen on
256 // the unix domain socket first. If we would, we would make the existing instance
257 // unreachable over its ctl socket, and then fail because the network addresses are
259 const mtastsdbRefresher = true
260 const skipForkExec = false
261 if err := start(mtastsdbRefresher, !mox.Conf.Static.NoOutgoingDMARCReports, !mox.Conf.Static.NoOutgoingTLSReports, skipForkExec); err != nil {
262 log.Fatalx("start", err)
264 log.Print("ready to serve")
266 if mox.Conf.Static.CheckUpdates {
267 checkUpdates := func() time.Duration {
268 next := 24 * time.Hour
269 current, lastknown, mtime, err := mox.LastKnown()
271 log.Infox("determining own version before checking for updates, trying again in 24h", err)
275 // We don't want to check for updates at every startup. So we sleep based on file
276 // mtime. But file won't exist initially.
277 if !mtime.IsZero() && time.Since(mtime) < 24*time.Hour {
278 d := 24*time.Hour - time.Since(mtime)
279 log.Debug("sleeping for next check for updates", slog.Duration("sleep", d))
284 if err := os.Chtimes(mox.DataDirPath("lastknownversion"), now, now); err != nil {
285 if !os.IsNotExist(err) {
286 log.Infox("setting mtime on lastknownversion file, continuing", err)
290 log.Debug("checking for updates", slog.Any("lastknown", lastknown))
291 updatesctx, updatescancel := context.WithTimeout(mox.Context, time.Minute)
292 latest, _, changelog, err := updates.Check(updatesctx, log.Logger, dns.StrictResolver{Log: log.Logger}, dns.Domain{ASCII: changelogDomain}, lastknown, changelogURL, changelogPubKey)
295 log.Infox("checking for updates", err, slog.Any("latest", latest))
298 if !latest.After(lastknown) {
299 log.Debug("no new version available")
302 if len(changelog.Changes) == 0 {
303 log.Info("new version available, but changelog is empty, ignoring", slog.Any("latest", latest))
308 for _, c := range changelog.Changes {
309 cl += "----\n\n" + strings.TrimSpace(c.Text) + "\n\n"
313 a, err := store.OpenAccount(log, mox.Conf.Static.Postmaster.Account)
315 log.Infox("open account for postmaster changelog delivery", err)
320 log.Check(err, "closing account")
322 f, err := store.CreateMessageTemp(log, "changelog")
324 log.Infox("making temporary message file for changelog delivery", err)
327 defer store.CloseRemoveTempFile(log, f, "message for changelog delivery")
330 Received: time.Now(),
331 Flags: store.Flags{Flagged: true},
333 n, err := fmt.Fprintf(f, "Date: %s\r\nSubject: mox %s available\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: 8-bit\r\n\r\nHi!\r\n\r\nVersion %s of mox is available, this install is at %s.\r\n\r\nChanges:\r\n\r\n%s\r\n\r\nRemember to make a backup with \"mox backup\" before upgrading.\r\nPlease report any issues at https://github.com/mjl-/mox, thanks!\r\n\r\nCheers,\r\nmox\r\n", time.Now().Format(message.RFC5322Z), latest, latest, current, strings.ReplaceAll(cl, "\n", "\r\n"))
335 log.Infox("writing temporary message file for changelog delivery", err)
342 derr = a.DeliverMailbox(log, mox.Conf.Static.Postmaster.Mailbox, &m, f)
345 log.Errorx("changelog delivery", derr)
349 log.Info("delivered changelog",
350 slog.Any("current", current),
351 slog.Any("lastknown", lastknown),
352 slog.Any("latest", latest))
353 if err := mox.StoreLastKnown(latest); err != nil {
354 // This will be awkward, we'll keep notifying the postmaster once every 24h...
355 log.Infox("updating last known version", err)
362 next := checkUpdates()
370 ctlpath := mox.DataDirPath("ctl")
371 _ = os.Remove(ctlpath)
372 ctl, err := net.Listen("unix", ctlpath)
374 log.Fatalx("listen on ctl unix domain socket", err)
378 conn, err := ctl.Accept()
380 log.Printx("accept for ctl", err)
384 ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
385 go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) })
389 // Remove old temporary files that somehow haven't been cleaned up.
390 tmpdir := mox.DataDirPath("tmp")
391 os.MkdirAll(tmpdir, 0770)
392 tmps, err := os.ReadDir(tmpdir)
394 log.Errorx("listing files in tmpdir", err)
397 for _, e := range tmps {
398 if fi, err := e.Info(); err != nil {
399 log.Errorx("stat tmp file", err, slog.String("filename", e.Name()))
400 } else if now.Sub(fi.ModTime()) > 7*24*time.Hour && !fi.IsDir() {
401 p := filepath.Join(tmpdir, e.Name())
402 if err := os.Remove(p); err != nil {
403 log.Errorx("removing stale temporary file", err, slog.String("path", p))
405 log.Info("removed stale temporary file", slog.String("path", p))
411 // Graceful shutdown.
412 sigc := make(chan os.Signal, 1)
413 signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
415 log.Print("shutting down, waiting max 3s for existing connections", slog.Any("signal", sig))
417 if num, ok := sig.(syscall.Signal); ok {
424// Set correct permissions for mox working directory, binary, config and data and service file.
426// We require being able to stat the basic non-optional paths. Then we'll try to
427// fix up permissions. If an error occurs when fixing permissions, we log and
428// continue (could not be an actual problem).
429func fixperms(log mlog.Log, workdir, configdir, datadir string, moxuid, moxgid uint32) (rerr error) {
430 type fserr struct{ Err error }
444 checkf := func(err error, format string, args ...any) {
446 panic(fserr{fmt.Errorf(format, args...)})
450 // Changes we have to make. We collect them first, then apply.
454 olduid, oldgid uint32
460 ensure := func(p string, uid, gid uint32, perm fs.FileMode) bool {
461 fi, err := os.Stat(p)
462 checkf(err, "stat %s", p)
464 st, ok := fi.Sys().(*syscall.Stat_t)
466 checkf(fmt.Errorf("got %T", st), "stat sys, expected syscall.Stat_t")
470 if st.Uid != uid || st.Gid != gid {
476 if perm != fi.Mode()&(fs.ModeSetgid|0777) {
478 ch.oldmode = fi.Mode() & (fs.ModeSetgid | 0777)
480 var zerochange change
481 if ch == zerochange {
485 changes = append(changes, ch)
489 xexists := func(p string) bool {
491 if err != nil && !os.IsNotExist(err) {
492 checkf(err, "stat %s", p)
497 // We ensure these permissions:
499 // $workdir root:mox 0751
500 // $configdir mox:root 0750 + setgid, and recursively (but files 0640)
501 // $datadir mox:root 0750 + setgid, and recursively (but files 0640)
502 // $workdir/mox (binary, optional) root:mox 0750
503 // $workdir/mox.service (systemd service file, optional) root:root 0644
506 ensure(workdir, root, moxgid, 0751)
507 fixconfig := ensure(configdir, moxuid, 0, fs.ModeSetgid|0750)
508 fixdata := ensure(datadir, moxuid, 0, fs.ModeSetgid|0750)
510 // Binary and systemd service file do not exist (there) when running under docker.
511 binary := filepath.Join(workdir, "mox")
513 ensure(binary, root, moxgid, 0750)
515 svc := filepath.Join(workdir, "mox.service")
517 ensure(svc, root, root, 0644)
520 if len(changes) == 0 {
525 log.Print("fixing up permissions, will continue on errors")
526 for _, ch := range changes {
528 err := os.Chown(ch.path, int(*ch.uid), int(*ch.gid))
529 log.Printx("chown, fixing uid/gid", err,
530 slog.String("path", ch.path),
531 slog.Any("olduid", ch.olduid),
532 slog.Any("oldgid", ch.oldgid),
533 slog.Any("newuid", *ch.uid),
534 slog.Any("newgid", *ch.gid))
537 err := os.Chmod(ch.path, *ch.mode)
538 log.Printx("chmod, fixing permissions", err,
539 slog.String("path", ch.path),
540 slog.Any("oldmode", fmt.Sprintf("%03o", ch.oldmode)),
541 slog.Any("newmode", fmt.Sprintf("%03o", *ch.mode)))
545 walkchange := func(dir string) {
546 err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
548 log.Printx("walk error, continuing", err, slog.String("path", path))
553 log.Printx("stat during walk, continuing", err, slog.String("path", path))
556 st, ok := fi.Sys().(*syscall.Stat_t)
558 log.Printx("syscall stat during walk, continuing", err, slog.String("path", path))
561 if st.Uid != moxuid || st.Gid != root {
562 err := os.Chown(path, int(moxuid), root)
563 log.Printx("walk chown, fixing uid/gid", err,
564 slog.String("path", path),
565 slog.Any("olduid", st.Uid),
566 slog.Any("oldgid", st.Gid),
567 slog.Any("newuid", moxuid),
568 slog.Any("newgid", root))
570 omode := fi.Mode() & (fs.ModeSetgid | 0777)
571 var nmode fs.FileMode
573 nmode = fs.ModeSetgid | 0750
578 err := os.Chmod(path, nmode)
579 log.Printx("walk chmod, fixing permissions", err,
580 slog.String("path", path),
581 slog.Any("oldmode", fmt.Sprintf("%03o", omode)),
582 slog.Any("newmode", fmt.Sprintf("%03o", nmode)))
586 log.Check(err, "walking dir to fix permissions", slog.String("dir", dir))
589 // If config or data dir needed fixing, also set uid/gid and mode and files/dirs
590 // inside, recursively. We don't always recurse, data probably contains many files.
592 log.Print("fixing permissions in config dir", slog.String("configdir", configdir))
593 walkchange(configdir)
596 log.Print("fixing permissions in data dir", slog.String("configdir", configdir))