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 for _, zone := range mox.Conf.MonitorDNSBLs() {
90 if !slices.Contains(zones, zone) {
91 zones = append(zones, zone)
92 }
93 }
94 // And gather IPs.
95 ips, err := mox.IPs(mox.Context, false)
96 if err != nil {
97 log.Errorx("listing ips for dnsbl monitor", err)
98 // Mark checks as broken.
99 for k := range prevResults {
100 metricDNSBL.WithLabelValues(k.zone.Name(), k.ip).Set(-1)
101 }
102 continue
103 }
104 var publicIPs []net.IP
105 var publicIPstrs []string
106 for _, ip := range ips {
107 if ip.IsLoopback() || ip.IsPrivate() {
108 continue
109 }
110 publicIPs = append(publicIPs, ip)
111 publicIPstrs = append(publicIPstrs, ip.String())
112 }
113
114 // Remove labels that no longer exist from metric.
115 for k := range prevResults {
116 if !slices.Contains(zones, k.zone) || !slices.Contains(publicIPstrs, k.ip) {
117 metricDNSBL.DeleteLabelValues(k.zone.Name(), k.ip)
118 delete(prevResults, k)
119 }
120 }
121
122 // Do DNSBL checks and update metric.
123 for _, ip := range publicIPs {
124 for _, zone := range zones {
125 status, expl, err := dnsbl.Lookup(mox.Context, log.Logger, resolver, zone, ip)
126 if err != nil {
127 log.Errorx("dnsbl monitor lookup", err,
128 slog.Any("ip", ip),
129 slog.Any("zone", zone),
130 slog.String("expl", expl),
131 slog.Any("status", status))
132 }
133 var v float64
134 if status == dnsbl.StatusPass {
135 v = 1
136 }
137 metricDNSBL.WithLabelValues(zone.Name(), ip.String()).Set(v)
138 k := key{zone, ip.String()}
139 prevResults[k] = struct{}{}
140
141 time.Sleep(time.Second)
142 }
143 }
144 }
145}
146
147// also see localserve.go, code is similar or even shared.
148func cmdServe(c *cmd) {
149 c.help = `Start mox, serving SMTP/IMAP/HTTPS.
150
151Incoming email is accepted over SMTP. Email can be retrieved by users using
152IMAP. HTTP listeners are started for the admin/account web interfaces, and for
153automated TLS configuration. Missing essential TLS certificates are immediately
154requested, other TLS certificates are requested on demand.
155
156Only implemented on unix systems, not Windows.
157`
158 args := c.Parse()
159 if len(args) != 0 {
160 c.Usage()
161 }
162
163 // Set debug logging until config is fully loaded.
164 mlog.Logfmt = true
165 mox.Conf.Log[""] = mlog.LevelDebug
166 mlog.SetConfig(mox.Conf.Log)
167
168 checkACMEHosts := os.Getuid() != 0
169
170 log := c.log
171
172 if os.Getuid() == 0 {
173 mox.MustLoadConfig(true, checkACMEHosts)
174
175 // No need to potentially start and keep multiple processes. As root, we just need
176 // to start the child process.
177 runtime.GOMAXPROCS(1)
178
179 moxconf, err := filepath.Abs(mox.ConfigStaticPath)
180 log.Check(err, "finding absolute mox.conf path")
181 domainsconf, err := filepath.Abs(mox.ConfigDynamicPath)
182 log.Check(err, "finding absolute domains.conf path")
183
184 log.Print("starting as root, initializing network listeners",
185 slog.String("version", moxvar.Version),
186 slog.Any("pid", os.Getpid()),
187 slog.String("moxconf", moxconf),
188 slog.String("domainsconf", domainsconf))
189 if os.Getenv("MOX_SOCKETS") != "" {
190 log.Fatal("refusing to start as root with $MOX_SOCKETS set")
191 }
192 if os.Getenv("MOX_FILES") != "" {
193 log.Fatal("refusing to start as root with $MOX_FILES set")
194 }
195
196 if !mox.Conf.Static.NoFixPermissions {
197 // Fix permissions now that we have privilege to do so. Useful for update of v0.0.1
198 // that was running directly as mox-user.
199 workdir, err := os.Getwd()
200 if err != nil {
201 log.Printx("get working dir, continuing without potentially fixing up permissions", err)
202 } else {
203 configdir := filepath.Dir(mox.ConfigStaticPath)
204 datadir := mox.DataDirPath(".")
205 err := fixperms(log, workdir, configdir, datadir, mox.Conf.Static.UID, mox.Conf.Static.GID)
206 if err != nil {
207 log.Fatalx("fixing permissions", err)
208 }
209 }
210 }
211 } else {
212 mox.RestorePassedFiles()
213 mox.MustLoadConfig(true, checkACMEHosts)
214 log.Print("starting as unprivileged user",
215 slog.String("user", mox.Conf.Static.User),
216 slog.Any("uid", mox.Conf.Static.UID),
217 slog.Any("gid", mox.Conf.Static.GID),
218 slog.Any("pid", os.Getpid()))
219 }
220
221 syscall.Umask(syscall.Umask(007) | 007)
222
223 // Initialize key and random buffer for creating opaque SMTP
224 // transaction IDs based on "cid"s.
225 recvidpath := mox.DataDirPath("receivedid.key")
226 recvidbuf, err := os.ReadFile(recvidpath)
227 if err != nil || len(recvidbuf) != 16+8 {
228 recvidbuf = make([]byte, 16+8)
229 if _, err := cryptorand.Read(recvidbuf); err != nil {
230 log.Fatalx("reading random recvid data", err)
231 }
232 if err := os.WriteFile(recvidpath, recvidbuf, 0660); err != nil {
233 log.Fatalx("writing recvidpath", err, slog.String("path", recvidpath))
234 }
235 err := os.Chown(recvidpath, int(mox.Conf.Static.UID), 0)
236 log.Check(err, "chown receveidid.key",
237 slog.String("path", recvidpath),
238 slog.Any("uid", mox.Conf.Static.UID),
239 slog.Any("gid", 0))
240 err = os.Chmod(recvidpath, 0640)
241 log.Check(err, "chmod receveidid.key to 0640", slog.String("path", recvidpath))
242 }
243 if err := mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:]); err != nil {
244 log.Fatalx("init receivedid", err)
245 }
246
247 // Start mox. If running as root, this will bind/listen on network sockets, and
248 // fork and exec itself as unprivileged user, then waits for the child to stop and
249 // exit. When running as root, this function never returns. But the new
250 // unprivileged user will get here again, with network sockets prepared.
251 //
252 // We listen to the unix domain ctl socket afterwards, which we always remove
253 // before listening. We need to do that because we may not have cleaned up our
254 // control socket during unexpected shutdown. We don't want to remove and listen on
255 // the unix domain socket first. If we would, we would make the existing instance
256 // unreachable over its ctl socket, and then fail because the network addresses are
257 // taken.
258 const mtastsdbRefresher = true
259 const skipForkExec = false
260 if err := start(mtastsdbRefresher, !mox.Conf.Static.NoOutgoingDMARCReports, !mox.Conf.Static.NoOutgoingTLSReports, skipForkExec); err != nil {
261 log.Fatalx("start", err)
262 }
263 log.Print("ready to serve")
264
265 if mox.Conf.Static.CheckUpdates {
266 checkUpdates := func() time.Duration {
267 next := 24 * time.Hour
268 current, lastknown, mtime, err := mox.LastKnown()
269 if err != nil {
270 log.Infox("determining own version before checking for updates, trying again in 24h", err)
271 return next
272 }
273
274 // We don't want to check for updates at every startup. So we sleep based on file
275 // mtime. But file won't exist initially.
276 if !mtime.IsZero() && time.Since(mtime) < 24*time.Hour {
277 d := 24*time.Hour - time.Since(mtime)
278 log.Debug("sleeping for next check for updates", slog.Duration("sleep", d))
279 time.Sleep(d)
280 next = 0
281 }
282 now := time.Now()
283 if err := os.Chtimes(mox.DataDirPath("lastknownversion"), now, now); err != nil {
284 if !os.IsNotExist(err) {
285 log.Infox("setting mtime on lastknownversion file, continuing", err)
286 }
287 }
288
289 log.Debug("checking for updates", slog.Any("lastknown", lastknown))
290 updatesctx, updatescancel := context.WithTimeout(mox.Context, time.Minute)
291 latest, _, changelog, err := updates.Check(updatesctx, log.Logger, dns.StrictResolver{Log: log.Logger}, dns.Domain{ASCII: changelogDomain}, lastknown, changelogURL, changelogPubKey)
292 updatescancel()
293 if err != nil {
294 log.Infox("checking for updates", err, slog.Any("latest", latest))
295 return next
296 }
297 if !latest.After(lastknown) {
298 log.Debug("no new version available")
299 return next
300 }
301 if len(changelog.Changes) == 0 {
302 log.Info("new version available, but changelog is empty, ignoring", slog.Any("latest", latest))
303 return next
304 }
305
306 var cl string
307 for _, c := range changelog.Changes {
308 cl += "----\n\n" + strings.TrimSpace(c.Text) + "\n\n"
309 }
310 cl += "----"
311
312 a, err := store.OpenAccount(log, mox.Conf.Static.Postmaster.Account)
313 if err != nil {
314 log.Infox("open account for postmaster changelog delivery", err)
315 return next
316 }
317 defer func() {
318 err := a.Close()
319 log.Check(err, "closing account")
320 }()
321 f, err := store.CreateMessageTemp(log, "changelog")
322 if err != nil {
323 log.Infox("making temporary message file for changelog delivery", err)
324 return next
325 }
326 defer store.CloseRemoveTempFile(log, f, "message for changelog delivery")
327
328 m := &store.Message{
329 Received: time.Now(),
330 Flags: store.Flags{Flagged: true},
331 }
332 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"))
333 if err != nil {
334 log.Infox("writing temporary message file for changelog delivery", err)
335 return next
336 }
337 m.Size = int64(n)
338 if err := a.DeliverMailbox(log, mox.Conf.Static.Postmaster.Mailbox, m, f); err != nil {
339 log.Errorx("changelog delivery", err)
340 return next
341 }
342 log.Info("delivered changelog",
343 slog.Any("current", current),
344 slog.Any("lastknown", lastknown),
345 slog.Any("latest", latest))
346 if err := mox.StoreLastKnown(latest); err != nil {
347 // This will be awkward, we'll keep notifying the postmaster once every 24h...
348 log.Infox("updating last known version", err)
349 }
350 return next
351 }
352
353 go func() {
354 for {
355 next := checkUpdates()
356 time.Sleep(next)
357 }
358 }()
359 }
360
361 go monitorDNSBL(log)
362
363 ctlpath := mox.DataDirPath("ctl")
364 _ = os.Remove(ctlpath)
365 ctl, err := net.Listen("unix", ctlpath)
366 if err != nil {
367 log.Fatalx("listen on ctl unix domain socket", err)
368 }
369 go func() {
370 for {
371 conn, err := ctl.Accept()
372 if err != nil {
373 log.Printx("accept for ctl", err)
374 continue
375 }
376 cid := mox.Cid()
377 ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
378 go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) })
379 }
380 }()
381
382 // Remove old temporary files that somehow haven't been cleaned up.
383 tmpdir := mox.DataDirPath("tmp")
384 os.MkdirAll(tmpdir, 0770)
385 tmps, err := os.ReadDir(tmpdir)
386 if err != nil {
387 log.Errorx("listing files in tmpdir", err)
388 } else {
389 now := time.Now()
390 for _, e := range tmps {
391 if fi, err := e.Info(); err != nil {
392 log.Errorx("stat tmp file", err, slog.String("filename", e.Name()))
393 } else if now.Sub(fi.ModTime()) > 7*24*time.Hour && !fi.IsDir() {
394 p := filepath.Join(tmpdir, e.Name())
395 if err := os.Remove(p); err != nil {
396 log.Errorx("removing stale temporary file", err, slog.String("path", p))
397 } else {
398 log.Info("removed stale temporary file", slog.String("path", p))
399 }
400 }
401 }
402 }
403
404 // Graceful shutdown.
405 sigc := make(chan os.Signal, 1)
406 signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
407 sig := <-sigc
408 log.Print("shutting down, waiting max 3s for existing connections", slog.Any("signal", sig))
409 shutdown(log)
410 if num, ok := sig.(syscall.Signal); ok {
411 os.Exit(int(num))
412 } else {
413 os.Exit(1)
414 }
415}
416
417// Set correct permissions for mox working directory, binary, config and data and service file.
418//
419// We require being able to stat the basic non-optional paths. Then we'll try to
420// fix up permissions. If an error occurs when fixing permissions, we log and
421// continue (could not be an actual problem).
422func fixperms(log mlog.Log, workdir, configdir, datadir string, moxuid, moxgid uint32) (rerr error) {
423 type fserr struct{ Err error }
424 defer func() {
425 x := recover()
426 if x == nil {
427 return
428 }
429 e, ok := x.(fserr)
430 if ok {
431 rerr = e.Err
432 } else {
433 panic(x)
434 }
435 }()
436
437 checkf := func(err error, format string, args ...any) {
438 if err != nil {
439 panic(fserr{fmt.Errorf(format, args...)})
440 }
441 }
442
443 // Changes we have to make. We collect them first, then apply.
444 type change struct {
445 path string
446 uid, gid *uint32
447 olduid, oldgid uint32
448 mode *fs.FileMode
449 oldmode fs.FileMode
450 }
451 var changes []change
452
453 ensure := func(p string, uid, gid uint32, perm fs.FileMode) bool {
454 fi, err := os.Stat(p)
455 checkf(err, "stat %s", p)
456
457 st, ok := fi.Sys().(*syscall.Stat_t)
458 if !ok {
459 checkf(fmt.Errorf("got %T", st), "stat sys, expected syscall.Stat_t")
460 }
461
462 var ch change
463 if st.Uid != uid || st.Gid != gid {
464 ch.uid = &uid
465 ch.gid = &gid
466 ch.olduid = st.Uid
467 ch.oldgid = st.Gid
468 }
469 if perm != fi.Mode()&(fs.ModeSetgid|0777) {
470 ch.mode = &perm
471 ch.oldmode = fi.Mode() & (fs.ModeSetgid | 0777)
472 }
473 var zerochange change
474 if ch == zerochange {
475 return false
476 }
477 ch.path = p
478 changes = append(changes, ch)
479 return true
480 }
481
482 xexists := func(p string) bool {
483 _, err := os.Stat(p)
484 if err != nil && !os.IsNotExist(err) {
485 checkf(err, "stat %s", p)
486 }
487 return err == nil
488 }
489
490 // We ensure these permissions:
491 //
492 // $workdir root:mox 0751
493 // $configdir mox:root 0750 + setgid, and recursively (but files 0640)
494 // $datadir mox:root 0750 + setgid, and recursively (but files 0640)
495 // $workdir/mox (binary, optional) root:mox 0750
496 // $workdir/mox.service (systemd service file, optional) root:root 0644
497
498 const root = 0
499 ensure(workdir, root, moxgid, 0751)
500 fixconfig := ensure(configdir, moxuid, 0, fs.ModeSetgid|0750)
501 fixdata := ensure(datadir, moxuid, 0, fs.ModeSetgid|0750)
502
503 // Binary and systemd service file do not exist (there) when running under docker.
504 binary := filepath.Join(workdir, "mox")
505 if xexists(binary) {
506 ensure(binary, root, moxgid, 0750)
507 }
508 svc := filepath.Join(workdir, "mox.service")
509 if xexists(svc) {
510 ensure(svc, root, root, 0644)
511 }
512
513 if len(changes) == 0 {
514 return
515 }
516
517 // Apply changes.
518 log.Print("fixing up permissions, will continue on errors")
519 for _, ch := range changes {
520 if ch.uid != nil {
521 err := os.Chown(ch.path, int(*ch.uid), int(*ch.gid))
522 log.Printx("chown, fixing uid/gid", err,
523 slog.String("path", ch.path),
524 slog.Any("olduid", ch.olduid),
525 slog.Any("oldgid", ch.oldgid),
526 slog.Any("newuid", *ch.uid),
527 slog.Any("newgid", *ch.gid))
528 }
529 if ch.mode != nil {
530 err := os.Chmod(ch.path, *ch.mode)
531 log.Printx("chmod, fixing permissions", err,
532 slog.String("path", ch.path),
533 slog.Any("oldmode", fmt.Sprintf("%03o", ch.oldmode)),
534 slog.Any("newmode", fmt.Sprintf("%03o", *ch.mode)))
535 }
536 }
537
538 walkchange := func(dir string) {
539 err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
540 if err != nil {
541 log.Printx("walk error, continuing", err, slog.String("path", path))
542 return nil
543 }
544 fi, err := d.Info()
545 if err != nil {
546 log.Printx("stat during walk, continuing", err, slog.String("path", path))
547 return nil
548 }
549 st, ok := fi.Sys().(*syscall.Stat_t)
550 if !ok {
551 log.Printx("syscall stat during walk, continuing", err, slog.String("path", path))
552 return nil
553 }
554 if st.Uid != moxuid || st.Gid != root {
555 err := os.Chown(path, int(moxuid), root)
556 log.Printx("walk chown, fixing uid/gid", err,
557 slog.String("path", path),
558 slog.Any("olduid", st.Uid),
559 slog.Any("oldgid", st.Gid),
560 slog.Any("newuid", moxuid),
561 slog.Any("newgid", root))
562 }
563 omode := fi.Mode() & (fs.ModeSetgid | 0777)
564 var nmode fs.FileMode
565 if fi.IsDir() {
566 nmode = fs.ModeSetgid | 0750
567 } else {
568 nmode = 0640
569 }
570 if omode != nmode {
571 err := os.Chmod(path, nmode)
572 log.Printx("walk chmod, fixing permissions", err,
573 slog.String("path", path),
574 slog.Any("oldmode", fmt.Sprintf("%03o", omode)),
575 slog.Any("newmode", fmt.Sprintf("%03o", nmode)))
576 }
577 return nil
578 })
579 log.Check(err, "walking dir to fix permissions", slog.String("dir", dir))
580 }
581
582 // If config or data dir needed fixing, also set uid/gid and mode and files/dirs
583 // inside, recursively. We don't always recurse, data probably contains many files.
584 if fixconfig {
585 log.Print("fixing permissions in config dir", slog.String("configdir", configdir))
586 walkchange(configdir)
587 }
588 if fixdata {
589 log.Print("fixing permissions in data dir", slog.String("configdir", configdir))
590 walkchange(datadir)
591 }
592 return nil
593}
594