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 := slices.Clone(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 cryptorand.Read(recvidbuf)
231 if err := os.WriteFile(recvidpath, recvidbuf, 0660); err != nil {
232 log.Fatalx("writing recvidpath", err, slog.String("path", recvidpath))
233 }
234 err := os.Chown(recvidpath, int(mox.Conf.Static.UID), 0)
235 log.Check(err, "chown receveidid.key",
236 slog.String("path", recvidpath),
237 slog.Any("uid", mox.Conf.Static.UID),
238 slog.Any("gid", 0))
239 err = os.Chmod(recvidpath, 0640)
240 log.Check(err, "chmod receveidid.key to 0640", slog.String("path", recvidpath))
241 }
242 if err := mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:]); err != nil {
243 log.Fatalx("init receivedid", err)
244 }
245
246 // Start mox. If running as root, this will bind/listen on network sockets, and
247 // fork and exec itself as unprivileged user, then waits for the child to stop and
248 // exit. When running as root, this function never returns. But the new
249 // unprivileged user will get here again, with network sockets prepared.
250 //
251 // We listen to the unix domain ctl socket afterwards, which we always remove
252 // before listening. We need to do that because we may not have cleaned up our
253 // control socket during unexpected shutdown. We don't want to remove and listen on
254 // the unix domain socket first. If we would, we would make the existing instance
255 // unreachable over its ctl socket, and then fail because the network addresses are
256 // taken.
257 const mtastsdbRefresher = true
258 const skipForkExec = false
259 if err := start(mtastsdbRefresher, !mox.Conf.Static.NoOutgoingDMARCReports, !mox.Conf.Static.NoOutgoingTLSReports, skipForkExec); err != nil {
260 log.Fatalx("start", err)
261 }
262 log.Print("ready to serve")
263
264 if mox.Conf.Static.CheckUpdates {
265 checkUpdates := func() time.Duration {
266 next := 24 * time.Hour
267 current, lastknown, mtime, err := store.LastKnown()
268 if err != nil {
269 log.Infox("determining own version before checking for updates, trying again in 24h", err)
270 return next
271 }
272
273 // We don't want to check for updates at every startup. So we sleep based on file
274 // mtime. But file won't exist initially.
275 if !mtime.IsZero() && time.Since(mtime) < 24*time.Hour {
276 d := 24*time.Hour - time.Since(mtime)
277 log.Debug("sleeping for next check for updates", slog.Duration("sleep", d))
278 time.Sleep(d)
279 next = 0
280 }
281 now := time.Now()
282 if err := os.Chtimes(mox.DataDirPath("lastknownversion"), now, now); err != nil {
283 if !os.IsNotExist(err) {
284 log.Infox("setting mtime on lastknownversion file, continuing", err)
285 }
286 }
287
288 log.Debug("checking for updates", slog.Any("lastknown", lastknown))
289 updatesctx, updatescancel := context.WithTimeout(mox.Context, time.Minute)
290 latest, _, changelog, err := updates.Check(updatesctx, log.Logger, dns.StrictResolver{Log: log.Logger}, dns.Domain{ASCII: changelogDomain}, lastknown, changelogURL, changelogPubKey)
291 updatescancel()
292 if err != nil {
293 log.Infox("checking for updates", err, slog.Any("latest", latest))
294 return next
295 }
296 if !latest.After(lastknown) {
297 log.Debug("no new version available")
298 return next
299 }
300 if len(changelog.Changes) == 0 {
301 log.Info("new version available, but changelog is empty, ignoring", slog.Any("latest", latest))
302 return next
303 }
304
305 var cl string
306 for _, c := range changelog.Changes {
307 cl += "----\n\n" + strings.TrimSpace(c.Text) + "\n\n"
308 }
309 cl += "----"
310
311 a, err := store.OpenAccount(log, mox.Conf.Static.Postmaster.Account, false)
312 if err != nil {
313 log.Infox("open account for postmaster changelog delivery", err)
314 return next
315 }
316 defer func() {
317 err := a.Close()
318 log.Check(err, "closing account")
319 }()
320 f, err := store.CreateMessageTemp(log, "changelog")
321 if err != nil {
322 log.Infox("making temporary message file for changelog delivery", err)
323 return next
324 }
325 defer store.CloseRemoveTempFile(log, f, "message for changelog delivery")
326
327 m := store.Message{
328 Received: time.Now(),
329 Flags: store.Flags{Flagged: true},
330 }
331 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\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"))
332 if err != nil {
333 log.Infox("writing temporary message file for changelog delivery", err)
334 return next
335 }
336 m.Size = int64(n)
337
338 var derr error
339 a.WithWLock(func() {
340 derr = a.DeliverMailbox(log, mox.Conf.Static.Postmaster.Mailbox, &m, f)
341 })
342 if derr != nil {
343 log.Errorx("changelog delivery", derr)
344 return next
345 }
346
347 log.Info("delivered changelog",
348 slog.Any("current", current),
349 slog.Any("lastknown", lastknown),
350 slog.Any("latest", latest))
351 if err := store.StoreLastKnown(latest); err != nil {
352 // This will be awkward, we'll keep notifying the postmaster once every 24h...
353 log.Infox("updating last known version", err)
354 }
355 return next
356 }
357
358 go func() {
359 for {
360 next := checkUpdates()
361 time.Sleep(next)
362 }
363 }()
364 }
365
366 go monitorDNSBL(log)
367
368 ctlpath := mox.DataDirPath("ctl")
369 _ = os.Remove(ctlpath)
370 ctl, err := net.Listen("unix", ctlpath)
371 if err != nil {
372 log.Fatalx("listen on ctl unix domain socket", err)
373 }
374 go func() {
375 for {
376 conn, err := ctl.Accept()
377 if err != nil {
378 log.Printx("accept for ctl", err)
379 continue
380 }
381 cid := mox.Cid()
382 ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
383 go servectl(ctx, cid, log.WithCid(cid), conn, func() { shutdown(log) })
384 }
385 }()
386
387 // Remove old temporary files that somehow haven't been cleaned up.
388 tmpdir := mox.DataDirPath("tmp")
389 os.MkdirAll(tmpdir, 0770)
390 tmps, err := os.ReadDir(tmpdir)
391 if err != nil {
392 log.Errorx("listing files in tmpdir", err)
393 } else {
394 now := time.Now()
395 for _, e := range tmps {
396 if fi, err := e.Info(); err != nil {
397 log.Errorx("stat tmp file", err, slog.String("filename", e.Name()))
398 } else if now.Sub(fi.ModTime()) > 7*24*time.Hour && !fi.IsDir() {
399 p := filepath.Join(tmpdir, e.Name())
400 if err := os.Remove(p); err != nil {
401 log.Errorx("removing stale temporary file", err, slog.String("path", p))
402 } else {
403 log.Info("removed stale temporary file", slog.String("path", p))
404 }
405 }
406 }
407 }
408
409 // Graceful shutdown.
410 sigc := make(chan os.Signal, 1)
411 signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
412 sig := <-sigc
413 log.Print("shutting down, waiting max 3s for existing connections", slog.Any("signal", sig))
414 shutdown(log)
415 if num, ok := sig.(syscall.Signal); ok {
416 os.Exit(int(num))
417 } else {
418 os.Exit(1)
419 }
420}
421
422// Set correct permissions for mox working directory, binary, config and data and service file.
423//
424// We require being able to stat the basic non-optional paths. Then we'll try to
425// fix up permissions. If an error occurs when fixing permissions, we log and
426// continue (could not be an actual problem).
427func fixperms(log mlog.Log, workdir, configdir, datadir string, moxuid, moxgid uint32) (rerr error) {
428 type fserr struct{ Err error }
429 defer func() {
430 x := recover()
431 if x == nil {
432 return
433 }
434 e, ok := x.(fserr)
435 if ok {
436 rerr = e.Err
437 } else {
438 panic(x)
439 }
440 }()
441
442 checkf := func(err error, format string, args ...any) {
443 if err != nil {
444 panic(fserr{fmt.Errorf(format, args...)})
445 }
446 }
447
448 // Changes we have to make. We collect them first, then apply.
449 type change struct {
450 path string
451 uid, gid *uint32
452 olduid, oldgid uint32
453 mode *fs.FileMode
454 oldmode fs.FileMode
455 }
456 var changes []change
457
458 ensure := func(p string, uid, gid uint32, perm fs.FileMode) bool {
459 fi, err := os.Stat(p)
460 checkf(err, "stat %s", p)
461
462 st, ok := fi.Sys().(*syscall.Stat_t)
463 if !ok {
464 checkf(fmt.Errorf("got %T", st), "stat sys, expected syscall.Stat_t")
465 }
466
467 var ch change
468 if st.Uid != uid || st.Gid != gid {
469 ch.uid = &uid
470 ch.gid = &gid
471 ch.olduid = st.Uid
472 ch.oldgid = st.Gid
473 }
474 if perm != fi.Mode()&(fs.ModeSetgid|0777) {
475 ch.mode = &perm
476 ch.oldmode = fi.Mode() & (fs.ModeSetgid | 0777)
477 }
478 var zerochange change
479 if ch == zerochange {
480 return false
481 }
482 ch.path = p
483 changes = append(changes, ch)
484 return true
485 }
486
487 xexists := func(p string) bool {
488 _, err := os.Stat(p)
489 if err != nil && !os.IsNotExist(err) {
490 checkf(err, "stat %s", p)
491 }
492 return err == nil
493 }
494
495 // We ensure these permissions:
496 //
497 // $workdir root:mox 0751
498 // $configdir mox:root 0750 + setgid, and recursively (but files 0640)
499 // $datadir mox:root 0750 + setgid, and recursively (but files 0640)
500 // $workdir/mox (binary, optional) root:mox 0750
501 // $workdir/mox.service (systemd service file, optional) root:root 0644
502
503 const root = 0
504 ensure(workdir, root, moxgid, 0751)
505 fixconfig := ensure(configdir, moxuid, 0, fs.ModeSetgid|0750)
506 fixdata := ensure(datadir, moxuid, 0, fs.ModeSetgid|0750)
507
508 // Binary and systemd service file do not exist (there) when running under docker.
509 binary := filepath.Join(workdir, "mox")
510 if xexists(binary) {
511 ensure(binary, root, moxgid, 0750)
512 }
513 svc := filepath.Join(workdir, "mox.service")
514 if xexists(svc) {
515 ensure(svc, root, root, 0644)
516 }
517
518 if len(changes) == 0 {
519 return
520 }
521
522 // Apply changes.
523 log.Print("fixing up permissions, will continue on errors")
524 for _, ch := range changes {
525 if ch.uid != nil {
526 err := os.Chown(ch.path, int(*ch.uid), int(*ch.gid))
527 log.Printx("chown, fixing uid/gid", err,
528 slog.String("path", ch.path),
529 slog.Any("olduid", ch.olduid),
530 slog.Any("oldgid", ch.oldgid),
531 slog.Any("newuid", *ch.uid),
532 slog.Any("newgid", *ch.gid))
533 }
534 if ch.mode != nil {
535 err := os.Chmod(ch.path, *ch.mode)
536 log.Printx("chmod, fixing permissions", err,
537 slog.String("path", ch.path),
538 slog.Any("oldmode", fmt.Sprintf("%03o", ch.oldmode)),
539 slog.Any("newmode", fmt.Sprintf("%03o", *ch.mode)))
540 }
541 }
542
543 walkchange := func(dir string) {
544 err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
545 if err != nil {
546 log.Printx("walk error, continuing", err, slog.String("path", path))
547 return nil
548 }
549 fi, err := d.Info()
550 if err != nil {
551 log.Printx("stat during walk, continuing", err, slog.String("path", path))
552 return nil
553 }
554 st, ok := fi.Sys().(*syscall.Stat_t)
555 if !ok {
556 log.Printx("syscall stat during walk, continuing", err, slog.String("path", path))
557 return nil
558 }
559 if st.Uid != moxuid || st.Gid != root {
560 err := os.Chown(path, int(moxuid), root)
561 log.Printx("walk chown, fixing uid/gid", err,
562 slog.String("path", path),
563 slog.Any("olduid", st.Uid),
564 slog.Any("oldgid", st.Gid),
565 slog.Any("newuid", moxuid),
566 slog.Any("newgid", root))
567 }
568 omode := fi.Mode() & (fs.ModeSetgid | 0777)
569 var nmode fs.FileMode
570 if fi.IsDir() {
571 nmode = fs.ModeSetgid | 0750
572 } else {
573 nmode = 0640
574 }
575 if omode != nmode {
576 err := os.Chmod(path, nmode)
577 log.Printx("walk chmod, fixing permissions", err,
578 slog.String("path", path),
579 slog.Any("oldmode", fmt.Sprintf("%03o", omode)),
580 slog.Any("newmode", fmt.Sprintf("%03o", nmode)))
581 }
582 return nil
583 })
584 log.Check(err, "walking dir to fix permissions", slog.String("dir", dir))
585 }
586
587 // If config or data dir needed fixing, also set uid/gid and mode and files/dirs
588 // inside, recursively. We don't always recurse, data probably contains many files.
589 if fixconfig {
590 log.Print("fixing permissions in config dir", slog.String("configdir", configdir))
591 walkchange(configdir)
592 }
593 if fixdata {
594 log.Print("fixing permissions in data dir", slog.String("configdir", configdir))
595 walkchange(datadir)
596 }
597 return nil
598}
599