1//go:build !windows
2
3package main
4
5import (
6 "context"
7 cryptorand "crypto/rand"
8 "fmt"
9 "io/fs"
10 "log/slog"
11 "net"
12 "os"
13 "os/signal"
14 "path/filepath"
15 "runtime"
16 "runtime/debug"
17 "slices"
18 "strings"
19 "syscall"
20 "time"
21
22 "github.com/prometheus/client_golang/prometheus"
23 "github.com/prometheus/client_golang/prometheus/promauto"
24
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"
35)
36
37var metricDNSBL = promauto.NewGaugeVec(
38 prometheus.GaugeOpts{
39 Name: "mox_dnsbl_ips_success",
40 Help: "DNSBL lookups to configured DNSBLs of our IPs.",
41 },
42 []string{
43 "zone",
44 "ip",
45 },
46)
47
48func monitorDNSBL(log mlog.Log) {
49 defer func() {
50 // On error, don't bring down the entire server.
51 x := recover()
52 if x != nil {
53 log.Error("monitordnsbl panic", slog.Any("panic", x))
54 debug.PrintStack()
55 metrics.PanicInc(metrics.Serve)
56 }
57 }()
58
59 publicListener := mox.Conf.Static.Listeners["public"]
60
61 // We keep track of the previous metric values, so we can delete those we no longer
62 // monitor.
63 type key struct {
64 zone dns.Domain
65 ip string
66 }
67 prevResults := map[key]struct{}{}
68
69 // Last time we checked, and how many outgoing delivery connections were made at that time.
70 var last time.Time
71 var lastConns int64
72
73 resolver := dns.StrictResolver{Pkg: "dnsblmonitor"}
74 var sleep time.Duration // No sleep on first iteration.
75 for {
76 time.Sleep(sleep)
77 // We check more often when we send more. Every 100 messages, and between 5 mins
78 // and 3 hours.
79 conns := queue.ConnectionCounter()
80 if sleep > 0 && conns < lastConns+100 && time.Since(last) < 3*time.Hour {
81 continue
82 }
83 sleep = 5 * time.Minute
84 lastConns = conns
85 last = time.Now()
86
87 // Gather zones.
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)
93 }
94 }
95 // And gather IPs.
96 ips, err := mox.IPs(mox.Context, false)
97 if err != nil {
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)
102 }
103 continue
104 }
105 var publicIPs []net.IP
106 var publicIPstrs []string
107 for _, ip := range ips {
108 if ip.IsLoopback() || ip.IsPrivate() {
109 continue
110 }
111 publicIPs = append(publicIPs, ip)
112 publicIPstrs = append(publicIPstrs, ip.String())
113 }
114
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)
120 }
121 }
122
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)
127 if err != nil {
128 log.Errorx("dnsbl monitor lookup", err,
129 slog.Any("ip", ip),
130 slog.Any("zone", zone),
131 slog.String("expl", expl),
132 slog.Any("status", status))
133 }
134 var v float64
135 if status == dnsbl.StatusPass {
136 v = 1
137 }
138 metricDNSBL.WithLabelValues(zone.Name(), ip.String()).Set(v)
139 k := key{zone, ip.String()}
140 prevResults[k] = struct{}{}
141
142 time.Sleep(time.Second)
143 }
144 }
145 }
146}
147
148// also see localserve.go, code is similar or even shared.
149func cmdServe(c *cmd) {
150 c.help = `Start mox, serving SMTP/IMAP/HTTPS.
151
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.
156
157Only implemented on unix systems, not Windows.
158`
159 args := c.Parse()
160 if len(args) != 0 {
161 c.Usage()
162 }
163
164 // Set debug logging until config is fully loaded.
165 mlog.Logfmt = true
166 mox.Conf.Log[""] = mlog.LevelDebug
167 mlog.SetConfig(mox.Conf.Log)
168
169 checkACMEHosts := os.Getuid() != 0
170
171 log := c.log
172
173 if os.Getuid() == 0 {
174 mox.MustLoadConfig(true, checkACMEHosts)
175
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)
179
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")
184
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")
192 }
193 if os.Getenv("MOX_FILES") != "" {
194 log.Fatal("refusing to start as root with $MOX_FILES set")
195 }
196
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()
201 if err != nil {
202 log.Printx("get working dir, continuing without potentially fixing up permissions", err)
203 } else {
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)
207 if err != nil {
208 log.Fatalx("fixing permissions", err)
209 }
210 }
211 }
212 } else {
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()))
220 }
221
222 syscall.Umask(syscall.Umask(007) | 007)
223
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)
232 }
233 if err := os.WriteFile(recvidpath, recvidbuf, 0660); err != nil {
234 log.Fatalx("writing recvidpath", err, slog.String("path", recvidpath))
235 }
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),
240 slog.Any("gid", 0))
241 err = os.Chmod(recvidpath, 0640)
242 log.Check(err, "chmod receveidid.key to 0640", slog.String("path", recvidpath))
243 }
244 if err := mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:]); err != nil {
245 log.Fatalx("init receivedid", err)
246 }
247
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.
252 //
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
258 // taken.
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)
263 }
264 log.Print("ready to serve")
265
266 if mox.Conf.Static.CheckUpdates {
267 checkUpdates := func() time.Duration {
268 next := 24 * time.Hour
269 current, lastknown, mtime, err := mox.LastKnown()
270 if err != nil {
271 log.Infox("determining own version before checking for updates, trying again in 24h", err)
272 return next
273 }
274
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))
280 time.Sleep(d)
281 next = 0
282 }
283 now := time.Now()
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)
287 }
288 }
289
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)
293 updatescancel()
294 if err != nil {
295 log.Infox("checking for updates", err, slog.Any("latest", latest))
296 return next
297 }
298 if !latest.After(lastknown) {
299 log.Debug("no new version available")
300 return next
301 }
302 if len(changelog.Changes) == 0 {
303 log.Info("new version available, but changelog is empty, ignoring", slog.Any("latest", latest))
304 return next
305 }
306
307 var cl string
308 for _, c := range changelog.Changes {
309 cl += "----\n\n" + strings.TrimSpace(c.Text) + "\n\n"
310 }
311 cl += "----"
312
313 a, err := store.OpenAccount(log, mox.Conf.Static.Postmaster.Account)
314 if err != nil {
315 log.Infox("open account for postmaster changelog delivery", err)
316 return next
317 }
318 defer func() {
319 err := a.Close()
320 log.Check(err, "closing account")
321 }()
322 f, err := store.CreateMessageTemp(log, "changelog")
323 if err != nil {
324 log.Infox("making temporary message file for changelog delivery", err)
325 return next
326 }
327 defer store.CloseRemoveTempFile(log, f, "message for changelog delivery")
328
329 m := store.Message{
330 Received: time.Now(),
331 Flags: store.Flags{Flagged: true},
332 }
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"))
334 if err != nil {
335 log.Infox("writing temporary message file for changelog delivery", err)
336 return next
337 }
338 m.Size = int64(n)
339
340 var derr error
341 a.WithWLock(func() {
342 derr = a.DeliverMailbox(log, mox.Conf.Static.Postmaster.Mailbox, &m, f)
343 })
344 if derr != nil {
345 log.Errorx("changelog delivery", derr)
346 return next
347 }
348
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)
356 }
357 return next
358 }
359
360 go func() {
361 for {
362 next := checkUpdates()
363 time.Sleep(next)
364 }
365 }()
366 }
367
368 go monitorDNSBL(log)
369
370 ctlpath := mox.DataDirPath("ctl")
371 _ = os.Remove(ctlpath)
372 ctl, err := net.Listen("unix", ctlpath)
373 if err != nil {
374 log.Fatalx("listen on ctl unix domain socket", err)
375 }
376 go func() {
377 for {
378 conn, err := ctl.Accept()
379 if err != nil {
380 log.Printx("accept for ctl", err)
381 continue
382 }
383 cid := mox.Cid()
384 ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
385 go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) })
386 }
387 }()
388
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)
393 if err != nil {
394 log.Errorx("listing files in tmpdir", err)
395 } else {
396 now := time.Now()
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))
404 } else {
405 log.Info("removed stale temporary file", slog.String("path", p))
406 }
407 }
408 }
409 }
410
411 // Graceful shutdown.
412 sigc := make(chan os.Signal, 1)
413 signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
414 sig := <-sigc
415 log.Print("shutting down, waiting max 3s for existing connections", slog.Any("signal", sig))
416 shutdown(log)
417 if num, ok := sig.(syscall.Signal); ok {
418 os.Exit(int(num))
419 } else {
420 os.Exit(1)
421 }
422}
423
424// Set correct permissions for mox working directory, binary, config and data and service file.
425//
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 }
431 defer func() {
432 x := recover()
433 if x == nil {
434 return
435 }
436 e, ok := x.(fserr)
437 if ok {
438 rerr = e.Err
439 } else {
440 panic(x)
441 }
442 }()
443
444 checkf := func(err error, format string, args ...any) {
445 if err != nil {
446 panic(fserr{fmt.Errorf(format, args...)})
447 }
448 }
449
450 // Changes we have to make. We collect them first, then apply.
451 type change struct {
452 path string
453 uid, gid *uint32
454 olduid, oldgid uint32
455 mode *fs.FileMode
456 oldmode fs.FileMode
457 }
458 var changes []change
459
460 ensure := func(p string, uid, gid uint32, perm fs.FileMode) bool {
461 fi, err := os.Stat(p)
462 checkf(err, "stat %s", p)
463
464 st, ok := fi.Sys().(*syscall.Stat_t)
465 if !ok {
466 checkf(fmt.Errorf("got %T", st), "stat sys, expected syscall.Stat_t")
467 }
468
469 var ch change
470 if st.Uid != uid || st.Gid != gid {
471 ch.uid = &uid
472 ch.gid = &gid
473 ch.olduid = st.Uid
474 ch.oldgid = st.Gid
475 }
476 if perm != fi.Mode()&(fs.ModeSetgid|0777) {
477 ch.mode = &perm
478 ch.oldmode = fi.Mode() & (fs.ModeSetgid | 0777)
479 }
480 var zerochange change
481 if ch == zerochange {
482 return false
483 }
484 ch.path = p
485 changes = append(changes, ch)
486 return true
487 }
488
489 xexists := func(p string) bool {
490 _, err := os.Stat(p)
491 if err != nil && !os.IsNotExist(err) {
492 checkf(err, "stat %s", p)
493 }
494 return err == nil
495 }
496
497 // We ensure these permissions:
498 //
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
504
505 const root = 0
506 ensure(workdir, root, moxgid, 0751)
507 fixconfig := ensure(configdir, moxuid, 0, fs.ModeSetgid|0750)
508 fixdata := ensure(datadir, moxuid, 0, fs.ModeSetgid|0750)
509
510 // Binary and systemd service file do not exist (there) when running under docker.
511 binary := filepath.Join(workdir, "mox")
512 if xexists(binary) {
513 ensure(binary, root, moxgid, 0750)
514 }
515 svc := filepath.Join(workdir, "mox.service")
516 if xexists(svc) {
517 ensure(svc, root, root, 0644)
518 }
519
520 if len(changes) == 0 {
521 return
522 }
523
524 // Apply changes.
525 log.Print("fixing up permissions, will continue on errors")
526 for _, ch := range changes {
527 if ch.uid != nil {
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))
535 }
536 if ch.mode != nil {
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)))
542 }
543 }
544
545 walkchange := func(dir string) {
546 err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
547 if err != nil {
548 log.Printx("walk error, continuing", err, slog.String("path", path))
549 return nil
550 }
551 fi, err := d.Info()
552 if err != nil {
553 log.Printx("stat during walk, continuing", err, slog.String("path", path))
554 return nil
555 }
556 st, ok := fi.Sys().(*syscall.Stat_t)
557 if !ok {
558 log.Printx("syscall stat during walk, continuing", err, slog.String("path", path))
559 return nil
560 }
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))
569 }
570 omode := fi.Mode() & (fs.ModeSetgid | 0777)
571 var nmode fs.FileMode
572 if fi.IsDir() {
573 nmode = fs.ModeSetgid | 0750
574 } else {
575 nmode = 0640
576 }
577 if omode != nmode {
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)))
583 }
584 return nil
585 })
586 log.Check(err, "walking dir to fix permissions", slog.String("dir", dir))
587 }
588
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.
591 if fixconfig {
592 log.Print("fixing permissions in config dir", slog.String("configdir", configdir))
593 walkchange(configdir)
594 }
595 if fixdata {
596 log.Print("fixing permissions in data dir", slog.String("configdir", configdir))
597 walkchange(datadir)
598 }
599 return nil
600}
601