mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 08:23:48 +03:00
make mox compile on windows, without "mox serve" but with working "mox localserve"
getting mox to compile required changing code in only a few places where package "syscall" was used: for accessing file access times and for umask handling. an open problem is how to start a process as an unprivileged user on windows. that's why "mox serve" isn't implemented yet. and just finding a way to implement it now may not be good enough in the near future: we may want to starting using a more complete privilege separation approach, with a process handling sensitive tasks (handling private keys, authentication), where we may want to pass file descriptors between processes. how would that work on windows? anyway, getting mox to compile for windows doesn't mean it works properly on windows. the largest issue: mox would normally open a file, rename or remove it, and finally close it. this happens during message delivery. that doesn't work on windows, the rename/remove would fail because the file is still open. so this commit swaps many "remove" and "close" calls. renames are a longer story: message delivery had two ways to deliver: with "consuming" the (temporary) message file (which would rename it to its final destination), and without consuming (by hardlinking the file, falling back to copying). the last delivery to a recipient of a message (and the only one in the common case of a single recipient) would consume the message, and the earlier recipients would not. during delivery, the already open message file was used, to parse the message. we still want to use that open message file, and the caller now stays responsible for closing it, but we no longer try to rename (consume) the file. we always hardlink (or copy) during delivery (this works on windows), and the caller is responsible for closing and removing (in that order) the original temporary file. this does cost one syscall more. but it makes the delivery code (responsibilities) a bit simpler. there is one more obvious issue: the file system path separator. mox already used the "filepath" package to join paths in many places, but not everywhere. and it still used strings with slashes for local file access. with this commit, the code now uses filepath.FromSlash for path strings with slashes, uses "filepath" in a few more places where it previously didn't. also switches from "filepath" to regular "path" package when handling mailbox names in a few places, because those always use forward slashes, regardless of local file system conventions. windows can handle forward slashes when opening files, so test code that passes path strings with forward slashes straight to go stdlib file i/o functions are left unchanged to reduce code churn. the regular non-test code, or test code that uses path strings in places other than standard i/o functions, does have the paths converted for consistent paths (otherwise we would end up with paths with mixed forward/backward slashes in log messages). windows cannot dup a listening socket. for "mox localserve", it isn't important, and we can work around the issue. the current approach for "mox serve" (forking a process and passing file descriptors of listening sockets on "privileged" ports) won't work on windows. perhaps it isn't needed on windows, and any user can listen on "privileged" ports? that would be welcome. on windows, os.Open cannot open a directory, so we cannot call Sync on it after message delivery. a cursory internet search indicates that directories cannot be synced on windows. the story is probably much more nuanced than that, with long deep technical details/discussions/disagreement/confusion, like on unix. for "mox localserve" we can get away with making syncdir a no-op.
This commit is contained in:
parent
96774de8d6
commit
28fae96a9b
78 changed files with 1155 additions and 938 deletions
16
Makefile
16
Makefile
|
@ -107,3 +107,19 @@ docker:
|
||||||
|
|
||||||
docker-release:
|
docker-release:
|
||||||
./docker-release.sh
|
./docker-release.sh
|
||||||
|
|
||||||
|
buildall:
|
||||||
|
GOOS=linux GOARCH=arm go build
|
||||||
|
GOOS=linux GOARCH=arm64 go build
|
||||||
|
GOOS=linux GOARCH=amd64 go build
|
||||||
|
GOOS=linux GOARCH=386 go build
|
||||||
|
GOOS=openbsd GOARCH=amd64 go build
|
||||||
|
GOOS=freebsd GOARCH=amd64 go build
|
||||||
|
GOOS=netbsd GOARCH=amd64 go build
|
||||||
|
GOOS=darwin GOARCH=amd64 go build
|
||||||
|
GOOS=dragonfly GOARCH=amd64 go build
|
||||||
|
GOOS=illumos GOARCH=amd64 go build
|
||||||
|
GOOS=solaris GOARCH=amd64 go build
|
||||||
|
GOOS=aix GOARCH=ppc64 go build
|
||||||
|
GOOS=windows GOARCH=amd64 go build
|
||||||
|
# no plan9 for now
|
||||||
|
|
|
@ -91,7 +91,10 @@ Verify you have a working mox binary:
|
||||||
|
|
||||||
./mox version
|
./mox version
|
||||||
|
|
||||||
Mox only compiles for/works on unix systems, not on Plan 9 or Windows.
|
Mox only compiles for and fully works on unix systems. Mox also compiles for
|
||||||
|
Windows, but "mox serve" does not yet work, though "mox localserve" (for a
|
||||||
|
local test instance) and most other subcommands do. Mox does not compile for
|
||||||
|
Plan 9.
|
||||||
|
|
||||||
You can also run mox with docker image `r.xmox.nl/mox`, with tags like `v0.0.1`
|
You can also run mox with docker image `r.xmox.nl/mox`, with tags like `v0.0.1`
|
||||||
and `v0.0.1-go1.20.1-alpine3.17.2`, see https://r.xmox.nl/r/mox/. Though new
|
and `v0.0.1-go1.20.1-alpine3.17.2`, see https://r.xmox.nl/r/mox/. Though new
|
||||||
|
|
|
@ -83,7 +83,7 @@ func Load(name, acmeDir, contactEmail, directoryURL string, getPrivateKey func(h
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load identity key if it exists. Otherwise, create a new key.
|
// Load identity key if it exists. Otherwise, create a new key.
|
||||||
p := filepath.Join(acmeDir + "/" + name + ".key")
|
p := filepath.Join(acmeDir, name+".key")
|
||||||
var key crypto.Signer
|
var key crypto.Signer
|
||||||
f, err := os.Open(p)
|
f, err := os.Open(p)
|
||||||
if f != nil {
|
if f != nil {
|
||||||
|
@ -135,7 +135,7 @@ func Load(name, acmeDir, contactEmail, directoryURL string, getPrivateKey func(h
|
||||||
}
|
}
|
||||||
|
|
||||||
m := &autocert.Manager{
|
m := &autocert.Manager{
|
||||||
Cache: dirCache(acmeDir + "/keycerts/" + name),
|
Cache: dirCache(filepath.Join(acmeDir, "keycerts", name)),
|
||||||
Prompt: autocert.AcceptTOS,
|
Prompt: autocert.AcceptTOS,
|
||||||
Email: contactEmail,
|
Email: contactEmail,
|
||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
|
|
|
@ -372,7 +372,7 @@ func backupctl(ctx context.Context, ctl *ctl) {
|
||||||
|
|
||||||
xvlog("queue backed finished", mlog.Field("duration", time.Since(tmQueue)))
|
xvlog("queue backed finished", mlog.Field("duration", time.Since(tmQueue)))
|
||||||
}
|
}
|
||||||
backupQueue("queue/index.db")
|
backupQueue(filepath.FromSlash("queue/index.db"))
|
||||||
|
|
||||||
backupAccount := func(acc *store.Account) {
|
backupAccount := func(acc *store.Account) {
|
||||||
defer acc.Close()
|
defer acc.Close()
|
||||||
|
@ -469,7 +469,7 @@ func backupctl(ctx context.Context, ctl *ctl) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ap := filepath.Join("accounts", acc.Name, p)
|
ap := filepath.Join("accounts", acc.Name, p)
|
||||||
if strings.HasPrefix(p, "msg/") {
|
if strings.HasPrefix(p, "msg"+string(filepath.Separator)) {
|
||||||
xwarnx("backing up unrecognized file in account message directory (should be moved away)", nil, mlog.Field("path", ap))
|
xwarnx("backing up unrecognized file in account message directory (should be moved away)", nil, mlog.Field("path", ap))
|
||||||
} else {
|
} else {
|
||||||
xwarnx("backing up unrecognized file in account directory", nil, mlog.Field("path", ap))
|
xwarnx("backing up unrecognized file in account directory", nil, mlog.Field("path", ap))
|
||||||
|
|
16
ctl.go
16
ctl.go
|
@ -320,12 +320,11 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
||||||
msgFile, err := store.CreateMessageTemp("ctl-deliver")
|
msgFile, err := store.CreateMessageTemp("ctl-deliver")
|
||||||
ctl.xcheck(err, "creating temporary message file")
|
ctl.xcheck(err, "creating temporary message file")
|
||||||
defer func() {
|
defer func() {
|
||||||
if msgFile != nil {
|
name := msgFile.Name()
|
||||||
err := os.Remove(msgFile.Name())
|
err := msgFile.Close()
|
||||||
log.Check(err, "removing temporary message file", mlog.Field("path", msgFile.Name()))
|
log.Check(err, "closing temporary message file")
|
||||||
err = msgFile.Close()
|
err = os.Remove(name)
|
||||||
log.Check(err, "closing temporary message file")
|
log.Check(err, "removing temporary message file", mlog.Field("path", name))
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
mw := message.NewWriter(msgFile)
|
mw := message.NewWriter(msgFile)
|
||||||
ctl.xwriteok()
|
ctl.xwriteok()
|
||||||
|
@ -340,14 +339,11 @@ func servectlcmd(ctx context.Context, ctl *ctl, shutdown func()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.WithWLock(func() {
|
a.WithWLock(func() {
|
||||||
err := a.DeliverDestination(log, addr, m, msgFile, true)
|
err := a.DeliverDestination(log, addr, m, msgFile)
|
||||||
ctl.xcheck(err, "delivering message")
|
ctl.xcheck(err, "delivering message")
|
||||||
log.Info("message delivered through ctl", mlog.Field("to", to))
|
log.Info("message delivered through ctl", mlog.Field("to", to))
|
||||||
})
|
})
|
||||||
|
|
||||||
err = msgFile.Close()
|
|
||||||
log.Check(err, "closing delivered message file")
|
|
||||||
msgFile = nil
|
|
||||||
err = a.Close()
|
err = a.Close()
|
||||||
ctl.xcheck(err, "closing account")
|
ctl.xcheck(err, "closing account")
|
||||||
ctl.xwriteok()
|
ctl.xwriteok()
|
||||||
|
|
23
ctl_test.go
23
ctl_test.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mjl-/mox/dmarcdb"
|
"github.com/mjl-/mox/dmarcdb"
|
||||||
|
@ -33,8 +34,8 @@ func tcheck(t *testing.T, err error, errmsg string) {
|
||||||
// unhandled errors would cause a panic.
|
// unhandled errors would cause a panic.
|
||||||
func TestCtl(t *testing.T) {
|
func TestCtl(t *testing.T) {
|
||||||
os.RemoveAll("testdata/ctl/data")
|
os.RemoveAll("testdata/ctl/data")
|
||||||
mox.ConfigStaticPath = "testdata/ctl/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("testdata/ctl/mox.conf")
|
||||||
mox.ConfigDynamicPath = "testdata/ctl/domains.conf"
|
mox.ConfigDynamicPath = filepath.FromSlash("testdata/ctl/domains.conf")
|
||||||
if errs := mox.LoadConfig(ctxbg, true, false); len(errs) > 0 {
|
if errs := mox.LoadConfig(ctxbg, true, false); len(errs) > 0 {
|
||||||
t.Fatalf("loading mox config: %v", errs)
|
t.Fatalf("loading mox config: %v", errs)
|
||||||
}
|
}
|
||||||
|
@ -147,13 +148,13 @@ func TestCtl(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Export data, import it again
|
// Export data, import it again
|
||||||
xcmdExport(true, []string{"testdata/ctl/data/tmp/export/mbox/", "testdata/ctl/data/accounts/mjl"}, nil)
|
xcmdExport(true, []string{filepath.FromSlash("testdata/ctl/data/tmp/export/mbox/"), filepath.FromSlash("testdata/ctl/data/accounts/mjl")}, nil)
|
||||||
xcmdExport(false, []string{"testdata/ctl/data/tmp/export/maildir/", "testdata/ctl/data/accounts/mjl"}, nil)
|
xcmdExport(false, []string{filepath.FromSlash("testdata/ctl/data/tmp/export/maildir/"), filepath.FromSlash("testdata/ctl/data/accounts/mjl")}, nil)
|
||||||
testctl(func(ctl *ctl) {
|
testctl(func(ctl *ctl) {
|
||||||
ctlcmdImport(ctl, true, "mjl", "inbox", "testdata/ctl/data/tmp/export/mbox/Inbox.mbox")
|
ctlcmdImport(ctl, true, "mjl", "inbox", filepath.FromSlash("testdata/ctl/data/tmp/export/mbox/Inbox.mbox"))
|
||||||
})
|
})
|
||||||
testctl(func(ctl *ctl) {
|
testctl(func(ctl *ctl) {
|
||||||
ctlcmdImport(ctl, false, "mjl", "inbox", "testdata/ctl/data/tmp/export/maildir/Inbox")
|
ctlcmdImport(ctl, false, "mjl", "inbox", filepath.FromSlash("testdata/ctl/data/tmp/export/maildir/Inbox"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// "recalculatemailboxcounts"
|
// "recalculatemailboxcounts"
|
||||||
|
@ -177,12 +178,12 @@ func TestCtl(t *testing.T) {
|
||||||
m.Size = int64(len(content))
|
m.Size = int64(len(content))
|
||||||
msgf, err := store.CreateMessageTemp("ctltest")
|
msgf, err := store.CreateMessageTemp("ctltest")
|
||||||
tcheck(t, err, "create temp file")
|
tcheck(t, err, "create temp file")
|
||||||
|
defer os.Remove(msgf.Name())
|
||||||
|
defer msgf.Close()
|
||||||
_, err = msgf.Write(content)
|
_, err = msgf.Write(content)
|
||||||
tcheck(t, err, "write message file")
|
tcheck(t, err, "write message file")
|
||||||
err = acc.DeliverMailbox(xlog, "Inbox", m, msgf, true)
|
err = acc.DeliverMailbox(xlog, "Inbox", m, msgf)
|
||||||
tcheck(t, err, "deliver message")
|
tcheck(t, err, "deliver message")
|
||||||
err = msgf.Close()
|
|
||||||
tcheck(t, err, "close message file")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var msgBadSize store.Message
|
var msgBadSize store.Message
|
||||||
|
@ -236,13 +237,13 @@ func TestCtl(t *testing.T) {
|
||||||
os.RemoveAll("testdata/ctl/data/tmp/backup-data")
|
os.RemoveAll("testdata/ctl/data/tmp/backup-data")
|
||||||
err := os.WriteFile("testdata/ctl/data/receivedid.key", make([]byte, 16), 0600)
|
err := os.WriteFile("testdata/ctl/data/receivedid.key", make([]byte, 16), 0600)
|
||||||
tcheck(t, err, "writing receivedid.key")
|
tcheck(t, err, "writing receivedid.key")
|
||||||
ctlcmdBackup(ctl, "testdata/ctl/data/tmp/backup-data", false)
|
ctlcmdBackup(ctl, filepath.FromSlash("testdata/ctl/data/tmp/backup-data"), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Verify the backup.
|
// Verify the backup.
|
||||||
xcmd := cmd{
|
xcmd := cmd{
|
||||||
flag: flag.NewFlagSet("", flag.ExitOnError),
|
flag: flag.NewFlagSet("", flag.ExitOnError),
|
||||||
flagArgs: []string{"testdata/ctl/data/tmp/backup-data"},
|
flagArgs: []string{filepath.FromSlash("testdata/ctl/data/tmp/backup-data")},
|
||||||
}
|
}
|
||||||
cmdVerifydata(&xcmd)
|
cmdVerifydata(&xcmd)
|
||||||
}
|
}
|
||||||
|
|
10
develop.txt
10
develop.txt
|
@ -1,5 +1,15 @@
|
||||||
This file has notes useful for mox developers.
|
This file has notes useful for mox developers.
|
||||||
|
|
||||||
|
# Code style & guidelines
|
||||||
|
|
||||||
|
- Keep the same style as existing code.
|
||||||
|
- For Windows: use package "path/filepath" when dealing with files/directories.
|
||||||
|
Test code can pass forward-slashed paths directly to standard library functions,
|
||||||
|
but use proper filepath functions when parameters are passed and in non-test
|
||||||
|
code. Mailbox names always use forward slash, so use package "path" for mailbox
|
||||||
|
name/path manipulation. Do not remove/rename files that are still open.
|
||||||
|
|
||||||
|
|
||||||
# TLS certificates
|
# TLS certificates
|
||||||
|
|
||||||
https://github.com/cloudflare/cfssl is useful for testing with TLS
|
https://github.com/cloudflare/cfssl is useful for testing with TLS
|
||||||
|
|
|
@ -17,16 +17,17 @@ var ctxbg = context.Background()
|
||||||
|
|
||||||
func TestDMARCDB(t *testing.T) {
|
func TestDMARCDB(t *testing.T) {
|
||||||
mox.Shutdown = ctxbg
|
mox.Shutdown = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/dmarcdb/fake.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dmarcdb/fake.conf")
|
||||||
mox.Conf.Static.DataDir = "."
|
mox.Conf.Static.DataDir = "."
|
||||||
|
|
||||||
dbpath := mox.DataDirPath("dmarcrpt.db")
|
dbpath := mox.DataDirPath("dmarcrpt.db")
|
||||||
os.MkdirAll(filepath.Dir(dbpath), 0770)
|
os.MkdirAll(filepath.Dir(dbpath), 0770)
|
||||||
defer os.Remove(dbpath)
|
|
||||||
|
|
||||||
if err := Init(); err != nil {
|
if err := Init(); err != nil {
|
||||||
t.Fatalf("init database: %s", err)
|
t.Fatalf("init database: %s", err)
|
||||||
}
|
}
|
||||||
|
defer os.Remove(dbpath)
|
||||||
|
defer DB.Close()
|
||||||
|
|
||||||
feedback := &dmarcrpt.Feedback{
|
feedback := &dmarcrpt.Feedback{
|
||||||
ReportMetadata: dmarcrpt.ReportMetadata{
|
ReportMetadata: dmarcrpt.ReportMetadata{
|
||||||
|
|
|
@ -2,6 +2,7 @@ package dmarcrpt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -122,14 +123,14 @@ func TestParseReport(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMessageReport(t *testing.T) {
|
func TestParseMessageReport(t *testing.T) {
|
||||||
const dir = "../testdata/dmarc-reports"
|
dir := filepath.FromSlash("../testdata/dmarc-reports")
|
||||||
files, err := os.ReadDir(dir)
|
files, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("listing dmarc report emails: %s", err)
|
t.Fatalf("listing dmarc report emails: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
p := dir + "/" + file.Name()
|
p := filepath.Join(dir, file.Name())
|
||||||
f, err := os.Open(p)
|
f, err := os.Open(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open %q: %s", p, err)
|
t.Fatalf("open %q: %s", p, err)
|
||||||
|
|
2
doc.go
2
doc.go
|
@ -89,6 +89,8 @@ IMAP. HTTP listeners are started for the admin/account web interfaces, and for
|
||||||
automated TLS configuration. Missing essential TLS certificates are immediately
|
automated TLS configuration. Missing essential TLS certificates are immediately
|
||||||
requested, other TLS certificates are requested on demand.
|
requested, other TLS certificates are requested on demand.
|
||||||
|
|
||||||
|
Only implemented on unix systems, not Windows.
|
||||||
|
|
||||||
usage: mox serve
|
usage: mox serve
|
||||||
|
|
||||||
# mox quickstart
|
# mox quickstart
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -131,7 +132,7 @@ func TestDSN(t *testing.T) {
|
||||||
|
|
||||||
// Test for valid DKIM signature.
|
// Test for valid DKIM signature.
|
||||||
mox.Context = context.Background()
|
mox.Context = context.Background()
|
||||||
mox.ConfigStaticPath = "../testdata/dsn/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/dsn/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
msgbuf, err = m.Compose(log, false)
|
msgbuf, err = m.Compose(log, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -85,8 +85,8 @@ Accounts:
|
||||||
IgnoreWords: 0.1
|
IgnoreWords: 0.1
|
||||||
`
|
`
|
||||||
|
|
||||||
mox.ConfigStaticPath = "/tmp/mox-bogus/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("/tmp/mox-bogus/mox.conf")
|
||||||
mox.ConfigDynamicPath = "/tmp/mox-bogus/domains.conf"
|
mox.ConfigDynamicPath = filepath.FromSlash("/tmp/mox-bogus/domains.conf")
|
||||||
mox.Conf.DynamicLastCheck = time.Now() // Should prevent warning.
|
mox.Conf.DynamicLastCheck = time.Now() // Should prevent warning.
|
||||||
mox.Conf.Static = config.Static{
|
mox.Conf.Static = config.Static{
|
||||||
DataDir: destDataDir,
|
DataDir: destDataDir,
|
||||||
|
@ -228,11 +228,12 @@ Accounts:
|
||||||
prefix := []byte{}
|
prefix := []byte{}
|
||||||
mf := tempfile()
|
mf := tempfile()
|
||||||
xcheckf(err, "temp file for queue message")
|
xcheckf(err, "temp file for queue message")
|
||||||
|
defer os.Remove(mf.Name())
|
||||||
defer mf.Close()
|
defer mf.Close()
|
||||||
const qmsg = "From: <test0@mox.example>\r\nTo: <other@remote.example>\r\nSubject: test\r\n\r\nthe message...\r\n"
|
const qmsg = "From: <test0@mox.example>\r\nTo: <other@remote.example>\r\nSubject: test\r\n\r\nthe message...\r\n"
|
||||||
_, err = fmt.Fprint(mf, qmsg)
|
_, err = fmt.Fprint(mf, qmsg)
|
||||||
xcheckf(err, "writing message")
|
xcheckf(err, "writing message")
|
||||||
_, err = queue.Add(ctxbg, log, "test0", mailfrom, rcptto, false, false, int64(len(qmsg)), "<test@localhost>", prefix, mf, nil, true)
|
_, err = queue.Add(ctxbg, log, "test0", mailfrom, rcptto, false, false, int64(len(qmsg)), "<test@localhost>", prefix, mf, nil)
|
||||||
xcheckf(err, "enqueue message")
|
xcheckf(err, "enqueue message")
|
||||||
|
|
||||||
// Create three accounts.
|
// Create three accounts.
|
||||||
|
@ -283,10 +284,14 @@ Accounts:
|
||||||
xcheckf(err, "creating temp file for delivery")
|
xcheckf(err, "creating temp file for delivery")
|
||||||
_, err = fmt.Fprint(mf, msg)
|
_, err = fmt.Fprint(mf, msg)
|
||||||
xcheckf(err, "writing deliver message to file")
|
xcheckf(err, "writing deliver message to file")
|
||||||
err = accTest1.DeliverMessage(log, tx, &m, mf, true, false, true, false)
|
err = accTest1.DeliverMessage(log, tx, &m, mf, false, true, false)
|
||||||
|
|
||||||
|
mfname := mf.Name()
|
||||||
xcheckf(err, "add message to account test1")
|
xcheckf(err, "add message to account test1")
|
||||||
err = mf.Close()
|
err = mf.Close()
|
||||||
xcheckf(err, "closing file")
|
xcheckf(err, "closing file")
|
||||||
|
err = os.Remove(mfname)
|
||||||
|
xcheckf(err, "removing temp message file")
|
||||||
|
|
||||||
err = tx.Get(&inbox)
|
err = tx.Get(&inbox)
|
||||||
xcheckf(err, "get inbox")
|
xcheckf(err, "get inbox")
|
||||||
|
@ -339,10 +344,14 @@ Accounts:
|
||||||
xcheckf(err, "creating temp file for delivery")
|
xcheckf(err, "creating temp file for delivery")
|
||||||
_, err = fmt.Fprint(mf0, msg0)
|
_, err = fmt.Fprint(mf0, msg0)
|
||||||
xcheckf(err, "writing deliver message to file")
|
xcheckf(err, "writing deliver message to file")
|
||||||
err = accTest2.DeliverMessage(log, tx, &m0, mf0, true, false, false, false)
|
err = accTest2.DeliverMessage(log, tx, &m0, mf0, false, false, false)
|
||||||
xcheckf(err, "add message to account test2")
|
xcheckf(err, "add message to account test2")
|
||||||
|
|
||||||
|
mf0name := mf0.Name()
|
||||||
err = mf0.Close()
|
err = mf0.Close()
|
||||||
xcheckf(err, "closing file")
|
xcheckf(err, "closing file")
|
||||||
|
err = os.Remove(mf0name)
|
||||||
|
xcheckf(err, "removing temp message file")
|
||||||
|
|
||||||
err = tx.Get(&inbox)
|
err = tx.Get(&inbox)
|
||||||
xcheckf(err, "get inbox")
|
xcheckf(err, "get inbox")
|
||||||
|
@ -366,10 +375,14 @@ Accounts:
|
||||||
xcheckf(err, "creating temp file for delivery")
|
xcheckf(err, "creating temp file for delivery")
|
||||||
_, err = fmt.Fprint(mf1, msg1)
|
_, err = fmt.Fprint(mf1, msg1)
|
||||||
xcheckf(err, "writing deliver message to file")
|
xcheckf(err, "writing deliver message to file")
|
||||||
err = accTest2.DeliverMessage(log, tx, &m1, mf1, true, false, false, false)
|
err = accTest2.DeliverMessage(log, tx, &m1, mf1, false, false, false)
|
||||||
xcheckf(err, "add message to account test2")
|
xcheckf(err, "add message to account test2")
|
||||||
|
|
||||||
|
mf1name := mf1.Name()
|
||||||
err = mf1.Close()
|
err = mf1.Close()
|
||||||
xcheckf(err, "closing file")
|
xcheckf(err, "closing file")
|
||||||
|
err = os.Remove(mf1name)
|
||||||
|
xcheckf(err, "removing temp message file")
|
||||||
|
|
||||||
err = tx.Get(&sent)
|
err = tx.Get(&sent)
|
||||||
xcheckf(err, "get sent")
|
xcheckf(err, "get sent")
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module github.com/mjl-/mox
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c
|
github.com/mjl-/adns v0.0.0-20231013194548-ea0378d616ab
|
||||||
github.com/mjl-/autocert v0.0.0-20231013072455-c361ae2e20a6
|
github.com/mjl-/autocert v0.0.0-20231013072455-c361ae2e20a6
|
||||||
github.com/mjl-/bstore v0.0.2
|
github.com/mjl-/bstore v0.0.2
|
||||||
github.com/mjl-/sconf v0.0.5
|
github.com/mjl-/sconf v0.0.5
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -145,10 +145,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c h1:ZOr9KnCxfAwJWSeZn8Qs6cSF7TrmBa8hVIpLcEvx/Ec=
|
github.com/mjl-/adns v0.0.0-20231013194548-ea0378d616ab h1:fL+dZP+IxX08+ugLq42bkvOfV42muXET+T+Ei1K16bI=
|
||||||
github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c/go.mod h1:JWhGACVviyVUEra9Zv1M8JMkDVXArVt+AIXjTXtuwb4=
|
github.com/mjl-/adns v0.0.0-20231013194548-ea0378d616ab/go.mod h1:v47qUMJnipnmDTRGaHwpCwzE6oypa5K33mUvBfzZBn8=
|
||||||
github.com/mjl-/autocert v0.0.0-20231009155929-d0d48f2f0290 h1:0hCRSu8+XCZ2cSRW+ZtP/7L5wMYjOKFSQthoyj+4cN8=
|
|
||||||
github.com/mjl-/autocert v0.0.0-20231009155929-d0d48f2f0290/go.mod h1:taMFU86abMxKLPV4Bynhv8enbYmS67b8LG80qZv2Qus=
|
|
||||||
github.com/mjl-/autocert v0.0.0-20231013072455-c361ae2e20a6 h1:TEXyTghAN9pmV2ffzdnhmzkML08e1Z/oGywJ9eunbRI=
|
github.com/mjl-/autocert v0.0.0-20231013072455-c361ae2e20a6 h1:TEXyTghAN9pmV2ffzdnhmzkML08e1Z/oGywJ9eunbRI=
|
||||||
github.com/mjl-/autocert v0.0.0-20231013072455-c361ae2e20a6/go.mod h1:taMFU86abMxKLPV4Bynhv8enbYmS67b8LG80qZv2Qus=
|
github.com/mjl-/autocert v0.0.0-20231013072455-c361ae2e20a6/go.mod h1:taMFU86abMxKLPV4Bynhv8enbYmS67b8LG80qZv2Qus=
|
||||||
github.com/mjl-/bstore v0.0.2 h1:4fdpIOY/+Dv1dBHyzdqa4PD90p8Mz86FeyRpI4qcehw=
|
github.com/mjl-/bstore v0.0.2 h1:4fdpIOY/+Dv1dBHyzdqa4PD90p8Mz86FeyRpI4qcehw=
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
//go:build !netbsd && !freebsd && !darwin
|
//go:build !netbsd && !freebsd && !darwin && !windows
|
||||||
|
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func statAtime(sys *syscall.Stat_t) int64 {
|
func statAtime(sys any) (int64, error) {
|
||||||
return int64(sys.Atim.Sec)*1000*1000*1000 + int64(sys.Atim.Nsec)
|
x, ok := sys.(*syscall.Stat_t)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("sys is a %T, expected *syscall.Stat_t", sys)
|
||||||
|
}
|
||||||
|
return int64(x.Atim.Sec)*1000*1000*1000 + int64(x.Atim.Nsec), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,15 @@
|
||||||
|
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import "syscall"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
func statAtime(sys *syscall.Stat_t) int64 {
|
func statAtime(sys any) (int64, error) {
|
||||||
return int64(sys.Atimespec.Sec)*1000*1000*1000 + int64(sys.Atimespec.Nsec)
|
x, ok := sys.(*syscall.Stat_t)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("stat sys is a %T, expected *syscall.Stat_t", sys)
|
||||||
|
}
|
||||||
|
return int64(x.Atimespec.Sec)*1000*1000*1000 + int64(x.Atimespec.Nsec), nil
|
||||||
}
|
}
|
||||||
|
|
16
http/atime_windows.go
Normal file
16
http/atime_windows.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func statAtime(sys any) (int64, error) {
|
||||||
|
x, ok := sys.(*syscall.Win32FileAttributeData)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("sys is a %T, expected *syscall.Win32FileAttributeData", sys)
|
||||||
|
}
|
||||||
|
return x.LastAccessTime.Nanoseconds(), nil
|
||||||
|
}
|
|
@ -14,7 +14,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
|
@ -109,11 +108,7 @@ func loadStaticGzipCache(dir string, maxSize int64) {
|
||||||
}
|
}
|
||||||
var atime int64
|
var atime int64
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if sys, sysok := fi.Sys().(*syscall.Stat_t); !sysok {
|
atime, err = statAtime(fi.Sys())
|
||||||
err = fmt.Errorf("FileInfo.Sys not a *syscall.Stat_t but %T", fi.Sys())
|
|
||||||
} else {
|
|
||||||
atime = statAtime(sys)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xlog.Infox("removing unusable/unrecognized file in static gzip cache dir", err)
|
xlog.Infox("removing unusable/unrecognized file in static gzip cache dir", err)
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
func TestServeHTTP(t *testing.T) {
|
func TestServeHTTP(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/web/data")
|
os.RemoveAll("../testdata/web/data")
|
||||||
mox.ConfigStaticPath = "../testdata/web/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/web/mox.conf")
|
||||||
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ func tcheck(t *testing.T, err error, msg string) {
|
||||||
|
|
||||||
func TestWebserver(t *testing.T) {
|
func TestWebserver(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/webserver/data")
|
os.RemoveAll("../testdata/webserver/data")
|
||||||
mox.ConfigStaticPath = "../testdata/webserver/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webserver/mox.conf")
|
||||||
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ func TestWebserver(t *testing.T) {
|
||||||
|
|
||||||
func TestWebsocket(t *testing.T) {
|
func TestWebsocket(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/websocket/data")
|
os.RemoveAll("../testdata/websocket/data")
|
||||||
mox.ConfigStaticPath = "../testdata/websocket/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/websocket/mox.conf")
|
||||||
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ func FuzzServer(f *testing.F) {
|
||||||
}
|
}
|
||||||
|
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/imapserverfuzz/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/imapserverfuzz/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
||||||
os.RemoveAll(dataDir)
|
os.RemoveAll(dataDir)
|
||||||
|
|
|
@ -2,7 +2,7 @@ package imapserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@ func (c *conn) cmdList(tag, cmd string, p *parser) {
|
||||||
err := q.ForEach(func(mb store.Mailbox) error {
|
err := q.ForEach(func(mb store.Mailbox) error {
|
||||||
names[mb.Name] = info{mailbox: &mb}
|
names[mb.Name] = info{mailbox: &mb}
|
||||||
nameList = append(nameList, mb.Name)
|
nameList = append(nameList, mb.Name)
|
||||||
for p := filepath.Dir(mb.Name); p != "."; p = filepath.Dir(p) {
|
for p := path.Dir(mb.Name); p != "."; p = path.Dir(p) {
|
||||||
hasChild[p] = true
|
hasChild[p] = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -148,7 +148,7 @@ func (c *conn) cmdList(tag, cmd string, p *parser) {
|
||||||
if !ok {
|
if !ok {
|
||||||
nameList = append(nameList, sub.Name)
|
nameList = append(nameList, sub.Name)
|
||||||
}
|
}
|
||||||
for p := filepath.Dir(sub.Name); p != "."; p = filepath.Dir(p) {
|
for p := path.Dir(sub.Name); p != "."; p = path.Dir(p) {
|
||||||
hasSubscribedChild[p] = true
|
hasSubscribedChild[p] = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -50,6 +50,7 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
@ -895,7 +896,7 @@ func xmailboxPatternMatcher(ref string, patterns []string) matchStringer {
|
||||||
|
|
||||||
s := pat
|
s := pat
|
||||||
if ref != "" {
|
if ref != "" {
|
||||||
s = filepath.Join(ref, pat)
|
s = path.Join(ref, pat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix casing for all Inbox paths.
|
// Fix casing for all Inbox paths.
|
||||||
|
@ -2481,7 +2482,7 @@ func (c *conn) cmdLsub(tag, cmd string, p *parser) {
|
||||||
for _, sub := range subscriptions {
|
for _, sub := range subscriptions {
|
||||||
name := sub.Name
|
name := sub.Name
|
||||||
if ispercent {
|
if ispercent {
|
||||||
for p := filepath.Dir(name); p != "."; p = filepath.Dir(p) {
|
for p := path.Dir(name); p != "."; p = path.Dir(p) {
|
||||||
subscribedKids[p] = true
|
subscribedKids[p] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2675,12 +2676,11 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||||
msgFile, err := store.CreateMessageTemp("imap-append")
|
msgFile, err := store.CreateMessageTemp("imap-append")
|
||||||
xcheckf(err, "creating temp file for message")
|
xcheckf(err, "creating temp file for message")
|
||||||
defer func() {
|
defer func() {
|
||||||
if msgFile != nil {
|
p := msgFile.Name()
|
||||||
err := os.Remove(msgFile.Name())
|
err := msgFile.Close()
|
||||||
c.xsanity(err, "removing APPEND temporary file")
|
c.xsanity(err, "closing APPEND temporary file")
|
||||||
err = msgFile.Close()
|
err = os.Remove(p)
|
||||||
c.xsanity(err, "closing APPEND temporary file")
|
c.xsanity(err, "removing APPEND temporary file")
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
defer c.xtrace(mlog.LevelTracedata)()
|
defer c.xtrace(mlog.LevelTracedata)()
|
||||||
mw := message.NewWriter(msgFile)
|
mw := message.NewWriter(msgFile)
|
||||||
|
@ -2740,7 +2740,7 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||||
err = tx.Update(&mb)
|
err = tx.Update(&mb)
|
||||||
xcheckf(err, "updating mailbox counts")
|
xcheckf(err, "updating mailbox counts")
|
||||||
|
|
||||||
err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, true, false, false)
|
err := c.account.DeliverMessage(c.log, tx, &m, msgFile, true, false, false)
|
||||||
xcheckf(err, "delivering message")
|
xcheckf(err, "delivering message")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2754,10 +2754,6 @@ func (c *conn) cmdAppend(tag, cmd string, p *parser) {
|
||||||
c.broadcast(changes)
|
c.broadcast(changes)
|
||||||
})
|
})
|
||||||
|
|
||||||
err = msgFile.Close()
|
|
||||||
c.log.Check(err, "closing appended file")
|
|
||||||
msgFile = nil
|
|
||||||
|
|
||||||
if c.mailboxID == mb.ID {
|
if c.mailboxID == mb.ID {
|
||||||
c.applyChanges(pendingChanges, false)
|
c.applyChanges(pendingChanges, false)
|
||||||
c.uidAppend(m.UID)
|
c.uidAppend(m.UID)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -338,7 +339,7 @@ func startArgs(t *testing.T, first, isTLS, allowLoginWithoutTLS bool) *testconn
|
||||||
os.RemoveAll("../testdata/imap/data")
|
os.RemoveAll("../testdata/imap/data")
|
||||||
}
|
}
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/imap/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
acc, err := store.OpenAccount("mjl")
|
acc, err := store.OpenAccount("mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
|
|
16
import.go
16
import.go
|
@ -276,11 +276,10 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) {
|
||||||
xdeliver := func(m *store.Message, mf *os.File) {
|
xdeliver := func(m *store.Message, mf *os.File) {
|
||||||
// todo: possibly set dmarcdomain to the domain of the from address? at least for non-spams that have been seen. otherwise user would start without any reputations. the assumption would be that the user has accepted email and deemed it legit, coming from the indicated sender.
|
// todo: possibly set dmarcdomain to the domain of the from address? at least for non-spams that have been seen. otherwise user would start without any reputations. the assumption would be that the user has accepted email and deemed it legit, coming from the indicated sender.
|
||||||
|
|
||||||
const consumeFile = true
|
|
||||||
const sync = false
|
const sync = false
|
||||||
const notrain = true
|
const notrain = true
|
||||||
const nothreads = true
|
const nothreads = true
|
||||||
err := a.DeliverMessage(ctl.log, tx, m, mf, consumeFile, sync, notrain, nothreads)
|
err := a.DeliverMessage(ctl.log, tx, m, mf, sync, notrain, nothreads)
|
||||||
ctl.xcheck(err, "delivering message")
|
ctl.xcheck(err, "delivering message")
|
||||||
deliveredIDs = append(deliveredIDs, m.ID)
|
deliveredIDs = append(deliveredIDs, m.ID)
|
||||||
ctl.log.Debug("delivered message", mlog.Field("id", m.ID))
|
ctl.log.Debug("delivered message", mlog.Field("id", m.ID))
|
||||||
|
@ -313,13 +312,11 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) {
|
||||||
|
|
||||||
process := func(m *store.Message, msgf *os.File, origPath string) {
|
process := func(m *store.Message, msgf *os.File, origPath string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if msgf == nil {
|
name := msgf.Name()
|
||||||
return
|
err := msgf.Close()
|
||||||
}
|
|
||||||
err := os.Remove(msgf.Name())
|
|
||||||
ctl.log.Check(err, "removing temporary message after failing to import")
|
|
||||||
err = msgf.Close()
|
|
||||||
ctl.log.Check(err, "closing temporary message after failing to import")
|
ctl.log.Check(err, "closing temporary message after failing to import")
|
||||||
|
err = os.Remove(name)
|
||||||
|
ctl.log.Check(err, "removing temporary message after failing to import", mlog.Field("path", name))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, kw := range m.Keywords {
|
for _, kw := range m.Keywords {
|
||||||
|
@ -373,9 +370,6 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) {
|
||||||
m.CreateSeq = modseq
|
m.CreateSeq = modseq
|
||||||
m.ModSeq = modseq
|
m.ModSeq = modseq
|
||||||
xdeliver(m, msgf)
|
xdeliver(m, msgf)
|
||||||
err = msgf.Close()
|
|
||||||
ctl.log.Check(err, "closing message after delivery")
|
|
||||||
msgf = nil
|
|
||||||
|
|
||||||
n++
|
n++
|
||||||
if n%1000 == 0 {
|
if n%1000 == 0 {
|
||||||
|
|
9
junk.go
9
junk.go
|
@ -20,6 +20,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -157,7 +158,7 @@ func cmdJunkTest(c *cmd) {
|
||||||
files, err := os.ReadDir(dir)
|
files, err := os.ReadDir(dir)
|
||||||
xcheckf(err, "readdir %q", dir)
|
xcheckf(err, "readdir %q", dir)
|
||||||
for _, fi := range files {
|
for _, fi := range files {
|
||||||
path := dir + "/" + fi.Name()
|
path := filepath.Join(dir, fi.Name())
|
||||||
prob, _, _, _, err := f.ClassifyMessagePath(context.Background(), path)
|
prob, _, _, _, err := f.ClassifyMessagePath(context.Background(), path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("classify message %q: %s", path, err)
|
log.Printf("classify message %q: %s", path, err)
|
||||||
|
@ -249,7 +250,7 @@ messages are shuffled, with optional random seed.`
|
||||||
|
|
||||||
testDir := func(dir string, files []string, ham bool) (ok, bad, malformed int) {
|
testDir := func(dir string, files []string, ham bool) (ok, bad, malformed int) {
|
||||||
for _, name := range files {
|
for _, name := range files {
|
||||||
path := dir + "/" + name
|
path := filepath.Join(dir, name)
|
||||||
prob, _, _, _, err := f.ClassifyMessagePath(context.Background(), path)
|
prob, _, _, _, err := f.ClassifyMessagePath(context.Background(), path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// log.Infof("%s: %s", path, err)
|
// log.Infof("%s: %s", path, err)
|
||||||
|
@ -313,7 +314,7 @@ func cmdJunkPlay(c *cmd) {
|
||||||
|
|
||||||
scanDir := func(dir string, ham, sent bool) {
|
scanDir := func(dir string, ham, sent bool) {
|
||||||
for _, name := range listDir(dir) {
|
for _, name := range listDir(dir) {
|
||||||
path := dir + "/" + name
|
path := filepath.Join(dir, name)
|
||||||
mf, err := os.Open(path)
|
mf, err := os.Open(path)
|
||||||
xcheckf(err, "open %q", path)
|
xcheckf(err, "open %q", path)
|
||||||
fi, err := mf.Stat()
|
fi, err := mf.Stat()
|
||||||
|
@ -366,7 +367,7 @@ func cmdJunkPlay(c *cmd) {
|
||||||
|
|
||||||
play := func(msg msg) {
|
play := func(msg msg) {
|
||||||
var words map[string]struct{}
|
var words map[string]struct{}
|
||||||
path := msg.dir + "/" + msg.filename
|
path := filepath.Join(msg.dir, msg.filename)
|
||||||
if !msg.sent {
|
if !msg.sent {
|
||||||
var prob float64
|
var prob float64
|
||||||
var err error
|
var err error
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -644,7 +645,7 @@ func (f *Filter) TrainDir(dir string, files []string, ham bool) (n, malformed ui
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, name := range files {
|
for _, name := range files {
|
||||||
p := fmt.Sprintf("%s/%s", dir, name)
|
p := filepath.Join(dir, name)
|
||||||
valid, words, err := f.tokenizeMail(p)
|
valid, words, err := f.tokenizeMail(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// f.log.Infox("tokenizing mail", err, mlog.Field("path", p))
|
// f.log.Infox("tokenizing mail", err, mlog.Field("path", p))
|
||||||
|
|
|
@ -42,8 +42,8 @@ func TestFilter(t *testing.T) {
|
||||||
IgnoreWords: 0.1,
|
IgnoreWords: 0.1,
|
||||||
RareWords: 1,
|
RareWords: 1,
|
||||||
}
|
}
|
||||||
dbPath := "../testdata/junk/filter.db"
|
dbPath := filepath.FromSlash("../testdata/junk/filter.db")
|
||||||
bloomPath := "../testdata/junk/filter.bloom"
|
bloomPath := filepath.FromSlash("../testdata/junk/filter.bloom")
|
||||||
os.Remove(dbPath)
|
os.Remove(dbPath)
|
||||||
os.Remove(bloomPath)
|
os.Remove(bloomPath)
|
||||||
f, err := NewFilter(ctxbg, log, params, dbPath, bloomPath)
|
f, err := NewFilter(ctxbg, log, params, dbPath, bloomPath)
|
||||||
|
@ -59,8 +59,8 @@ func TestFilter(t *testing.T) {
|
||||||
os.MkdirAll("../testdata/train/ham", 0770)
|
os.MkdirAll("../testdata/train/ham", 0770)
|
||||||
os.MkdirAll("../testdata/train/spam", 0770)
|
os.MkdirAll("../testdata/train/spam", 0770)
|
||||||
|
|
||||||
hamdir := "../testdata/train/ham"
|
hamdir := filepath.FromSlash("../testdata/train/ham")
|
||||||
spamdir := "../testdata/train/spam"
|
spamdir := filepath.FromSlash("../testdata/train/spam")
|
||||||
hamfiles := tlistdir(t, hamdir)
|
hamfiles := tlistdir(t, hamdir)
|
||||||
if len(hamfiles) > 100 {
|
if len(hamfiles) > 100 {
|
||||||
hamfiles = hamfiles[:100]
|
hamfiles = hamfiles[:100]
|
||||||
|
|
|
@ -2,6 +2,7 @@ package junk
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,12 +15,12 @@ func FuzzParseMessage(f *testing.F) {
|
||||||
}
|
}
|
||||||
f.Add(string(buf))
|
f.Add(string(buf))
|
||||||
}
|
}
|
||||||
add("../testdata/junk/parse.eml")
|
add(filepath.FromSlash("../testdata/junk/parse.eml"))
|
||||||
add("../testdata/junk/parse2.eml")
|
add(filepath.FromSlash("../testdata/junk/parse2.eml"))
|
||||||
add("../testdata/junk/parse3.eml")
|
add(filepath.FromSlash("../testdata/junk/parse3.eml"))
|
||||||
|
|
||||||
dbPath := "../testdata/junk/parse.db"
|
dbPath := filepath.FromSlash("../testdata/junk/parse.db")
|
||||||
bloomPath := "../testdata/junk/parse.bloom"
|
bloomPath := filepath.FromSlash("../testdata/junk/parse.bloom")
|
||||||
os.Remove(dbPath)
|
os.Remove(dbPath)
|
||||||
os.Remove(bloomPath)
|
os.Remove(bloomPath)
|
||||||
params := Params{Twograms: true}
|
params := Params{Twograms: true}
|
||||||
|
|
|
@ -321,11 +321,15 @@ func writeLocalConfig(log *mlog.Log, dir, ip string) (rerr error) {
|
||||||
local.WebserverHTTPS.Enabled = true
|
local.WebserverHTTPS.Enabled = true
|
||||||
local.WebserverHTTPS.Port = 1443
|
local.WebserverHTTPS.Port = 1443
|
||||||
|
|
||||||
|
uid := os.Getuid()
|
||||||
|
if uid < 0 {
|
||||||
|
uid = 1 // For windows.
|
||||||
|
}
|
||||||
static := config.Static{
|
static := config.Static{
|
||||||
DataDir: ".",
|
DataDir: ".",
|
||||||
LogLevel: "traceauth",
|
LogLevel: "traceauth",
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
User: fmt.Sprintf("%d", os.Getuid()),
|
User: fmt.Sprintf("%d", uid),
|
||||||
AdminPasswordFile: "adminpasswd",
|
AdminPasswordFile: "adminpasswd",
|
||||||
Pedantic: true,
|
Pedantic: true,
|
||||||
Listeners: map[string]config.Listener{
|
Listeners: map[string]config.Listener{
|
||||||
|
|
4
main.go
4
main.go
|
@ -428,7 +428,7 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
flag.StringVar(&mox.ConfigStaticPath, "config", envString("MOXCONF", "config/mox.conf"), "configuration file, other config files are looked up in the same directory, defaults to $MOXCONF with a fallback to mox.conf")
|
flag.StringVar(&mox.ConfigStaticPath, "config", envString("MOXCONF", filepath.FromSlash("config/mox.conf")), "configuration file, other config files are looked up in the same directory, defaults to $MOXCONF with a fallback to mox.conf")
|
||||||
flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
|
flag.StringVar(&loglevel, "loglevel", "", "if non-empty, this log level is set early in startup")
|
||||||
flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
|
flag.BoolVar(&pedantic, "pedantic", false, "protocol violations result in errors instead of accepting/working around them")
|
||||||
flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
|
flag.BoolVar(&store.CheckConsistencyOnClose, "checkconsistency", false, "dangerous option for testing only, enables data checks that abort/panic when inconsistencies are found")
|
||||||
|
@ -1050,7 +1050,7 @@ for a domain and create the TLSA DNS records it suggests to enable DANE.
|
||||||
p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
|
p := mox.DataDirPath(filepath.Join("acme", "keycerts", l.TLS.ACME, filename))
|
||||||
privKey := xtryLoadPrivateKey(kt, p)
|
privKey := xtryLoadPrivateKey(kt, p)
|
||||||
|
|
||||||
relPath := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind)
|
relPath := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", host.Name(), timestamp, kind))
|
||||||
destPath := mox.ConfigDirPath(relPath)
|
destPath := mox.ConfigDirPath(relPath)
|
||||||
err := writeHostPrivateKey(privKey, destPath)
|
err := writeHostPrivateKey(privKey, destPath)
|
||||||
xcheckf(err, "writing host private key file to %s: %v", destPath, err)
|
xcheckf(err, "writing host private key file to %s: %v", destPath, err)
|
||||||
|
|
|
@ -178,10 +178,10 @@ func MakeDomainConfig(ctx context.Context, domain, hostname dns.Domain, accountN
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
err := os.Remove(path)
|
err := f.Close()
|
||||||
log.Check(err, "removing file after error")
|
|
||||||
err = f.Close()
|
|
||||||
log.Check(err, "closing file after error")
|
log.Check(err, "closing file after error")
|
||||||
|
err = os.Remove(path)
|
||||||
|
log.Check(err, "removing file after error", mlog.Field("path", path))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if _, err := f.Write(data); err != nil {
|
if _, err := f.Write(data); err != nil {
|
||||||
|
|
|
@ -471,8 +471,7 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
|
||||||
c.User = "mox"
|
c.User = "mox"
|
||||||
}
|
}
|
||||||
u, err := user.Lookup(c.User)
|
u, err := user.Lookup(c.User)
|
||||||
var userErr user.UnknownUserError
|
if err != nil {
|
||||||
if err != nil && errors.As(err, &userErr) {
|
|
||||||
uid, err := strconv.ParseUint(c.User, 10, 32)
|
uid, err := strconv.ParseUint(c.User, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
|
addErrorf("parsing unknown user %s as uid: %v (hint: add user mox with \"useradd -d $PWD mox\" or specify a different username on the quickstart command-line)", c.User, err)
|
||||||
|
@ -481,8 +480,6 @@ func PrepareStaticConfig(ctx context.Context, configFile string, conf *Config, c
|
||||||
c.UID = uint32(uid)
|
c.UID = uint32(uid)
|
||||||
c.GID = uint32(uid)
|
c.GID = uint32(uid)
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
|
||||||
addErrorf("looking up user: %v", err)
|
|
||||||
} else {
|
} else {
|
||||||
if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
|
if uid, err := strconv.ParseUint(u.Uid, 10, 32); err != nil {
|
||||||
addErrorf("parsing uid %s: %v", u.Uid, err)
|
addErrorf("parsing uid %s: %v", u.Uid, err)
|
||||||
|
|
74
mox-/forkexec_unix.go
Normal file
74
mox-/forkexec_unix.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package mox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fork and exec as unprivileged user.
|
||||||
|
//
|
||||||
|
// We don't use just setuid because it is hard to guarantee that no other
|
||||||
|
// privileged go worker processes have been started before we get here. E.g. init
|
||||||
|
// functions in packages can start goroutines.
|
||||||
|
func ForkExecUnprivileged() {
|
||||||
|
prog, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Fatalx("finding executable for exec", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []*os.File{os.Stdin, os.Stdout, os.Stderr}
|
||||||
|
var addrs []string
|
||||||
|
for addr, f := range passedListeners {
|
||||||
|
files = append(files, f)
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
var paths []string
|
||||||
|
for path, fl := range passedFiles {
|
||||||
|
for _, f := range fl {
|
||||||
|
files = append(files, f)
|
||||||
|
paths = append(paths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env := os.Environ()
|
||||||
|
env = append(env, "MOX_SOCKETS="+strings.Join(addrs, ","), "MOX_FILES="+strings.Join(paths, ","))
|
||||||
|
|
||||||
|
p, err := os.StartProcess(prog, os.Args, &os.ProcAttr{
|
||||||
|
Env: env,
|
||||||
|
Files: files,
|
||||||
|
Sys: &syscall.SysProcAttr{
|
||||||
|
Credential: &syscall.Credential{
|
||||||
|
Uid: Conf.Static.UID,
|
||||||
|
Gid: Conf.Static.GID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
xlog.Fatalx("fork and exec", err)
|
||||||
|
}
|
||||||
|
CleanupPassedFiles()
|
||||||
|
|
||||||
|
// If we get a interrupt/terminate signal, pass it on to the child. For interrupt,
|
||||||
|
// the child probably already got it.
|
||||||
|
// todo: see if we tie up child and root process so a kill -9 of the root process
|
||||||
|
// kills the child process too.
|
||||||
|
sigc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
sig := <-sigc
|
||||||
|
p.Signal(sig)
|
||||||
|
}()
|
||||||
|
|
||||||
|
st, err := p.Wait()
|
||||||
|
if err != nil {
|
||||||
|
xlog.Fatalx("wait", err)
|
||||||
|
}
|
||||||
|
code := st.ExitCode()
|
||||||
|
xlog.Print("stopping after child exit", mlog.Field("exitcode", code))
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
9
mox-/forkexec_windows.go
Normal file
9
mox-/forkexec_windows.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package mox
|
||||||
|
|
||||||
|
// Fork and exec as unprivileged user.
|
||||||
|
//
|
||||||
|
// Not implemented yet on windows. Would need to understand its security model
|
||||||
|
// first.
|
||||||
|
func ForkExecUnprivileged() {
|
||||||
|
xlog.Fatal("fork and exec to unprivileged user not yet implemented on windows")
|
||||||
|
}
|
|
@ -5,18 +5,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
"github.com/mjl-/mox/mlog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// We start up as root, bind to sockets, open private key/cert files and fork and
|
// We start up as root, bind to sockets, open private key/cert files and fork and
|
||||||
|
@ -56,68 +52,6 @@ func RestorePassedFiles() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fork and exec as unprivileged user.
|
|
||||||
//
|
|
||||||
// We don't use just setuid because it is hard to guarantee that no other
|
|
||||||
// privileged go worker processes have been started before we get here. E.g. init
|
|
||||||
// functions in packages can start goroutines.
|
|
||||||
func ForkExecUnprivileged() {
|
|
||||||
prog, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
xlog.Fatalx("finding executable for exec", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
files := []*os.File{os.Stdin, os.Stdout, os.Stderr}
|
|
||||||
var addrs []string
|
|
||||||
for addr, f := range passedListeners {
|
|
||||||
files = append(files, f)
|
|
||||||
addrs = append(addrs, addr)
|
|
||||||
}
|
|
||||||
var paths []string
|
|
||||||
for path, fl := range passedFiles {
|
|
||||||
for _, f := range fl {
|
|
||||||
files = append(files, f)
|
|
||||||
paths = append(paths, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
env := os.Environ()
|
|
||||||
env = append(env, "MOX_SOCKETS="+strings.Join(addrs, ","), "MOX_FILES="+strings.Join(paths, ","))
|
|
||||||
|
|
||||||
p, err := os.StartProcess(prog, os.Args, &os.ProcAttr{
|
|
||||||
Env: env,
|
|
||||||
Files: files,
|
|
||||||
Sys: &syscall.SysProcAttr{
|
|
||||||
Credential: &syscall.Credential{
|
|
||||||
Uid: Conf.Static.UID,
|
|
||||||
Gid: Conf.Static.GID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
xlog.Fatalx("fork and exec", err)
|
|
||||||
}
|
|
||||||
CleanupPassedFiles()
|
|
||||||
|
|
||||||
// If we get a interrupt/terminate signal, pass it on to the child. For interrupt,
|
|
||||||
// the child probably already got it.
|
|
||||||
// todo: see if we tie up child and root process so a kill -9 of the root process
|
|
||||||
// kills the child process too.
|
|
||||||
sigc := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
sig := <-sigc
|
|
||||||
p.Signal(sig)
|
|
||||||
}()
|
|
||||||
|
|
||||||
st, err := p.Wait()
|
|
||||||
if err != nil {
|
|
||||||
xlog.Fatalx("wait", err)
|
|
||||||
}
|
|
||||||
code := st.ExitCode()
|
|
||||||
xlog.Print("stopping after child exit", mlog.Field("exitcode", code))
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CleanupPassedFiles closes the listening socket file descriptors and files passed
|
// CleanupPassedFiles closes the listening socket file descriptors and files passed
|
||||||
// in by the parent process. To be called by the unprivileged child after listeners
|
// in by the parent process. To be called by the unprivileged child after listeners
|
||||||
// have been recreated (they dup the file descriptor), and by the privileged
|
// have been recreated (they dup the file descriptor), and by the privileged
|
||||||
|
@ -164,15 +98,19 @@ func Listen(network, addr string) (net.Listener, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tcpln, ok := ln.(*net.TCPListener)
|
// On windows, we cannot duplicate a socket. We don't need to for mox localserve
|
||||||
if !ok {
|
// with FilesImmediate.
|
||||||
return nil, fmt.Errorf("listener not a tcp listener, but %T, for network %s, address %s", ln, network, addr)
|
if !FilesImmediate {
|
||||||
|
tcpln, ok := ln.(*net.TCPListener)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("listener not a tcp listener, but %T, for network %s, address %s", ln, network, addr)
|
||||||
|
}
|
||||||
|
f, err := tcpln.File()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dup listener: %v", err)
|
||||||
|
}
|
||||||
|
passedListeners[addr] = f
|
||||||
}
|
}
|
||||||
f, err := tcpln.File()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("dup listener: %v", err)
|
|
||||||
}
|
|
||||||
passedListeners[addr] = f
|
|
||||||
return ln, err
|
return ln, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,10 @@ func LinkOrCopy(log *mlog.Log, dst, src string, srcReaderOpt io.Reader, sync boo
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if df != nil {
|
if df != nil {
|
||||||
err = os.Remove(dst)
|
err := df.Close()
|
||||||
log.Check(err, "removing partial destination file")
|
|
||||||
err = df.Close()
|
|
||||||
log.Check(err, "closing partial destination file")
|
log.Check(err, "closing partial destination file")
|
||||||
|
err = os.Remove(dst)
|
||||||
|
log.Check(err, "removing partial destination file", mlog.Field("path", dst))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ func LinkOrCopy(log *mlog.Log, dst, src string, srcReaderOpt io.Reader, sync boo
|
||||||
df = nil
|
df = nil
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := os.Remove(dst)
|
err := os.Remove(dst)
|
||||||
log.Check(err, "removing partial destination file")
|
log.Check(err, "removing partial destination file", mlog.Field("path", dst))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -26,6 +26,7 @@ func TestLinkOrCopy(t *testing.T) {
|
||||||
f, err := os.Create(src)
|
f, err := os.Create(src)
|
||||||
tcheckf(t, err, "creating test file")
|
tcheckf(t, err, "creating test file")
|
||||||
defer os.Remove(src)
|
defer os.Remove(src)
|
||||||
|
defer f.Close()
|
||||||
err = LinkOrCopy(log, "linkorcopytest-dst.txt", src, nil, false)
|
err = LinkOrCopy(log, "linkorcopytest-dst.txt", src, nil, false)
|
||||||
tcheckf(t, err, "linking file")
|
tcheckf(t, err, "linking file")
|
||||||
err = os.Remove("linkorcopytest-dst.txt")
|
err = os.Remove("linkorcopytest-dst.txt")
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
package moxio
|
package moxio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
8
moxio/syncdir_windows.go
Normal file
8
moxio/syncdir_windows.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package moxio
|
||||||
|
|
||||||
|
// SyncDir opens a directory and syncs its contents to disk.
|
||||||
|
// SyncDir is a no-op on Windows.
|
||||||
|
func SyncDir(dir string) error {
|
||||||
|
// todo: how to sync a directory on windows?
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ func tcheckf(t *testing.T, err error, format string, args ...any) {
|
||||||
|
|
||||||
func TestDB(t *testing.T) {
|
func TestDB(t *testing.T) {
|
||||||
mox.Shutdown = ctxbg
|
mox.Shutdown = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/mtasts/fake.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/mtasts/fake.conf")
|
||||||
mox.Conf.Static.DataDir = "."
|
mox.Conf.Static.DataDir = "."
|
||||||
|
|
||||||
dbpath := mox.DataDirPath("mtasts.db")
|
dbpath := mox.DataDirPath("mtasts.db")
|
||||||
|
|
|
@ -30,7 +30,7 @@ var ctxbg = context.Background()
|
||||||
|
|
||||||
func TestRefresh(t *testing.T) {
|
func TestRefresh(t *testing.T) {
|
||||||
mox.Shutdown = ctxbg
|
mox.Shutdown = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/mtasts/fake.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/mtasts/fake.conf")
|
||||||
mox.Conf.Static.DataDir = "."
|
mox.Conf.Static.DataDir = "."
|
||||||
|
|
||||||
dbpath := mox.DataDirPath("mtasts.db")
|
dbpath := mox.DataDirPath("mtasts.db")
|
||||||
|
|
16
queue/dsn.go
16
queue/dsn.go
|
@ -154,12 +154,11 @@ func queueDSN(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg stri
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if msgFile != nil {
|
name := msgFile.Name()
|
||||||
err := os.Remove(msgFile.Name())
|
err := msgFile.Close()
|
||||||
log.Check(err, "removing message file", mlog.Field("path", msgFile.Name()))
|
log.Check(err, "closing message file")
|
||||||
err = msgFile.Close()
|
err = os.Remove(name)
|
||||||
log.Check(err, "closing message file")
|
log.Check(err, "removing message file", mlog.Field("path", name))
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
msgWriter := message.NewWriter(msgFile)
|
msgWriter := message.NewWriter(msgFile)
|
||||||
|
@ -174,12 +173,9 @@ func queueDSN(log *mlog.Log, m Msg, remoteMTA dsn.NameIP, secodeOpt, errmsg stri
|
||||||
MsgPrefix: []byte{},
|
MsgPrefix: []byte{},
|
||||||
}
|
}
|
||||||
acc.WithWLock(func() {
|
acc.WithWLock(func() {
|
||||||
if err := acc.DeliverMailbox(log, mailbox, msg, msgFile, true); err != nil {
|
if err := acc.DeliverMailbox(log, mailbox, msg, msgFile); err != nil {
|
||||||
qlog("delivering dsn to mailbox", err)
|
qlog("delivering dsn to mailbox", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
err = msgFile.Close()
|
|
||||||
log.Check(err, "closing dsn file")
|
|
||||||
msgFile = nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,7 +118,7 @@ func (m Msg) MessagePath() string {
|
||||||
|
|
||||||
// Init opens the queue database without starting delivery.
|
// Init opens the queue database without starting delivery.
|
||||||
func Init() error {
|
func Init() error {
|
||||||
qpath := mox.DataDirPath("queue/index.db")
|
qpath := mox.DataDirPath(filepath.FromSlash("queue/index.db"))
|
||||||
os.MkdirAll(filepath.Dir(qpath), 0770)
|
os.MkdirAll(filepath.Dir(qpath), 0770)
|
||||||
isNew := false
|
isNew := false
|
||||||
if _, err := os.Stat(qpath); err != nil && os.IsNotExist(err) {
|
if _, err := os.Stat(qpath); err != nil && os.IsNotExist(err) {
|
||||||
|
@ -176,14 +176,11 @@ func Count(ctx context.Context) (int, error) {
|
||||||
// Add a new message to the queue. The queue is kicked immediately to start a
|
// Add a new message to the queue. The queue is kicked immediately to start a
|
||||||
// first delivery attempt.
|
// first delivery attempt.
|
||||||
//
|
//
|
||||||
// If consumeFile is true, it is removed as part of delivery (by rename or copy
|
|
||||||
// and remove). msgFile is never closed by Add.
|
|
||||||
//
|
|
||||||
// dnsutf8Opt is a utf8-version of the message, to be used only for DNSs. If set,
|
// dnsutf8Opt is a utf8-version of the message, to be used only for DNSs. If set,
|
||||||
// this data is used as the message when delivering the DSN and the remote SMTP
|
// this data is used as the message when delivering the DSN and the remote SMTP
|
||||||
// server supports SMTPUTF8. If the remote SMTP server does not support SMTPUTF8,
|
// server supports SMTPUTF8. If the remote SMTP server does not support SMTPUTF8,
|
||||||
// the regular non-utf8 message is delivered.
|
// the regular non-utf8 message is delivered.
|
||||||
func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcptTo smtp.Path, has8bit, smtputf8 bool, size int64, messageID string, msgPrefix []byte, msgFile *os.File, dsnutf8Opt []byte, consumeFile bool) (int64, error) {
|
func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcptTo smtp.Path, has8bit, smtputf8 bool, size int64, messageID string, msgPrefix []byte, msgFile *os.File, dsnutf8Opt []byte) (int64, error) {
|
||||||
// todo: Add should accept multiple rcptTo if they are for the same domain. so we can queue them for delivery in one (or just a few) session(s), transferring the data only once. ../rfc/5321:3759
|
// todo: Add should accept multiple rcptTo if they are for the same domain. so we can queue them for delivery in one (or just a few) session(s), transferring the data only once. ../rfc/5321:3759
|
||||||
|
|
||||||
if Localserve {
|
if Localserve {
|
||||||
|
@ -202,7 +199,7 @@ func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcp
|
||||||
conf, _ := acc.Conf()
|
conf, _ := acc.Conf()
|
||||||
dest := conf.Destinations[mailFrom.String()]
|
dest := conf.Destinations[mailFrom.String()]
|
||||||
acc.WithWLock(func() {
|
acc.WithWLock(func() {
|
||||||
err = acc.DeliverDestination(log, dest, &m, msgFile, consumeFile)
|
err = acc.DeliverDestination(log, dest, &m, msgFile)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("delivering message: %v", err)
|
return 0, fmt.Errorf("delivering message: %v", err)
|
||||||
|
@ -239,12 +236,7 @@ func Add(ctx context.Context, log *mlog.Log, senderAccount string, mailFrom, rcp
|
||||||
}()
|
}()
|
||||||
dstDir := filepath.Dir(dst)
|
dstDir := filepath.Dir(dst)
|
||||||
os.MkdirAll(dstDir, 0770)
|
os.MkdirAll(dstDir, 0770)
|
||||||
if consumeFile {
|
if err := moxio.LinkOrCopy(log, dst, msgFile.Name(), nil, true); err != nil {
|
||||||
if err := os.Rename(msgFile.Name(), dst); err != nil {
|
|
||||||
// Could be due to cross-filesystem rename. Users shouldn't configure their systems that way.
|
|
||||||
return 0, fmt.Errorf("move message into queue dir: %w", err)
|
|
||||||
}
|
|
||||||
} else if err := moxio.LinkOrCopy(log, dst, msgFile.Name(), nil, true); err != nil {
|
|
||||||
return 0, fmt.Errorf("linking/copying message to new file: %s", err)
|
return 0, fmt.Errorf("linking/copying message to new file: %s", err)
|
||||||
} else if err := moxio.SyncDir(dstDir); err != nil {
|
} else if err := moxio.SyncDir(dstDir); err != nil {
|
||||||
return 0, fmt.Errorf("sync directory: %v", err)
|
return 0, fmt.Errorf("sync directory: %v", err)
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -40,7 +41,7 @@ func setup(t *testing.T) (*store.Account, func()) {
|
||||||
// Prepare config so email can be delivered to mjl@mox.example.
|
// Prepare config so email can be delivered to mjl@mox.example.
|
||||||
os.RemoveAll("../testdata/queue/data")
|
os.RemoveAll("../testdata/queue/data")
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/queue/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/queue/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
acc, err := store.OpenAccount("mjl")
|
acc, err := store.OpenAccount("mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
|
@ -86,13 +87,15 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
path := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}
|
path := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
|
mf := prepareFile(t)
|
||||||
|
defer os.Remove(mf.Name())
|
||||||
|
defer mf.Close()
|
||||||
|
|
||||||
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
|
||||||
mf2 := prepareFile(t)
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf2, nil, false)
|
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
os.Remove(mf2.Name())
|
|
||||||
|
|
||||||
msgs, err = List(ctxbg)
|
msgs, err = List(ctxbg)
|
||||||
tcheck(t, err, "listing queue")
|
tcheck(t, err, "listing queue")
|
||||||
|
@ -385,7 +388,7 @@ func TestQueue(t *testing.T) {
|
||||||
|
|
||||||
// Add a message to be delivered with submit because of its route.
|
// Add a message to be delivered with submit because of its route.
|
||||||
topath := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "submit.example"}}}
|
topath := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "submit.example"}}}
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, topath, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
|
_, err = Add(ctxbg, xlog, "mjl", path, topath, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
wasNetDialer = testDeliver(fakeSubmitServer)
|
wasNetDialer = testDeliver(fakeSubmitServer)
|
||||||
if !wasNetDialer {
|
if !wasNetDialer {
|
||||||
|
@ -393,7 +396,7 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a message to be delivered with submit because of explicitly configured transport, that uses TLS.
|
// Add a message to be delivered with submit because of explicitly configured transport, that uses TLS.
|
||||||
msgID, err := Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err := Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
transportSubmitTLS := "submittls"
|
transportSubmitTLS := "submittls"
|
||||||
n, err = Kick(ctxbg, msgID, "", "", &transportSubmitTLS)
|
n, err = Kick(ctxbg, msgID, "", "", &transportSubmitTLS)
|
||||||
|
@ -417,7 +420,7 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a message to be delivered with socks.
|
// Add a message to be delivered with socks.
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<socks@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<socks@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
transportSocks := "socks"
|
transportSocks := "socks"
|
||||||
n, err = Kick(ctxbg, msgID, "", "", &transportSocks)
|
n, err = Kick(ctxbg, msgID, "", "", &transportSocks)
|
||||||
|
@ -431,7 +434,7 @@ func TestQueue(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add message to be delivered with opportunistic TLS verification.
|
// Add message to be delivered with opportunistic TLS verification.
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<opportunistictls@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -441,7 +444,7 @@ func TestQueue(t *testing.T) {
|
||||||
testDeliver(fakeSMTPSTARTTLSServer)
|
testDeliver(fakeSMTPSTARTTLSServer)
|
||||||
|
|
||||||
// Test fallback to plain text with TLS handshake fails.
|
// Test fallback to plain text with TLS handshake fails.
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<badtls@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<badtls@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -457,7 +460,7 @@ func TestQueue(t *testing.T) {
|
||||||
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: moxCert.Leaf.RawSubjectPublicKeyInfo},
|
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: moxCert.Leaf.RawSubjectPublicKeyInfo},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<dane@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<dane@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -472,7 +475,7 @@ func TestQueue(t *testing.T) {
|
||||||
{},
|
{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneunusable@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneunusable@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -489,7 +492,7 @@ func TestQueue(t *testing.T) {
|
||||||
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: make([]byte, sha256.Size)},
|
{Usage: adns.TLSAUsageDANEEE, Selector: adns.TLSASelectorSPKI, MatchType: adns.TLSAMatchTypeFull, CertAssoc: make([]byte, sha256.Size)},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneinsecure@localhost>", nil, prepareFile(t), nil, true)
|
msgID, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<daneinsecure@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
n, err = Kick(ctxbg, msgID, "", "", nil)
|
n, err = Kick(ctxbg, msgID, "", "", nil)
|
||||||
tcheck(t, err, "kick queue")
|
tcheck(t, err, "kick queue")
|
||||||
|
@ -504,7 +507,7 @@ func TestQueue(t *testing.T) {
|
||||||
resolver.TLSA = nil
|
resolver.TLSA = nil
|
||||||
|
|
||||||
// Add another message that we'll fail to deliver entirely.
|
// Add another message that we'll fail to deliver entirely.
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
|
|
||||||
msgs, err = List(ctxbg)
|
msgs, err = List(ctxbg)
|
||||||
|
@ -660,7 +663,10 @@ func TestQueueStart(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
path := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}
|
path := smtp.Path{Localpart: "mjl", IPDomain: dns.IPDomain{Domain: dns.Domain{ASCII: "mox.example"}}}
|
||||||
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, prepareFile(t), nil, true)
|
mf := prepareFile(t)
|
||||||
|
defer os.Remove(mf.Name())
|
||||||
|
defer mf.Close()
|
||||||
|
_, err = Add(ctxbg, xlog, "mjl", path, path, false, false, int64(len(testmsg)), "<test@localhost>", nil, mf, nil)
|
||||||
tcheck(t, err, "add message to queue for delivery")
|
tcheck(t, err, "add message to queue for delivery")
|
||||||
checkDialed(true)
|
checkDialed(true)
|
||||||
|
|
||||||
|
|
|
@ -592,7 +592,7 @@ many authentication failures).
|
||||||
|
|
||||||
dc := config.Dynamic{}
|
dc := config.Dynamic{}
|
||||||
sc := config.Static{
|
sc := config.Static{
|
||||||
DataDir: "../data",
|
DataDir: filepath.FromSlash("../data"),
|
||||||
User: user,
|
User: user,
|
||||||
LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
|
LogLevel: "debug", // Help new users, they'll bring it back to info when it all works.
|
||||||
Hostname: dnshostname.Name(),
|
Hostname: dnshostname.Name(),
|
||||||
|
@ -625,9 +625,9 @@ many authentication failures).
|
||||||
public.IMAPS.Enabled = true
|
public.IMAPS.Enabled = true
|
||||||
|
|
||||||
if existingWebserver {
|
if existingWebserver {
|
||||||
hostbase := fmt.Sprintf("path/to/%s", dnshostname.Name())
|
hostbase := filepath.FromSlash("path/to/" + dnshostname.Name())
|
||||||
mtastsbase := fmt.Sprintf("path/to/mta-sts.%s", domain.Name())
|
mtastsbase := filepath.FromSlash("path/to/mta-sts." + domain.Name())
|
||||||
autoconfigbase := fmt.Sprintf("path/to/autoconfig.%s", domain.Name())
|
autoconfigbase := filepath.FromSlash("path/to/autoconfig." + domain.Name())
|
||||||
public.TLS = &config.TLS{
|
public.TLS = &config.TLS{
|
||||||
KeyCerts: []config.KeyCert{
|
KeyCerts: []config.KeyCert{
|
||||||
{CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
|
{CertFile: hostbase + "-chain.crt.pem", KeyFile: hostbase + ".key.pem"},
|
||||||
|
@ -657,8 +657,8 @@ and check the admin page for the needed DNS records.`)
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
timestamp := now.Format("20060102T150405")
|
timestamp := now.Format("20060102T150405")
|
||||||
hostRSAPrivateKeyFile := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048")
|
hostRSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "rsa2048"))
|
||||||
hostECDSAPrivateKeyFile := fmt.Sprintf("hostkeys/%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256")
|
hostECDSAPrivateKeyFile := filepath.Join("hostkeys", fmt.Sprintf("%s.%s.%s.privatekey.pkcs8.pem", dnshostname.Name(), timestamp, "ecdsap256"))
|
||||||
xwritehostkeyfile := func(path string, key crypto.Signer) {
|
xwritehostkeyfile := func(path string, key crypto.Signer) {
|
||||||
buf, err := x509.MarshalPKCS8PrivateKey(key)
|
buf, err := x509.MarshalPKCS8PrivateKey(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -727,8 +727,8 @@ and check the admin page for the needed DNS records.`)
|
||||||
sc.Postmaster.Account = accountName
|
sc.Postmaster.Account = accountName
|
||||||
sc.Postmaster.Mailbox = "Postmaster"
|
sc.Postmaster.Mailbox = "Postmaster"
|
||||||
|
|
||||||
mox.ConfigStaticPath = "config/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("config/mox.conf")
|
||||||
mox.ConfigDynamicPath = "config/domains.conf"
|
mox.ConfigDynamicPath = filepath.FromSlash("config/domains.conf")
|
||||||
|
|
||||||
mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
|
mox.Conf.DynamicLastCheck = time.Now() // Prevent error logging by Make calls below.
|
||||||
|
|
||||||
|
@ -759,7 +759,7 @@ and check the admin page for the needed DNS records.`)
|
||||||
for _, bl := range public.SMTP.DNSBLs {
|
for _, bl := range public.SMTP.DNSBLs {
|
||||||
confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
|
confstr = strings.ReplaceAll(confstr, "- "+bl+"\n", "#- "+bl+"\n")
|
||||||
}
|
}
|
||||||
xwritefile("config/mox.conf", []byte(confstr), 0660)
|
xwritefile(filepath.FromSlash("config/mox.conf"), []byte(confstr), 0660)
|
||||||
|
|
||||||
// Generate domains config, and add a commented out example for delivery to a mailing list.
|
// Generate domains config, and add a commented out example for delivery to a mailing list.
|
||||||
var db bytes.Buffer
|
var db bytes.Buffer
|
||||||
|
@ -798,11 +798,11 @@ and check the admin page for the needed DNS records.`)
|
||||||
ndests += "#\t\t" + line + "\n"
|
ndests += "#\t\t" + line + "\n"
|
||||||
}
|
}
|
||||||
dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
|
dconfstr := strings.ReplaceAll(db.String(), odests, ndests)
|
||||||
xwritefile("config/domains.conf", []byte(dconfstr), 0660)
|
xwritefile(filepath.FromSlash("config/domains.conf"), []byte(dconfstr), 0660)
|
||||||
|
|
||||||
// Verify config.
|
// Verify config.
|
||||||
loadTLSKeyCerts := !existingWebserver
|
loadTLSKeyCerts := !existingWebserver
|
||||||
mc, errs := mox.ParseConfig(context.Background(), "config/mox.conf", true, loadTLSKeyCerts, false)
|
mc, errs := mox.ParseConfig(context.Background(), filepath.FromSlash("config/mox.conf"), true, loadTLSKeyCerts, false)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
if len(errs) > 1 {
|
if len(errs) > 1 {
|
||||||
log.Printf("checking generated config, multiple errors:")
|
log.Printf("checking generated config, multiple errors:")
|
||||||
|
|
|
@ -220,13 +220,7 @@ binary should be setgid that group:
|
||||||
os.Mkdir(maildir, 0700)
|
os.Mkdir(maildir, 0700)
|
||||||
f, err := os.CreateTemp(maildir, "newmsg.")
|
f, err := os.CreateTemp(maildir, "newmsg.")
|
||||||
xcheckf(err, "creating temp file for storing message after failed delivery")
|
xcheckf(err, "creating temp file for storing message after failed delivery")
|
||||||
defer func() {
|
// note: not removing the partial file if writing/closing below fails.
|
||||||
if f != nil {
|
|
||||||
if err := os.Remove(f.Name()); err != nil {
|
|
||||||
log.Printf("removing temp file after failure storing failed delivery: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
_, err = f.Write([]byte(msg))
|
_, err = f.Write([]byte(msg))
|
||||||
xcheckf(err, "writing message to temp file after failed delivery")
|
xcheckf(err, "writing message to temp file after failed delivery")
|
||||||
name := f.Name()
|
name := f.Name()
|
||||||
|
|
532
serve.go
532
serve.go
|
@ -1,393 +1,23 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
cryptorand "crypto/rand"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
|
|
||||||
"github.com/mjl-/mox/dmarcdb"
|
"github.com/mjl-/mox/dmarcdb"
|
||||||
"github.com/mjl-/mox/dns"
|
"github.com/mjl-/mox/dns"
|
||||||
"github.com/mjl-/mox/dnsbl"
|
|
||||||
"github.com/mjl-/mox/http"
|
"github.com/mjl-/mox/http"
|
||||||
"github.com/mjl-/mox/imapserver"
|
"github.com/mjl-/mox/imapserver"
|
||||||
"github.com/mjl-/mox/message"
|
|
||||||
"github.com/mjl-/mox/metrics"
|
|
||||||
"github.com/mjl-/mox/mlog"
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/mox-"
|
"github.com/mjl-/mox/mox-"
|
||||||
"github.com/mjl-/mox/moxvar"
|
|
||||||
"github.com/mjl-/mox/mtastsdb"
|
"github.com/mjl-/mox/mtastsdb"
|
||||||
"github.com/mjl-/mox/queue"
|
"github.com/mjl-/mox/queue"
|
||||||
"github.com/mjl-/mox/smtpserver"
|
"github.com/mjl-/mox/smtpserver"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
"github.com/mjl-/mox/tlsrptdb"
|
"github.com/mjl-/mox/tlsrptdb"
|
||||||
"github.com/mjl-/mox/updates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func monitorDNSBL(log *mlog.Log) {
|
|
||||||
defer func() {
|
|
||||||
// On error, don't bring down the entire server.
|
|
||||||
x := recover()
|
|
||||||
if x != nil {
|
|
||||||
log.Error("monitordnsbl panic", mlog.Field("panic", x))
|
|
||||||
debug.PrintStack()
|
|
||||||
metrics.PanicInc(metrics.Serve)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
l, ok := mox.Conf.Static.Listeners["public"]
|
|
||||||
if !ok {
|
|
||||||
log.Info("no listener named public, not monitoring our ips at dnsbls")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var zones []dns.Domain
|
|
||||||
for _, zone := range l.SMTP.DNSBLs {
|
|
||||||
d, err := dns.ParseDomain(zone)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalx("parsing dnsbls zone", err, mlog.Field("zone", zone))
|
|
||||||
}
|
|
||||||
zones = append(zones, d)
|
|
||||||
}
|
|
||||||
if len(zones) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
type key struct {
|
|
||||||
zone dns.Domain
|
|
||||||
ip string
|
|
||||||
}
|
|
||||||
metrics := map[key]prometheus.GaugeFunc{}
|
|
||||||
var statusMutex sync.Mutex
|
|
||||||
statuses := map[key]bool{}
|
|
||||||
|
|
||||||
resolver := dns.StrictResolver{Pkg: "dnsblmonitor"}
|
|
||||||
var sleep time.Duration // No sleep on first iteration.
|
|
||||||
for {
|
|
||||||
time.Sleep(sleep)
|
|
||||||
sleep = 3 * time.Hour
|
|
||||||
|
|
||||||
ips, err := mox.IPs(mox.Context, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorx("listing ips for dnsbl monitor", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, ip := range ips {
|
|
||||||
if ip.IsLoopback() || ip.IsPrivate() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, zone := range zones {
|
|
||||||
status, expl, err := dnsbl.Lookup(mox.Context, resolver, zone, ip)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorx("dnsbl monitor lookup", err, mlog.Field("ip", ip), mlog.Field("zone", zone), mlog.Field("expl", expl), mlog.Field("status", status))
|
|
||||||
}
|
|
||||||
k := key{zone, ip.String()}
|
|
||||||
|
|
||||||
statusMutex.Lock()
|
|
||||||
statuses[k] = status == dnsbl.StatusPass
|
|
||||||
statusMutex.Unlock()
|
|
||||||
|
|
||||||
if _, ok := metrics[k]; !ok {
|
|
||||||
metrics[k] = promauto.NewGaugeFunc(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "mox_dnsbl_ips_success",
|
|
||||||
Help: "DNSBL lookups to configured DNSBLs of our IPs.",
|
|
||||||
ConstLabels: prometheus.Labels{
|
|
||||||
"zone": zone.LogString(),
|
|
||||||
"ip": k.ip,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
func() float64 {
|
|
||||||
statusMutex.Lock()
|
|
||||||
defer statusMutex.Unlock()
|
|
||||||
if statuses[k] {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// also see localserve.go, code is similar or even shared.
|
|
||||||
func cmdServe(c *cmd) {
|
|
||||||
c.help = `Start mox, serving SMTP/IMAP/HTTPS.
|
|
||||||
|
|
||||||
Incoming email is accepted over SMTP. Email can be retrieved by users using
|
|
||||||
IMAP. HTTP listeners are started for the admin/account web interfaces, and for
|
|
||||||
automated TLS configuration. Missing essential TLS certificates are immediately
|
|
||||||
requested, other TLS certificates are requested on demand.
|
|
||||||
`
|
|
||||||
args := c.Parse()
|
|
||||||
if len(args) != 0 {
|
|
||||||
c.Usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set debug logging until config is fully loaded.
|
|
||||||
mlog.Logfmt = true
|
|
||||||
mox.Conf.Log[""] = mlog.LevelDebug
|
|
||||||
mlog.SetConfig(mox.Conf.Log)
|
|
||||||
|
|
||||||
checkACMEHosts := os.Getuid() != 0
|
|
||||||
|
|
||||||
log := mlog.New("serve")
|
|
||||||
|
|
||||||
if os.Getuid() == 0 {
|
|
||||||
mox.MustLoadConfig(true, checkACMEHosts)
|
|
||||||
|
|
||||||
// No need to potentially start and keep multiple processes. As root, we just need
|
|
||||||
// to start the child process.
|
|
||||||
runtime.GOMAXPROCS(1)
|
|
||||||
|
|
||||||
moxconf, err := filepath.Abs(mox.ConfigStaticPath)
|
|
||||||
log.Check(err, "finding absolute mox.conf path")
|
|
||||||
domainsconf, err := filepath.Abs(mox.ConfigDynamicPath)
|
|
||||||
log.Check(err, "finding absolute domains.conf path")
|
|
||||||
|
|
||||||
log.Print("starting as root, initializing network listeners", mlog.Field("version", moxvar.Version), mlog.Field("pid", os.Getpid()), mlog.Field("moxconf", moxconf), mlog.Field("domainsconf", domainsconf))
|
|
||||||
if os.Getenv("MOX_SOCKETS") != "" {
|
|
||||||
log.Fatal("refusing to start as root with $MOX_SOCKETS set")
|
|
||||||
}
|
|
||||||
if os.Getenv("MOX_FILES") != "" {
|
|
||||||
log.Fatal("refusing to start as root with $MOX_FILES set")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mox.Conf.Static.NoFixPermissions {
|
|
||||||
// Fix permissions now that we have privilege to do so. Useful for update of v0.0.1
|
|
||||||
// that was running directly as mox-user.
|
|
||||||
workdir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Printx("get working dir, continuing without potentially fixing up permissions", err)
|
|
||||||
} else {
|
|
||||||
configdir := filepath.Dir(mox.ConfigStaticPath)
|
|
||||||
datadir := mox.DataDirPath(".")
|
|
||||||
err := fixperms(log, workdir, configdir, datadir, mox.Conf.Static.UID, mox.Conf.Static.GID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalx("fixing permissions", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mox.RestorePassedFiles()
|
|
||||||
mox.MustLoadConfig(true, checkACMEHosts)
|
|
||||||
log.Print("starting as unprivileged user", mlog.Field("user", mox.Conf.Static.User), mlog.Field("uid", mox.Conf.Static.UID), mlog.Field("gid", mox.Conf.Static.GID), mlog.Field("pid", os.Getpid()))
|
|
||||||
}
|
|
||||||
|
|
||||||
syscall.Umask(syscall.Umask(007) | 007)
|
|
||||||
|
|
||||||
// Initialize key and random buffer for creating opaque SMTP
|
|
||||||
// transaction IDs based on "cid"s.
|
|
||||||
recvidpath := mox.DataDirPath("receivedid.key")
|
|
||||||
recvidbuf, err := os.ReadFile(recvidpath)
|
|
||||||
if err != nil || len(recvidbuf) != 16+8 {
|
|
||||||
recvidbuf = make([]byte, 16+8)
|
|
||||||
if _, err := cryptorand.Read(recvidbuf); err != nil {
|
|
||||||
log.Fatalx("reading random recvid data", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(recvidpath, recvidbuf, 0660); err != nil {
|
|
||||||
log.Fatalx("writing recvidpath", err, mlog.Field("path", recvidpath))
|
|
||||||
}
|
|
||||||
err := os.Chown(recvidpath, int(mox.Conf.Static.UID), 0)
|
|
||||||
log.Check(err, "chown receveidid.key", mlog.Field("path", recvidpath), mlog.Field("uid", mox.Conf.Static.UID), mlog.Field("gid", 0))
|
|
||||||
err = os.Chmod(recvidpath, 0640)
|
|
||||||
log.Check(err, "chmod receveidid.key to 0640", mlog.Field("path", recvidpath))
|
|
||||||
}
|
|
||||||
if err := mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:]); err != nil {
|
|
||||||
log.Fatalx("init receivedid", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start mox. If running as root, this will bind/listen on network sockets, and
|
|
||||||
// fork and exec itself as unprivileged user, then waits for the child to stop and
|
|
||||||
// exit. When running as root, this function never returns. But the new
|
|
||||||
// unprivileged user will get here again, with network sockets prepared.
|
|
||||||
//
|
|
||||||
// We listen to the unix domain ctl socket afterwards, which we always remove
|
|
||||||
// before listening. We need to do that because we may not have cleaned up our
|
|
||||||
// control socket during unexpected shutdown. We don't want to remove and listen on
|
|
||||||
// the unix domain socket first. If we would, we would make the existing instance
|
|
||||||
// unreachable over its ctl socket, and then fail because the network addresses are
|
|
||||||
// taken.
|
|
||||||
const mtastsdbRefresher = true
|
|
||||||
const skipForkExec = false
|
|
||||||
if err := start(mtastsdbRefresher, skipForkExec); err != nil {
|
|
||||||
log.Fatalx("start", err)
|
|
||||||
}
|
|
||||||
log.Print("ready to serve")
|
|
||||||
|
|
||||||
if mox.Conf.Static.CheckUpdates {
|
|
||||||
checkUpdates := func() time.Duration {
|
|
||||||
next := 24 * time.Hour
|
|
||||||
current, lastknown, mtime, err := mox.LastKnown()
|
|
||||||
if err != nil {
|
|
||||||
log.Infox("determining own version before checking for updates, trying again in 24h", err)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't want to check for updates at every startup. So we sleep based on file
|
|
||||||
// mtime. But file won't exist initially.
|
|
||||||
if !mtime.IsZero() && time.Since(mtime) < 24*time.Hour {
|
|
||||||
d := 24*time.Hour - time.Since(mtime)
|
|
||||||
log.Debug("sleeping for next check for updates", mlog.Field("sleep", d))
|
|
||||||
time.Sleep(d)
|
|
||||||
next = 0
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
if err := os.Chtimes(mox.DataDirPath("lastknownversion"), now, now); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
log.Infox("setting mtime on lastknownversion file, continuing", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("checking for updates", mlog.Field("lastknown", lastknown))
|
|
||||||
updatesctx, updatescancel := context.WithTimeout(mox.Context, time.Minute)
|
|
||||||
latest, _, changelog, err := updates.Check(updatesctx, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain}, lastknown, changelogURL, changelogPubKey)
|
|
||||||
updatescancel()
|
|
||||||
if err != nil {
|
|
||||||
log.Infox("checking for updates", err, mlog.Field("latest", latest))
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
if !latest.After(lastknown) {
|
|
||||||
log.Debug("no new version available")
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
if len(changelog.Changes) == 0 {
|
|
||||||
log.Info("new version available, but changelog is empty, ignoring", mlog.Field("latest", latest))
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
var cl string
|
|
||||||
for _, c := range changelog.Changes {
|
|
||||||
cl += "----\n\n" + strings.TrimSpace(c.Text) + "\n\n"
|
|
||||||
}
|
|
||||||
cl += "----"
|
|
||||||
|
|
||||||
a, err := store.OpenAccount(mox.Conf.Static.Postmaster.Account)
|
|
||||||
if err != nil {
|
|
||||||
log.Infox("open account for postmaster changelog delivery", err)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := a.Close()
|
|
||||||
log.Check(err, "closing account")
|
|
||||||
}()
|
|
||||||
f, err := store.CreateMessageTemp("changelog")
|
|
||||||
if err != nil {
|
|
||||||
log.Infox("making temporary message file for changelog delivery", err)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if f != nil {
|
|
||||||
err := os.Remove(f.Name())
|
|
||||||
log.Check(err, "removing temp changelog file")
|
|
||||||
err = f.Close()
|
|
||||||
log.Check(err, "closing temp changelog file")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
m := &store.Message{
|
|
||||||
Received: time.Now(),
|
|
||||||
Flags: store.Flags{Flagged: true},
|
|
||||||
}
|
|
||||||
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"))
|
|
||||||
if err != nil {
|
|
||||||
log.Infox("writing temporary message file for changelog delivery", err)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
m.Size = int64(n)
|
|
||||||
if err := a.DeliverMailbox(log, mox.Conf.Static.Postmaster.Mailbox, m, f, true); err != nil {
|
|
||||||
log.Errorx("changelog delivery", err)
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
f = nil
|
|
||||||
log.Info("delivered changelog", mlog.Field("current", current), mlog.Field("lastknown", lastknown), mlog.Field("latest", latest))
|
|
||||||
if err := mox.StoreLastKnown(latest); err != nil {
|
|
||||||
// This will be awkward, we'll keep notifying the postmaster once every 24h...
|
|
||||||
log.Infox("updating last known version", err)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
next := checkUpdates()
|
|
||||||
time.Sleep(next)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
go monitorDNSBL(log)
|
|
||||||
|
|
||||||
ctlpath := mox.DataDirPath("ctl")
|
|
||||||
_ = os.Remove(ctlpath)
|
|
||||||
ctl, err := net.Listen("unix", ctlpath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalx("listen on ctl unix domain socket", err)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
conn, err := ctl.Accept()
|
|
||||||
if err != nil {
|
|
||||||
log.Printx("accept for ctl", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cid := mox.Cid()
|
|
||||||
ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
|
|
||||||
go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) })
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Remove old temporary files that somehow haven't been cleaned up.
|
|
||||||
tmpdir := mox.DataDirPath("tmp")
|
|
||||||
os.MkdirAll(tmpdir, 0770)
|
|
||||||
tmps, err := os.ReadDir(tmpdir)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorx("listing files in tmpdir", err)
|
|
||||||
} else {
|
|
||||||
now := time.Now()
|
|
||||||
for _, e := range tmps {
|
|
||||||
if fi, err := e.Info(); err != nil {
|
|
||||||
log.Errorx("stat tmp file", err, mlog.Field("filename", e.Name()))
|
|
||||||
} else if now.Sub(fi.ModTime()) > 7*24*time.Hour && !fi.IsDir() {
|
|
||||||
p := filepath.Join(tmpdir, e.Name())
|
|
||||||
if err := os.Remove(p); err != nil {
|
|
||||||
log.Errorx("removing stale temporary file", err, mlog.Field("path", p))
|
|
||||||
} else {
|
|
||||||
log.Info("removed stale temporary file", mlog.Field("path", p))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Graceful shutdown.
|
|
||||||
sigc := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
|
||||||
sig := <-sigc
|
|
||||||
log.Print("shutting down, waiting max 3s for existing connections", mlog.Field("signal", sig))
|
|
||||||
shutdown(log)
|
|
||||||
if num, ok := sig.(syscall.Signal); ok {
|
|
||||||
os.Exit(int(num))
|
|
||||||
} else {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shutdown(log *mlog.Log) {
|
func shutdown(log *mlog.Log) {
|
||||||
// We indicate we are shutting down. Causes new connections and new SMTP commands
|
// We indicate we are shutting down. Causes new connections and new SMTP commands
|
||||||
// to be rejected. Should stop active connections pretty quickly.
|
// to be rejected. Should stop active connections pretty quickly.
|
||||||
|
@ -420,168 +50,6 @@ func shutdown(log *mlog.Log) {
|
||||||
log.Check(err, "removing ctl unix domain socket during shutdown")
|
log.Check(err, "removing ctl unix domain socket during shutdown")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set correct permissions for mox working directory, binary, config and data and service file.
|
|
||||||
//
|
|
||||||
// We require being able to stat the basic non-optional paths. Then we'll try to
|
|
||||||
// fix up permissions. If an error occurs when fixing permissions, we log and
|
|
||||||
// continue (could not be an actual problem).
|
|
||||||
func fixperms(log *mlog.Log, workdir, configdir, datadir string, moxuid, moxgid uint32) (rerr error) {
|
|
||||||
type fserr struct{ Err error }
|
|
||||||
defer func() {
|
|
||||||
x := recover()
|
|
||||||
if x == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e, ok := x.(fserr)
|
|
||||||
if ok {
|
|
||||||
rerr = e.Err
|
|
||||||
} else {
|
|
||||||
panic(x)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
checkf := func(err error, format string, args ...any) {
|
|
||||||
if err != nil {
|
|
||||||
panic(fserr{fmt.Errorf(format, args...)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changes we have to make. We collect them first, then apply.
|
|
||||||
type change struct {
|
|
||||||
path string
|
|
||||||
uid, gid *uint32
|
|
||||||
olduid, oldgid uint32
|
|
||||||
mode *fs.FileMode
|
|
||||||
oldmode fs.FileMode
|
|
||||||
}
|
|
||||||
var changes []change
|
|
||||||
|
|
||||||
ensure := func(p string, uid, gid uint32, perm fs.FileMode) bool {
|
|
||||||
fi, err := os.Stat(p)
|
|
||||||
checkf(err, "stat %s", p)
|
|
||||||
|
|
||||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
|
||||||
if !ok {
|
|
||||||
checkf(fmt.Errorf("got %T", st), "stat sys, expected syscall.Stat_t")
|
|
||||||
}
|
|
||||||
|
|
||||||
var ch change
|
|
||||||
if st.Uid != uid || st.Gid != gid {
|
|
||||||
ch.uid = &uid
|
|
||||||
ch.gid = &gid
|
|
||||||
ch.olduid = st.Uid
|
|
||||||
ch.oldgid = st.Gid
|
|
||||||
}
|
|
||||||
if perm != fi.Mode()&(fs.ModeSetgid|0777) {
|
|
||||||
ch.mode = &perm
|
|
||||||
ch.oldmode = fi.Mode() & (fs.ModeSetgid | 0777)
|
|
||||||
}
|
|
||||||
var zerochange change
|
|
||||||
if ch == zerochange {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ch.path = p
|
|
||||||
changes = append(changes, ch)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
xexists := func(p string) bool {
|
|
||||||
_, err := os.Stat(p)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
checkf(err, "stat %s", p)
|
|
||||||
}
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We ensure these permissions:
|
|
||||||
//
|
|
||||||
// $workdir root:mox 0751
|
|
||||||
// $configdir mox:root 0750 + setgid, and recursively (but files 0640)
|
|
||||||
// $datadir mox:root 0750 + setgid, and recursively (but files 0640)
|
|
||||||
// $workdir/mox (binary, optional) root:mox 0750
|
|
||||||
// $workdir/mox.service (systemd service file, optional) root:root 0644
|
|
||||||
|
|
||||||
const root = 0
|
|
||||||
ensure(workdir, root, moxgid, 0751)
|
|
||||||
fixconfig := ensure(configdir, moxuid, 0, fs.ModeSetgid|0750)
|
|
||||||
fixdata := ensure(datadir, moxuid, 0, fs.ModeSetgid|0750)
|
|
||||||
|
|
||||||
// Binary and systemd service file do not exist (there) when running under docker.
|
|
||||||
binary := filepath.Join(workdir, "mox")
|
|
||||||
if xexists(binary) {
|
|
||||||
ensure(binary, root, moxgid, 0750)
|
|
||||||
}
|
|
||||||
svc := filepath.Join(workdir, "mox.service")
|
|
||||||
if xexists(svc) {
|
|
||||||
ensure(svc, root, root, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(changes) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply changes.
|
|
||||||
log.Print("fixing up permissions, will continue on errors")
|
|
||||||
for _, ch := range changes {
|
|
||||||
if ch.uid != nil {
|
|
||||||
err := os.Chown(ch.path, int(*ch.uid), int(*ch.gid))
|
|
||||||
log.Printx("chown, fixing uid/gid", err, mlog.Field("path", ch.path), mlog.Field("olduid", ch.olduid), mlog.Field("oldgid", ch.oldgid), mlog.Field("newuid", *ch.uid), mlog.Field("newgid", *ch.gid))
|
|
||||||
}
|
|
||||||
if ch.mode != nil {
|
|
||||||
err := os.Chmod(ch.path, *ch.mode)
|
|
||||||
log.Printx("chmod, fixing permissions", err, mlog.Field("path", ch.path), mlog.Field("oldmode", fmt.Sprintf("%03o", ch.oldmode)), mlog.Field("newmode", fmt.Sprintf("%03o", *ch.mode)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
walkchange := func(dir string) {
|
|
||||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
log.Printx("walk error, continuing", err, mlog.Field("path", path))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fi, err := d.Info()
|
|
||||||
if err != nil {
|
|
||||||
log.Printx("stat during walk, continuing", err, mlog.Field("path", path))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
|
||||||
if !ok {
|
|
||||||
log.Printx("syscall stat during walk, continuing", err, mlog.Field("path", path))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if st.Uid != moxuid || st.Gid != root {
|
|
||||||
err := os.Chown(path, int(moxuid), root)
|
|
||||||
log.Printx("walk chown, fixing uid/gid", err, mlog.Field("path", path), mlog.Field("olduid", st.Uid), mlog.Field("oldgid", st.Gid), mlog.Field("newuid", moxuid), mlog.Field("newgid", root))
|
|
||||||
}
|
|
||||||
omode := fi.Mode() & (fs.ModeSetgid | 0777)
|
|
||||||
var nmode fs.FileMode
|
|
||||||
if fi.IsDir() {
|
|
||||||
nmode = fs.ModeSetgid | 0750
|
|
||||||
} else {
|
|
||||||
nmode = 0640
|
|
||||||
}
|
|
||||||
if omode != nmode {
|
|
||||||
err := os.Chmod(path, nmode)
|
|
||||||
log.Printx("walk chmod, fixing permissions", err, mlog.Field("path", path), mlog.Field("oldmode", fmt.Sprintf("%03o", omode)), mlog.Field("newmode", fmt.Sprintf("%03o", nmode)))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
log.Check(err, "walking dir to fix permissions", mlog.Field("dir", dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If config or data dir needed fixing, also set uid/gid and mode and files/dirs
|
|
||||||
// inside, recursively. We don't always recurse, data probably contains many files.
|
|
||||||
if fixconfig {
|
|
||||||
log.Print("fixing permissions in config dir", mlog.Field("configdir", configdir))
|
|
||||||
walkchange(configdir)
|
|
||||||
}
|
|
||||||
if fixdata {
|
|
||||||
log.Print("fixing permissions in data dir", mlog.Field("configdir", configdir))
|
|
||||||
walkchange(datadir)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// start initializes all packages, starts all listeners and the switchboard
|
// start initializes all packages, starts all listeners and the switchboard
|
||||||
// goroutine, then returns.
|
// goroutine, then returns.
|
||||||
func start(mtastsdbRefresher, skipForkExec bool) error {
|
func start(mtastsdbRefresher, skipForkExec bool) error {
|
||||||
|
|
546
serve_unix.go
Normal file
546
serve_unix.go
Normal file
|
@ -0,0 +1,546 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
cryptorand "crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
|
"github.com/mjl-/mox/dns"
|
||||||
|
"github.com/mjl-/mox/dnsbl"
|
||||||
|
"github.com/mjl-/mox/message"
|
||||||
|
"github.com/mjl-/mox/metrics"
|
||||||
|
"github.com/mjl-/mox/mlog"
|
||||||
|
"github.com/mjl-/mox/mox-"
|
||||||
|
"github.com/mjl-/mox/moxvar"
|
||||||
|
"github.com/mjl-/mox/store"
|
||||||
|
"github.com/mjl-/mox/updates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func monitorDNSBL(log *mlog.Log) {
|
||||||
|
defer func() {
|
||||||
|
// On error, don't bring down the entire server.
|
||||||
|
x := recover()
|
||||||
|
if x != nil {
|
||||||
|
log.Error("monitordnsbl panic", mlog.Field("panic", x))
|
||||||
|
debug.PrintStack()
|
||||||
|
metrics.PanicInc(metrics.Serve)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
l, ok := mox.Conf.Static.Listeners["public"]
|
||||||
|
if !ok {
|
||||||
|
log.Info("no listener named public, not monitoring our ips at dnsbls")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var zones []dns.Domain
|
||||||
|
for _, zone := range l.SMTP.DNSBLs {
|
||||||
|
d, err := dns.ParseDomain(zone)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalx("parsing dnsbls zone", err, mlog.Field("zone", zone))
|
||||||
|
}
|
||||||
|
zones = append(zones, d)
|
||||||
|
}
|
||||||
|
if len(zones) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type key struct {
|
||||||
|
zone dns.Domain
|
||||||
|
ip string
|
||||||
|
}
|
||||||
|
metrics := map[key]prometheus.GaugeFunc{}
|
||||||
|
var statusMutex sync.Mutex
|
||||||
|
statuses := map[key]bool{}
|
||||||
|
|
||||||
|
resolver := dns.StrictResolver{Pkg: "dnsblmonitor"}
|
||||||
|
var sleep time.Duration // No sleep on first iteration.
|
||||||
|
for {
|
||||||
|
time.Sleep(sleep)
|
||||||
|
sleep = 3 * time.Hour
|
||||||
|
|
||||||
|
ips, err := mox.IPs(mox.Context, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorx("listing ips for dnsbl monitor", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.IsLoopback() || ip.IsPrivate() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, zone := range zones {
|
||||||
|
status, expl, err := dnsbl.Lookup(mox.Context, resolver, zone, ip)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorx("dnsbl monitor lookup", err, mlog.Field("ip", ip), mlog.Field("zone", zone), mlog.Field("expl", expl), mlog.Field("status", status))
|
||||||
|
}
|
||||||
|
k := key{zone, ip.String()}
|
||||||
|
|
||||||
|
statusMutex.Lock()
|
||||||
|
statuses[k] = status == dnsbl.StatusPass
|
||||||
|
statusMutex.Unlock()
|
||||||
|
|
||||||
|
if _, ok := metrics[k]; !ok {
|
||||||
|
metrics[k] = promauto.NewGaugeFunc(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "mox_dnsbl_ips_success",
|
||||||
|
Help: "DNSBL lookups to configured DNSBLs of our IPs.",
|
||||||
|
ConstLabels: prometheus.Labels{
|
||||||
|
"zone": zone.LogString(),
|
||||||
|
"ip": k.ip,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
func() float64 {
|
||||||
|
statusMutex.Lock()
|
||||||
|
defer statusMutex.Unlock()
|
||||||
|
if statuses[k] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// also see localserve.go, code is similar or even shared.
|
||||||
|
func cmdServe(c *cmd) {
|
||||||
|
c.help = `Start mox, serving SMTP/IMAP/HTTPS.
|
||||||
|
|
||||||
|
Incoming email is accepted over SMTP. Email can be retrieved by users using
|
||||||
|
IMAP. HTTP listeners are started for the admin/account web interfaces, and for
|
||||||
|
automated TLS configuration. Missing essential TLS certificates are immediately
|
||||||
|
requested, other TLS certificates are requested on demand.
|
||||||
|
|
||||||
|
Only implemented on unix systems, not Windows.
|
||||||
|
`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 0 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set debug logging until config is fully loaded.
|
||||||
|
mlog.Logfmt = true
|
||||||
|
mox.Conf.Log[""] = mlog.LevelDebug
|
||||||
|
mlog.SetConfig(mox.Conf.Log)
|
||||||
|
|
||||||
|
checkACMEHosts := os.Getuid() != 0
|
||||||
|
|
||||||
|
log := mlog.New("serve")
|
||||||
|
|
||||||
|
if os.Getuid() == 0 {
|
||||||
|
mox.MustLoadConfig(true, checkACMEHosts)
|
||||||
|
|
||||||
|
// No need to potentially start and keep multiple processes. As root, we just need
|
||||||
|
// to start the child process.
|
||||||
|
runtime.GOMAXPROCS(1)
|
||||||
|
|
||||||
|
moxconf, err := filepath.Abs(mox.ConfigStaticPath)
|
||||||
|
log.Check(err, "finding absolute mox.conf path")
|
||||||
|
domainsconf, err := filepath.Abs(mox.ConfigDynamicPath)
|
||||||
|
log.Check(err, "finding absolute domains.conf path")
|
||||||
|
|
||||||
|
log.Print("starting as root, initializing network listeners", mlog.Field("version", moxvar.Version), mlog.Field("pid", os.Getpid()), mlog.Field("moxconf", moxconf), mlog.Field("domainsconf", domainsconf))
|
||||||
|
if os.Getenv("MOX_SOCKETS") != "" {
|
||||||
|
log.Fatal("refusing to start as root with $MOX_SOCKETS set")
|
||||||
|
}
|
||||||
|
if os.Getenv("MOX_FILES") != "" {
|
||||||
|
log.Fatal("refusing to start as root with $MOX_FILES set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mox.Conf.Static.NoFixPermissions {
|
||||||
|
// Fix permissions now that we have privilege to do so. Useful for update of v0.0.1
|
||||||
|
// that was running directly as mox-user.
|
||||||
|
workdir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Printx("get working dir, continuing without potentially fixing up permissions", err)
|
||||||
|
} else {
|
||||||
|
configdir := filepath.Dir(mox.ConfigStaticPath)
|
||||||
|
datadir := mox.DataDirPath(".")
|
||||||
|
err := fixperms(log, workdir, configdir, datadir, mox.Conf.Static.UID, mox.Conf.Static.GID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalx("fixing permissions", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mox.RestorePassedFiles()
|
||||||
|
mox.MustLoadConfig(true, checkACMEHosts)
|
||||||
|
log.Print("starting as unprivileged user", mlog.Field("user", mox.Conf.Static.User), mlog.Field("uid", mox.Conf.Static.UID), mlog.Field("gid", mox.Conf.Static.GID), mlog.Field("pid", os.Getpid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
syscall.Umask(syscall.Umask(007) | 007)
|
||||||
|
|
||||||
|
// Initialize key and random buffer for creating opaque SMTP
|
||||||
|
// transaction IDs based on "cid"s.
|
||||||
|
recvidpath := mox.DataDirPath("receivedid.key")
|
||||||
|
recvidbuf, err := os.ReadFile(recvidpath)
|
||||||
|
if err != nil || len(recvidbuf) != 16+8 {
|
||||||
|
recvidbuf = make([]byte, 16+8)
|
||||||
|
if _, err := cryptorand.Read(recvidbuf); err != nil {
|
||||||
|
log.Fatalx("reading random recvid data", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(recvidpath, recvidbuf, 0660); err != nil {
|
||||||
|
log.Fatalx("writing recvidpath", err, mlog.Field("path", recvidpath))
|
||||||
|
}
|
||||||
|
err := os.Chown(recvidpath, int(mox.Conf.Static.UID), 0)
|
||||||
|
log.Check(err, "chown receveidid.key", mlog.Field("path", recvidpath), mlog.Field("uid", mox.Conf.Static.UID), mlog.Field("gid", 0))
|
||||||
|
err = os.Chmod(recvidpath, 0640)
|
||||||
|
log.Check(err, "chmod receveidid.key to 0640", mlog.Field("path", recvidpath))
|
||||||
|
}
|
||||||
|
if err := mox.ReceivedIDInit(recvidbuf[:16], recvidbuf[16:]); err != nil {
|
||||||
|
log.Fatalx("init receivedid", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start mox. If running as root, this will bind/listen on network sockets, and
|
||||||
|
// fork and exec itself as unprivileged user, then waits for the child to stop and
|
||||||
|
// exit. When running as root, this function never returns. But the new
|
||||||
|
// unprivileged user will get here again, with network sockets prepared.
|
||||||
|
//
|
||||||
|
// We listen to the unix domain ctl socket afterwards, which we always remove
|
||||||
|
// before listening. We need to do that because we may not have cleaned up our
|
||||||
|
// control socket during unexpected shutdown. We don't want to remove and listen on
|
||||||
|
// the unix domain socket first. If we would, we would make the existing instance
|
||||||
|
// unreachable over its ctl socket, and then fail because the network addresses are
|
||||||
|
// taken.
|
||||||
|
const mtastsdbRefresher = true
|
||||||
|
const skipForkExec = false
|
||||||
|
if err := start(mtastsdbRefresher, skipForkExec); err != nil {
|
||||||
|
log.Fatalx("start", err)
|
||||||
|
}
|
||||||
|
log.Print("ready to serve")
|
||||||
|
|
||||||
|
if mox.Conf.Static.CheckUpdates {
|
||||||
|
checkUpdates := func() time.Duration {
|
||||||
|
next := 24 * time.Hour
|
||||||
|
current, lastknown, mtime, err := mox.LastKnown()
|
||||||
|
if err != nil {
|
||||||
|
log.Infox("determining own version before checking for updates, trying again in 24h", err)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to check for updates at every startup. So we sleep based on file
|
||||||
|
// mtime. But file won't exist initially.
|
||||||
|
if !mtime.IsZero() && time.Since(mtime) < 24*time.Hour {
|
||||||
|
d := 24*time.Hour - time.Since(mtime)
|
||||||
|
log.Debug("sleeping for next check for updates", mlog.Field("sleep", d))
|
||||||
|
time.Sleep(d)
|
||||||
|
next = 0
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
if err := os.Chtimes(mox.DataDirPath("lastknownversion"), now, now); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
log.Infox("setting mtime on lastknownversion file, continuing", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("checking for updates", mlog.Field("lastknown", lastknown))
|
||||||
|
updatesctx, updatescancel := context.WithTimeout(mox.Context, time.Minute)
|
||||||
|
latest, _, changelog, err := updates.Check(updatesctx, dns.StrictResolver{}, dns.Domain{ASCII: changelogDomain}, lastknown, changelogURL, changelogPubKey)
|
||||||
|
updatescancel()
|
||||||
|
if err != nil {
|
||||||
|
log.Infox("checking for updates", err, mlog.Field("latest", latest))
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
if !latest.After(lastknown) {
|
||||||
|
log.Debug("no new version available")
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
if len(changelog.Changes) == 0 {
|
||||||
|
log.Info("new version available, but changelog is empty, ignoring", mlog.Field("latest", latest))
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
var cl string
|
||||||
|
for _, c := range changelog.Changes {
|
||||||
|
cl += "----\n\n" + strings.TrimSpace(c.Text) + "\n\n"
|
||||||
|
}
|
||||||
|
cl += "----"
|
||||||
|
|
||||||
|
a, err := store.OpenAccount(mox.Conf.Static.Postmaster.Account)
|
||||||
|
if err != nil {
|
||||||
|
log.Infox("open account for postmaster changelog delivery", err)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := a.Close()
|
||||||
|
log.Check(err, "closing account")
|
||||||
|
}()
|
||||||
|
f, err := store.CreateMessageTemp("changelog")
|
||||||
|
if err != nil {
|
||||||
|
log.Infox("making temporary message file for changelog delivery", err)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
name := f.Name()
|
||||||
|
err = f.Close()
|
||||||
|
log.Check(err, "closing temp changelog file")
|
||||||
|
err := os.Remove(name)
|
||||||
|
log.Check(err, "removing temp changelog file", mlog.Field("path", name))
|
||||||
|
}()
|
||||||
|
m := &store.Message{
|
||||||
|
Received: time.Now(),
|
||||||
|
Flags: store.Flags{Flagged: true},
|
||||||
|
}
|
||||||
|
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"))
|
||||||
|
if err != nil {
|
||||||
|
log.Infox("writing temporary message file for changelog delivery", err)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
m.Size = int64(n)
|
||||||
|
if err := a.DeliverMailbox(log, mox.Conf.Static.Postmaster.Mailbox, m, f); err != nil {
|
||||||
|
log.Errorx("changelog delivery", err)
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
log.Info("delivered changelog", mlog.Field("current", current), mlog.Field("lastknown", lastknown), mlog.Field("latest", latest))
|
||||||
|
if err := mox.StoreLastKnown(latest); err != nil {
|
||||||
|
// This will be awkward, we'll keep notifying the postmaster once every 24h...
|
||||||
|
log.Infox("updating last known version", err)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
next := checkUpdates()
|
||||||
|
time.Sleep(next)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
go monitorDNSBL(log)
|
||||||
|
|
||||||
|
ctlpath := mox.DataDirPath("ctl")
|
||||||
|
_ = os.Remove(ctlpath)
|
||||||
|
ctl, err := net.Listen("unix", ctlpath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalx("listen on ctl unix domain socket", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := ctl.Accept()
|
||||||
|
if err != nil {
|
||||||
|
log.Printx("accept for ctl", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cid := mox.Cid()
|
||||||
|
ctx := context.WithValue(mox.Context, mlog.CidKey, cid)
|
||||||
|
go servectl(ctx, log.WithCid(cid), conn, func() { shutdown(log) })
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Remove old temporary files that somehow haven't been cleaned up.
|
||||||
|
tmpdir := mox.DataDirPath("tmp")
|
||||||
|
os.MkdirAll(tmpdir, 0770)
|
||||||
|
tmps, err := os.ReadDir(tmpdir)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorx("listing files in tmpdir", err)
|
||||||
|
} else {
|
||||||
|
now := time.Now()
|
||||||
|
for _, e := range tmps {
|
||||||
|
if fi, err := e.Info(); err != nil {
|
||||||
|
log.Errorx("stat tmp file", err, mlog.Field("filename", e.Name()))
|
||||||
|
} else if now.Sub(fi.ModTime()) > 7*24*time.Hour && !fi.IsDir() {
|
||||||
|
p := filepath.Join(tmpdir, e.Name())
|
||||||
|
if err := os.Remove(p); err != nil {
|
||||||
|
log.Errorx("removing stale temporary file", err, mlog.Field("path", p))
|
||||||
|
} else {
|
||||||
|
log.Info("removed stale temporary file", mlog.Field("path", p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown.
|
||||||
|
sigc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
|
||||||
|
sig := <-sigc
|
||||||
|
log.Print("shutting down, waiting max 3s for existing connections", mlog.Field("signal", sig))
|
||||||
|
shutdown(log)
|
||||||
|
if num, ok := sig.(syscall.Signal); ok {
|
||||||
|
os.Exit(int(num))
|
||||||
|
} else {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set correct permissions for mox working directory, binary, config and data and service file.
|
||||||
|
//
|
||||||
|
// We require being able to stat the basic non-optional paths. Then we'll try to
|
||||||
|
// fix up permissions. If an error occurs when fixing permissions, we log and
|
||||||
|
// continue (could not be an actual problem).
|
||||||
|
func fixperms(log *mlog.Log, workdir, configdir, datadir string, moxuid, moxgid uint32) (rerr error) {
|
||||||
|
type fserr struct{ Err error }
|
||||||
|
defer func() {
|
||||||
|
x := recover()
|
||||||
|
if x == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e, ok := x.(fserr)
|
||||||
|
if ok {
|
||||||
|
rerr = e.Err
|
||||||
|
} else {
|
||||||
|
panic(x)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
checkf := func(err error, format string, args ...any) {
|
||||||
|
if err != nil {
|
||||||
|
panic(fserr{fmt.Errorf(format, args...)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes we have to make. We collect them first, then apply.
|
||||||
|
type change struct {
|
||||||
|
path string
|
||||||
|
uid, gid *uint32
|
||||||
|
olduid, oldgid uint32
|
||||||
|
mode *fs.FileMode
|
||||||
|
oldmode fs.FileMode
|
||||||
|
}
|
||||||
|
var changes []change
|
||||||
|
|
||||||
|
ensure := func(p string, uid, gid uint32, perm fs.FileMode) bool {
|
||||||
|
fi, err := os.Stat(p)
|
||||||
|
checkf(err, "stat %s", p)
|
||||||
|
|
||||||
|
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok {
|
||||||
|
checkf(fmt.Errorf("got %T", st), "stat sys, expected syscall.Stat_t")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ch change
|
||||||
|
if st.Uid != uid || st.Gid != gid {
|
||||||
|
ch.uid = &uid
|
||||||
|
ch.gid = &gid
|
||||||
|
ch.olduid = st.Uid
|
||||||
|
ch.oldgid = st.Gid
|
||||||
|
}
|
||||||
|
if perm != fi.Mode()&(fs.ModeSetgid|0777) {
|
||||||
|
ch.mode = &perm
|
||||||
|
ch.oldmode = fi.Mode() & (fs.ModeSetgid | 0777)
|
||||||
|
}
|
||||||
|
var zerochange change
|
||||||
|
if ch == zerochange {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ch.path = p
|
||||||
|
changes = append(changes, ch)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
xexists := func(p string) bool {
|
||||||
|
_, err := os.Stat(p)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
checkf(err, "stat %s", p)
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We ensure these permissions:
|
||||||
|
//
|
||||||
|
// $workdir root:mox 0751
|
||||||
|
// $configdir mox:root 0750 + setgid, and recursively (but files 0640)
|
||||||
|
// $datadir mox:root 0750 + setgid, and recursively (but files 0640)
|
||||||
|
// $workdir/mox (binary, optional) root:mox 0750
|
||||||
|
// $workdir/mox.service (systemd service file, optional) root:root 0644
|
||||||
|
|
||||||
|
const root = 0
|
||||||
|
ensure(workdir, root, moxgid, 0751)
|
||||||
|
fixconfig := ensure(configdir, moxuid, 0, fs.ModeSetgid|0750)
|
||||||
|
fixdata := ensure(datadir, moxuid, 0, fs.ModeSetgid|0750)
|
||||||
|
|
||||||
|
// Binary and systemd service file do not exist (there) when running under docker.
|
||||||
|
binary := filepath.Join(workdir, "mox")
|
||||||
|
if xexists(binary) {
|
||||||
|
ensure(binary, root, moxgid, 0750)
|
||||||
|
}
|
||||||
|
svc := filepath.Join(workdir, "mox.service")
|
||||||
|
if xexists(svc) {
|
||||||
|
ensure(svc, root, root, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(changes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply changes.
|
||||||
|
log.Print("fixing up permissions, will continue on errors")
|
||||||
|
for _, ch := range changes {
|
||||||
|
if ch.uid != nil {
|
||||||
|
err := os.Chown(ch.path, int(*ch.uid), int(*ch.gid))
|
||||||
|
log.Printx("chown, fixing uid/gid", err, mlog.Field("path", ch.path), mlog.Field("olduid", ch.olduid), mlog.Field("oldgid", ch.oldgid), mlog.Field("newuid", *ch.uid), mlog.Field("newgid", *ch.gid))
|
||||||
|
}
|
||||||
|
if ch.mode != nil {
|
||||||
|
err := os.Chmod(ch.path, *ch.mode)
|
||||||
|
log.Printx("chmod, fixing permissions", err, mlog.Field("path", ch.path), mlog.Field("oldmode", fmt.Sprintf("%03o", ch.oldmode)), mlog.Field("newmode", fmt.Sprintf("%03o", *ch.mode)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walkchange := func(dir string) {
|
||||||
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
log.Printx("walk error, continuing", err, mlog.Field("path", path))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fi, err := d.Info()
|
||||||
|
if err != nil {
|
||||||
|
log.Printx("stat during walk, continuing", err, mlog.Field("path", path))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok {
|
||||||
|
log.Printx("syscall stat during walk, continuing", err, mlog.Field("path", path))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if st.Uid != moxuid || st.Gid != root {
|
||||||
|
err := os.Chown(path, int(moxuid), root)
|
||||||
|
log.Printx("walk chown, fixing uid/gid", err, mlog.Field("path", path), mlog.Field("olduid", st.Uid), mlog.Field("oldgid", st.Gid), mlog.Field("newuid", moxuid), mlog.Field("newgid", root))
|
||||||
|
}
|
||||||
|
omode := fi.Mode() & (fs.ModeSetgid | 0777)
|
||||||
|
var nmode fs.FileMode
|
||||||
|
if fi.IsDir() {
|
||||||
|
nmode = fs.ModeSetgid | 0750
|
||||||
|
} else {
|
||||||
|
nmode = 0640
|
||||||
|
}
|
||||||
|
if omode != nmode {
|
||||||
|
err := os.Chmod(path, nmode)
|
||||||
|
log.Printx("walk chmod, fixing permissions", err, mlog.Field("path", path), mlog.Field("oldmode", fmt.Sprintf("%03o", omode)), mlog.Field("newmode", fmt.Sprintf("%03o", nmode)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
log.Check(err, "walking dir to fix permissions", mlog.Field("dir", dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If config or data dir needed fixing, also set uid/gid and mode and files/dirs
|
||||||
|
// inside, recursively. We don't always recurse, data probably contains many files.
|
||||||
|
if fixconfig {
|
||||||
|
log.Print("fixing permissions in config dir", mlog.Field("configdir", configdir))
|
||||||
|
walkchange(configdir)
|
||||||
|
}
|
||||||
|
if fixdata {
|
||||||
|
log.Print("fixing permissions in data dir", mlog.Field("configdir", configdir))
|
||||||
|
walkchange(datadir)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
16
serve_windows.go
Normal file
16
serve_windows.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// also see localserve.go, code is similar or even shared.
|
||||||
|
func cmdServe(c *cmd) {
|
||||||
|
c.help = `Start mox, serving SMTP/IMAP/HTTPS. Not implemented on windows.
|
||||||
|
`
|
||||||
|
args := c.Parse()
|
||||||
|
if len(args) != 0 {
|
||||||
|
c.Usage()
|
||||||
|
}
|
||||||
|
log.Fatalln("mox serve not implemented on windows yet due to unfamiliarity with the windows security model, other commands including localserve do work")
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/mjl-/mox/dsn"
|
"github.com/mjl-/mox/dsn"
|
||||||
|
"github.com/mjl-/mox/mlog"
|
||||||
"github.com/mjl-/mox/queue"
|
"github.com/mjl-/mox/queue"
|
||||||
"github.com/mjl-/mox/smtp"
|
"github.com/mjl-/mox/smtp"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
|
@ -30,12 +31,11 @@ func queueDSN(ctx context.Context, c *conn, rcptTo smtp.Path, m dsn.Message) err
|
||||||
return fmt.Errorf("creating temp file: %w", err)
|
return fmt.Errorf("creating temp file: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
name := f.Name()
|
||||||
err := os.Remove(f.Name())
|
err = f.Close()
|
||||||
c.log.Check(err, "removing temporary dsn message file")
|
c.log.Check(err, "closing temporary dsn message file")
|
||||||
err = f.Close()
|
err := os.Remove(name)
|
||||||
c.log.Check(err, "closing temporary dsn message file")
|
c.log.Check(err, "removing temporary dsn message file", mlog.Field("path", name))
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
if _, err := f.Write([]byte(buf)); err != nil {
|
if _, err := f.Write([]byte(buf)); err != nil {
|
||||||
return fmt.Errorf("writing dsn file: %w", err)
|
return fmt.Errorf("writing dsn file: %w", err)
|
||||||
|
@ -46,11 +46,8 @@ func queueDSN(ctx context.Context, c *conn, rcptTo smtp.Path, m dsn.Message) err
|
||||||
// ../rfc/3464:433
|
// ../rfc/3464:433
|
||||||
const has8bit = false
|
const has8bit = false
|
||||||
const smtputf8 = false
|
const smtputf8 = false
|
||||||
if _, err := queue.Add(ctx, c.log, "", smtp.Path{}, rcptTo, has8bit, smtputf8, int64(len(buf)), m.MessageID, nil, f, bufUTF8, true); err != nil {
|
if _, err := queue.Add(ctx, c.log, "", smtp.Path{}, rcptTo, has8bit, smtputf8, int64(len(buf)), m.MessageID, nil, f, bufUTF8); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = f.Close()
|
|
||||||
c.log.Check(err, "closing dsn file")
|
|
||||||
f = nil
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ func FuzzServer(f *testing.F) {
|
||||||
f.Add("QUIT")
|
f.Add("QUIT")
|
||||||
|
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/smtpserverfuzz/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/smtpserverfuzz/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
dataDir := mox.ConfigDirPath(mox.Conf.Static.DataDir)
|
||||||
os.RemoveAll(dataDir)
|
os.RemoveAll(dataDir)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ func TestReputation(t *testing.T) {
|
||||||
check := func(m store.Message, history []store.Message, expJunk *bool, expConclusive bool, expMethod reputationMethod) {
|
check := func(m store.Message, history []store.Message, expJunk *bool, expConclusive bool, expMethod reputationMethod) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
p := "../testdata/smtpserver-reputation.db"
|
p := filepath.FromSlash("../testdata/smtpserver-reputation.db")
|
||||||
defer os.Remove(p)
|
defer os.Remove(p)
|
||||||
|
|
||||||
db, err := bstore.Open(ctxbg, p, &bstore.Options{Timeout: 5 * time.Second}, store.DBTypes...)
|
db, err := bstore.Open(ctxbg, p, &bstore.Options{Timeout: 5 * time.Second}, store.DBTypes...)
|
||||||
|
|
|
@ -1514,12 +1514,11 @@ func (c *conn) cmdData(p *parser) {
|
||||||
xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
|
xsmtpServerErrorf(errCodes(smtp.C451LocalErr, smtp.SeSys3Other0, err), "creating temporary file for message: %s", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if dataFile != nil {
|
name := dataFile.Name()
|
||||||
err := os.Remove(dataFile.Name())
|
err := dataFile.Close()
|
||||||
c.log.Check(err, "removing temporary message file", mlog.Field("path", dataFile.Name()))
|
c.log.Check(err, "removing temporary message file")
|
||||||
err = dataFile.Close()
|
err = os.Remove(name)
|
||||||
c.log.Check(err, "removing temporary message file")
|
c.log.Check(err, "removing temporary message file", mlog.Field("path", name))
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
msgWriter := message.NewWriter(dataFile)
|
msgWriter := message.NewWriter(dataFile)
|
||||||
dr := smtp.NewDataReader(c.r)
|
dr := smtp.NewDataReader(c.r)
|
||||||
|
@ -1660,18 +1659,16 @@ func (c *conn) cmdData(p *parser) {
|
||||||
// handle it first, and leave the rest of the function for handling wild west
|
// handle it first, and leave the rest of the function for handling wild west
|
||||||
// internet traffic.
|
// internet traffic.
|
||||||
if c.submission {
|
if c.submission {
|
||||||
c.submit(cmdctx, recvHdrFor, msgWriter, &dataFile)
|
c.submit(cmdctx, recvHdrFor, msgWriter, dataFile)
|
||||||
} else {
|
} else {
|
||||||
c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, &dataFile)
|
c.deliver(cmdctx, recvHdrFor, msgWriter, iprevStatus, iprevAuthentic, dataFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// submit is used for mail from authenticated users that we will try to deliver.
|
// submit is used for mail from authenticated users that we will try to deliver.
|
||||||
func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, pdataFile **os.File) {
|
func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, dataFile *os.File) {
|
||||||
// Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
|
// Similar between ../smtpserver/server.go:/submit\( and ../webmail/webmail.go:/MessageSubmit\(
|
||||||
|
|
||||||
dataFile := *pdataFile
|
|
||||||
|
|
||||||
var msgPrefix []byte
|
var msgPrefix []byte
|
||||||
|
|
||||||
// Check that user is only sending email as one of its configured identities. Not
|
// Check that user is only sending email as one of its configured identities. Not
|
||||||
|
@ -1774,7 +1771,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
// We always deliver through the queue. It would be more efficient to deliver
|
// We always deliver through the queue. It would be more efficient to deliver
|
||||||
// directly, but we don't want to circumvent all the anti-spam measures. Accounts
|
// directly, but we don't want to circumvent all the anti-spam measures. Accounts
|
||||||
// on a single mox instance should be allowed to block each other.
|
// on a single mox instance should be allowed to block each other.
|
||||||
for i, rcptAcc := range c.recipients {
|
for _, rcptAcc := range c.recipients {
|
||||||
if Localserve {
|
if Localserve {
|
||||||
code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
|
code, timeout := localserveNeedsError(rcptAcc.rcptTo.Localpart)
|
||||||
if timeout {
|
if timeout {
|
||||||
|
@ -1790,7 +1787,7 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
|
xmsgPrefix := append([]byte(recvHdrFor(rcptAcc.rcptTo.String())), msgPrefix...)
|
||||||
|
|
||||||
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
msgSize := int64(len(xmsgPrefix)) + msgWriter.Size
|
||||||
if _, err := queue.Add(ctx, c.log, c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, dataFile, nil, i == len(c.recipients)-1); err != nil {
|
if _, err := queue.Add(ctx, c.log, c.account.Name, *c.mailFrom, rcptAcc.rcptTo, msgWriter.Has8bit, c.smtputf8, msgSize, messageID, xmsgPrefix, dataFile, nil); err != nil {
|
||||||
// Aborting the transaction is not great. But continuing and generating DSNs will
|
// Aborting the transaction is not great. But continuing and generating DSNs will
|
||||||
// probably result in errors as well...
|
// probably result in errors as well...
|
||||||
metricSubmission.WithLabelValues("queueerror").Inc()
|
metricSubmission.WithLabelValues("queueerror").Inc()
|
||||||
|
@ -1804,10 +1801,6 @@ func (c *conn) submit(ctx context.Context, recvHdrFor func(string) string, msgWr
|
||||||
xcheckf(err, "adding outgoing message")
|
xcheckf(err, "adding outgoing message")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataFile.Close()
|
|
||||||
c.log.Check(err, "closing file after submission")
|
|
||||||
*pdataFile = nil
|
|
||||||
|
|
||||||
c.transactionGood++
|
c.transactionGood++
|
||||||
c.transactionBad-- // Compensate for early earlier pessimistic increase.
|
c.transactionBad-- // Compensate for early earlier pessimistic increase.
|
||||||
|
|
||||||
|
@ -1866,9 +1859,7 @@ func (c *conn) xlocalserveError(lp smtp.Localpart) {
|
||||||
|
|
||||||
// deliver is called for incoming messages from external, typically untrusted
|
// deliver is called for incoming messages from external, typically untrusted
|
||||||
// sources. i.e. not submitted by authenticated users.
|
// sources. i.e. not submitted by authenticated users.
|
||||||
func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, pdataFile **os.File) {
|
func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgWriter *message.Writer, iprevStatus iprev.Status, iprevAuthentic bool, dataFile *os.File) {
|
||||||
dataFile := *pdataFile
|
|
||||||
|
|
||||||
// todo: in decision making process, if we run into (some) temporary errors, attempt to continue. if we decide to accept, all good. if we decide to reject, we'll make it a temporary reject.
|
// todo: in decision making process, if we run into (some) temporary errors, attempt to continue. if we decide to accept, all good. if we decide to reject, we'll make it a temporary reject.
|
||||||
|
|
||||||
msgFrom, headers, err := message.From(c.log, false, dataFile)
|
msgFrom, headers, err := message.From(c.log, false, dataFile)
|
||||||
|
@ -2381,7 +2372,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorx("tidying rejects mailbox", err)
|
log.Errorx("tidying rejects mailbox", err)
|
||||||
} else if hasSpace {
|
} else if hasSpace {
|
||||||
if err := acc.DeliverMailbox(log, conf.RejectsMailbox, m, dataFile, false); err != nil {
|
if err := acc.DeliverMailbox(log, conf.RejectsMailbox, m, dataFile); err != nil {
|
||||||
log.Errorx("delivering spammy mail to rejects mailbox", err)
|
log.Errorx("delivering spammy mail to rejects mailbox", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("delivered spammy mail to rejects mailbox")
|
log.Info("delivered spammy mail to rejects mailbox")
|
||||||
|
@ -2456,7 +2447,7 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acc.WithWLock(func() {
|
acc.WithWLock(func() {
|
||||||
if err := acc.DeliverMailbox(log, a.mailbox, m, dataFile, false); err != nil {
|
if err := acc.DeliverMailbox(log, a.mailbox, m, dataFile); err != nil {
|
||||||
log.Errorx("delivering", err)
|
log.Errorx("delivering", err)
|
||||||
metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
|
metricDelivery.WithLabelValues("delivererror", a.reason).Inc()
|
||||||
addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
addError(rcptAcc, smtp.C451LocalErr, smtp.SeSys3Other0, false, "error processing")
|
||||||
|
@ -2562,12 +2553,6 @@ func (c *conn) deliver(ctx context.Context, recvHdrFor func(string) string, msgW
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Remove(dataFile.Name())
|
|
||||||
c.log.Check(err, "removing file after delivery")
|
|
||||||
err = dataFile.Close()
|
|
||||||
c.log.Check(err, "closing data file after delivery")
|
|
||||||
*pdataFile = nil
|
|
||||||
|
|
||||||
c.transactionGood++
|
c.transactionGood++
|
||||||
c.transactionBad-- // Compensate for early earlier pessimistic increase.
|
c.transactionBad-- // Compensate for early earlier pessimistic increase.
|
||||||
c.rset()
|
c.rset()
|
||||||
|
|
|
@ -193,7 +193,7 @@ func fakeCert(t *testing.T) tls.Certificate {
|
||||||
|
|
||||||
// Test submission from authenticated user.
|
// Test submission from authenticated user.
|
||||||
func TestSubmission(t *testing.T) {
|
func TestSubmission(t *testing.T) {
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", dns.MockResolver{})
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
// Set DKIM signing config.
|
// Set DKIM signing config.
|
||||||
|
@ -254,7 +254,7 @@ func TestDelivery(t *testing.T) {
|
||||||
},
|
},
|
||||||
PTR: map[string][]string{},
|
PTR: map[string][]string{},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
ts.run(func(err error, client *smtpclient.Client) {
|
ts.run(func(err error, client *smtpclient.Client) {
|
||||||
|
@ -333,9 +333,11 @@ func TestDelivery(t *testing.T) {
|
||||||
func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
|
func tinsertmsg(t *testing.T, acc *store.Account, mailbox string, m *store.Message, msg string) {
|
||||||
mf, err := store.CreateMessageTemp("queue-dsn")
|
mf, err := store.CreateMessageTemp("queue-dsn")
|
||||||
tcheck(t, err, "temp message")
|
tcheck(t, err, "temp message")
|
||||||
|
defer os.Remove(mf.Name())
|
||||||
|
defer mf.Close()
|
||||||
_, err = mf.Write([]byte(msg))
|
_, err = mf.Write([]byte(msg))
|
||||||
tcheck(t, err, "write message")
|
tcheck(t, err, "write message")
|
||||||
err = acc.DeliverMailbox(xlog, mailbox, m, mf, true)
|
err = acc.DeliverMailbox(xlog, mailbox, m, mf)
|
||||||
tcheck(t, err, "deliver message")
|
tcheck(t, err, "deliver message")
|
||||||
err = mf.Close()
|
err = mf.Close()
|
||||||
tcheck(t, err, "close message")
|
tcheck(t, err, "close message")
|
||||||
|
@ -392,7 +394,7 @@ func TestSpam(t *testing.T) {
|
||||||
"_dmarc.example.org.": {"v=DMARC1;p=reject"},
|
"_dmarc.example.org.": {"v=DMARC1;p=reject"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/junk/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
// Insert spammy messages. No junkfilter training yet.
|
// Insert spammy messages. No junkfilter training yet.
|
||||||
|
@ -538,7 +540,7 @@ func TestForward(t *testing.T) {
|
||||||
rcptTo = "mjl@mox.example" // Without IsForward rule.
|
rcptTo = "mjl@mox.example" // Without IsForward rule.
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := newTestServer(t, "../testdata/smtp/junk/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
|
var msgBad = strings.ReplaceAll(`From: <remote@bad.example>
|
||||||
|
@ -645,7 +647,7 @@ func TestDMARCSent(t *testing.T) {
|
||||||
"_dmarc.example.org.": {"v=DMARC1;p=reject"},
|
"_dmarc.example.org.": {"v=DMARC1;p=reject"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/junk/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/junk/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
// Insert spammy messages not related to the test message.
|
// Insert spammy messages not related to the test message.
|
||||||
|
@ -710,7 +712,7 @@ func TestBlocklistedSubjectpass(t *testing.T) {
|
||||||
"127.0.0.10": {"example.org."}, // For iprev check.
|
"127.0.0.10": {"example.org."}, // For iprev check.
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
|
ts.dnsbls = []dns.Domain{{ASCII: "dnsbl.example"}}
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
|
@ -776,7 +778,7 @@ func TestDMARCReport(t *testing.T) {
|
||||||
"127.0.0.10": {"example.org."}, // For iprev check.
|
"127.0.0.10": {"example.org."}, // For iprev check.
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/dmarcreport/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
run := func(report string, n int) {
|
run := func(report string, n int) {
|
||||||
|
@ -899,7 +901,7 @@ func TestTLSReport(t *testing.T) {
|
||||||
"127.0.0.10": {"example.org."}, // For iprev check.
|
"127.0.0.10": {"example.org."}, // For iprev check.
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/tlsrpt/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/tlsrpt/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
run := func(tlsrpt string, n int) {
|
run := func(tlsrpt string, n int) {
|
||||||
|
@ -939,7 +941,7 @@ func TestTLSReport(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRatelimitConnectionrate(t *testing.T) {
|
func TestRatelimitConnectionrate(t *testing.T) {
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", dns.MockResolver{})
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
// We'll be creating 300 connections, no TLS and reduce noise.
|
// We'll be creating 300 connections, no TLS and reduce noise.
|
||||||
|
@ -965,7 +967,7 @@ func TestRatelimitConnectionrate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRatelimitAuth(t *testing.T) {
|
func TestRatelimitAuth(t *testing.T) {
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", dns.MockResolver{})
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
ts.submission = true
|
ts.submission = true
|
||||||
|
@ -1006,7 +1008,7 @@ func TestRatelimitDelivery(t *testing.T) {
|
||||||
"127.0.0.10": {"example.org."},
|
"127.0.0.10": {"example.org."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
orig := limitIPMasked1MessagesPerMinute
|
orig := limitIPMasked1MessagesPerMinute
|
||||||
|
@ -1061,7 +1063,7 @@ func TestRatelimitDelivery(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNonSMTP(t *testing.T) {
|
func TestNonSMTP(t *testing.T) {
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", dns.MockResolver{})
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), dns.MockResolver{})
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
ts.cid += 2
|
ts.cid += 2
|
||||||
|
|
||||||
|
@ -1105,7 +1107,7 @@ func TestNonSMTP(t *testing.T) {
|
||||||
|
|
||||||
// Test limits on outgoing messages.
|
// Test limits on outgoing messages.
|
||||||
func TestLimitOutgoing(t *testing.T) {
|
func TestLimitOutgoing(t *testing.T) {
|
||||||
ts := newTestServer(t, "../testdata/smtp/sendlimit/mox.conf", dns.MockResolver{})
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/sendlimit/mox.conf"), dns.MockResolver{})
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
ts.user = "mjl@mox.example"
|
ts.user = "mjl@mox.example"
|
||||||
|
@ -1149,7 +1151,7 @@ func TestCatchall(t *testing.T) {
|
||||||
"127.0.0.10": {"other.example."},
|
"127.0.0.10": {"other.example."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/catchall/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/catchall/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
|
testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
|
||||||
|
@ -1195,7 +1197,7 @@ func TestDKIMSign(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
// Set DKIM signing config.
|
// Set DKIM signing config.
|
||||||
|
@ -1287,7 +1289,7 @@ func TestPostmaster(t *testing.T) {
|
||||||
"127.0.0.10": {"other.example."},
|
"127.0.0.10": {"other.example."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/postmaster/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/postmaster/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
|
testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
|
||||||
|
@ -1321,7 +1323,7 @@ func TestEmptylocalpart(t *testing.T) {
|
||||||
"127.0.0.10": {"other.example."},
|
"127.0.0.10": {"other.example."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ts := newTestServer(t, "../testdata/smtp/mox.conf", resolver)
|
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
|
||||||
defer ts.close()
|
defer ts.close()
|
||||||
|
|
||||||
testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
|
testDeliver := func(rcptTo string, expErr *smtpclient.Error) {
|
||||||
|
|
|
@ -1189,9 +1189,6 @@ func (a *Account) WithRLock(fn func()) {
|
||||||
|
|
||||||
// DeliverMessage delivers a mail message to the account.
|
// DeliverMessage delivers a mail message to the account.
|
||||||
//
|
//
|
||||||
// If consumeFile is set, the original msgFile is moved/renamed or copied and
|
|
||||||
// removed as part of delivery.
|
|
||||||
//
|
|
||||||
// The message, with msg.MsgPrefix and msgFile combined, must have a header
|
// The message, with msg.MsgPrefix and msgFile combined, must have a header
|
||||||
// section. The caller is responsible for adding a header separator to
|
// section. The caller is responsible for adding a header separator to
|
||||||
// msg.MsgPrefix if missing from an incoming message.
|
// msg.MsgPrefix if missing from an incoming message.
|
||||||
|
@ -1210,7 +1207,7 @@ func (a *Account) WithRLock(fn func()) {
|
||||||
// Caller must broadcast new message.
|
// Caller must broadcast new message.
|
||||||
//
|
//
|
||||||
// Caller must update mailbox counts.
|
// Caller must update mailbox counts.
|
||||||
func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, consumeFile, sync, notrain, nothreads bool) error {
|
func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, sync, notrain, nothreads bool) error {
|
||||||
if m.Expunged {
|
if m.Expunged {
|
||||||
return fmt.Errorf("cannot deliver expunged message")
|
return fmt.Errorf("cannot deliver expunged message")
|
||||||
}
|
}
|
||||||
|
@ -1346,12 +1343,7 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if consumeFile {
|
if err := moxio.LinkOrCopy(log, msgPath, msgFile.Name(), &moxio.AtReader{R: msgFile}, true); err != nil {
|
||||||
if err := os.Rename(msgFile.Name(), msgPath); err != nil {
|
|
||||||
// Could be due to cross-filesystem rename. Users shouldn't configure their systems that way.
|
|
||||||
return fmt.Errorf("moving msg file to destination directory: %w", err)
|
|
||||||
}
|
|
||||||
} else if err := moxio.LinkOrCopy(log, msgPath, msgFile.Name(), &moxio.AtReader{R: msgFile}, true); err != nil {
|
|
||||||
return fmt.Errorf("linking/copying message to new file: %w", err)
|
return fmt.Errorf("linking/copying message to new file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1647,7 +1639,7 @@ ruleset:
|
||||||
|
|
||||||
// MessagePath returns the file system path of a message.
|
// MessagePath returns the file system path of a message.
|
||||||
func (a *Account) MessagePath(messageID int64) string {
|
func (a *Account) MessagePath(messageID int64) string {
|
||||||
return strings.Join(append([]string{a.Dir, "msg"}, messagePathElems(messageID)...), "/")
|
return strings.Join(append([]string{a.Dir, "msg"}, messagePathElems(messageID)...), string(filepath.Separator))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageReader opens a message for reading, transparently combining the
|
// MessageReader opens a message for reading, transparently combining the
|
||||||
|
@ -1661,7 +1653,7 @@ func (a *Account) MessageReader(m Message) *MsgReader {
|
||||||
// Caller must hold account wlock (mailbox may be created).
|
// Caller must hold account wlock (mailbox may be created).
|
||||||
// Message delivery, possible mailbox creation, and updated mailbox counts are
|
// Message delivery, possible mailbox creation, and updated mailbox counts are
|
||||||
// broadcasted.
|
// broadcasted.
|
||||||
func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *Message, msgFile *os.File, consumeFile bool) error {
|
func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *Message, msgFile *os.File) error {
|
||||||
var mailbox string
|
var mailbox string
|
||||||
rs := MessageRuleset(log, dest, m, m.MsgPrefix, msgFile)
|
rs := MessageRuleset(log, dest, m, m.MsgPrefix, msgFile)
|
||||||
if rs != nil {
|
if rs != nil {
|
||||||
|
@ -1671,7 +1663,7 @@ func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *
|
||||||
} else {
|
} else {
|
||||||
mailbox = dest.Mailbox
|
mailbox = dest.Mailbox
|
||||||
}
|
}
|
||||||
return a.DeliverMailbox(log, mailbox, m, msgFile, consumeFile)
|
return a.DeliverMailbox(log, mailbox, m, msgFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeliverMailbox delivers an email to the specified mailbox.
|
// DeliverMailbox delivers an email to the specified mailbox.
|
||||||
|
@ -1679,7 +1671,7 @@ func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *
|
||||||
// Caller must hold account wlock (mailbox may be created).
|
// Caller must hold account wlock (mailbox may be created).
|
||||||
// Message delivery, possible mailbox creation, and updated mailbox counts are
|
// Message delivery, possible mailbox creation, and updated mailbox counts are
|
||||||
// broadcasted.
|
// broadcasted.
|
||||||
func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgFile *os.File, consumeFile bool) error {
|
func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgFile *os.File) error {
|
||||||
var changes []Change
|
var changes []Change
|
||||||
err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
|
err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
|
||||||
mb, chl, err := a.MailboxEnsure(tx, mailbox, true)
|
mb, chl, err := a.MailboxEnsure(tx, mailbox, true)
|
||||||
|
@ -1696,7 +1688,7 @@ func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgF
|
||||||
return fmt.Errorf("updating mailbox for delivery: %w", err)
|
return fmt.Errorf("updating mailbox for delivery: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.DeliverMessage(log, tx, m, msgFile, consumeFile, true, false, false); err != nil {
|
if err := a.DeliverMessage(log, tx, m, msgFile, true, false, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1977,10 +1969,11 @@ func OpenEmail(email string) (*Account, config.Destination, error) {
|
||||||
// 64 characters, must be power of 2 for MessagePath
|
// 64 characters, must be power of 2 for MessagePath
|
||||||
const msgDirChars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
const msgDirChars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
|
||||||
|
|
||||||
// MessagePath returns the filename of the on-disk filename, relative to the containing directory such as <account>/msg or queue.
|
// MessagePath returns the filename of the on-disk filename, relative to the
|
||||||
|
// containing directory such as <account>/msg or queue.
|
||||||
// Returns names like "AB/1".
|
// Returns names like "AB/1".
|
||||||
func MessagePath(messageID int64) string {
|
func MessagePath(messageID int64) string {
|
||||||
return strings.Join(messagePathElems(messageID), "/")
|
return strings.Join(messagePathElems(messageID), string(filepath.Separator))
|
||||||
}
|
}
|
||||||
|
|
||||||
// messagePathElems returns the elems, for a single join without intermediate
|
// messagePathElems returns the elems, for a single join without intermediate
|
||||||
|
|
|
@ -3,6 +3,7 @@ package store
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -28,7 +29,7 @@ func tcheck(t *testing.T, err error, msg string) {
|
||||||
|
|
||||||
func TestMailbox(t *testing.T) {
|
func TestMailbox(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/store/data")
|
os.RemoveAll("../testdata/store/data")
|
||||||
mox.ConfigStaticPath = "../testdata/store/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
acc, err := OpenAccount("mjl")
|
acc, err := OpenAccount("mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
|
@ -44,6 +45,7 @@ func TestMailbox(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creating temp msg file: %s", err)
|
t.Fatalf("creating temp msg file: %s", err)
|
||||||
}
|
}
|
||||||
|
defer os.Remove(msgFile.Name())
|
||||||
defer msgFile.Close()
|
defer msgFile.Close()
|
||||||
msgWriter := message.NewWriter(msgFile)
|
msgWriter := message.NewWriter(msgFile)
|
||||||
if _, err := msgWriter.Write([]byte(" message")); err != nil {
|
if _, err := msgWriter.Write([]byte(" message")); err != nil {
|
||||||
|
@ -70,7 +72,7 @@ func TestMailbox(t *testing.T) {
|
||||||
}
|
}
|
||||||
acc.WithWLock(func() {
|
acc.WithWLock(func() {
|
||||||
conf, _ := acc.Conf()
|
conf, _ := acc.Conf()
|
||||||
err := acc.DeliverDestination(xlog, conf.Destinations["mjl"], &m, msgFile, false)
|
err := acc.DeliverDestination(xlog, conf.Destinations["mjl"], &m, msgFile)
|
||||||
tcheck(t, err, "deliver without consume")
|
tcheck(t, err, "deliver without consume")
|
||||||
|
|
||||||
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
||||||
|
@ -79,7 +81,7 @@ func TestMailbox(t *testing.T) {
|
||||||
tcheck(t, err, "sent mailbox")
|
tcheck(t, err, "sent mailbox")
|
||||||
msent.MailboxID = mbsent.ID
|
msent.MailboxID = mbsent.ID
|
||||||
msent.MailboxOrigID = mbsent.ID
|
msent.MailboxOrigID = mbsent.ID
|
||||||
err = acc.DeliverMessage(xlog, tx, &msent, msgFile, false, true, false, false)
|
err = acc.DeliverMessage(xlog, tx, &msent, msgFile, true, false, false)
|
||||||
tcheck(t, err, "deliver message")
|
tcheck(t, err, "deliver message")
|
||||||
if !msent.ThreadMuted || !msent.ThreadCollapsed {
|
if !msent.ThreadMuted || !msent.ThreadCollapsed {
|
||||||
t.Fatalf("thread muted & collapsed should have been copied from parent (duplicate message-id) m")
|
t.Fatalf("thread muted & collapsed should have been copied from parent (duplicate message-id) m")
|
||||||
|
@ -95,7 +97,7 @@ func TestMailbox(t *testing.T) {
|
||||||
tcheck(t, err, "insert rejects mailbox")
|
tcheck(t, err, "insert rejects mailbox")
|
||||||
mreject.MailboxID = mbrejects.ID
|
mreject.MailboxID = mbrejects.ID
|
||||||
mreject.MailboxOrigID = mbrejects.ID
|
mreject.MailboxOrigID = mbrejects.ID
|
||||||
err = acc.DeliverMessage(xlog, tx, &mreject, msgFile, false, true, false, false)
|
err = acc.DeliverMessage(xlog, tx, &mreject, msgFile, true, false, false)
|
||||||
tcheck(t, err, "deliver message")
|
tcheck(t, err, "deliver message")
|
||||||
|
|
||||||
err = tx.Get(&mbrejects)
|
err = tx.Get(&mbrejects)
|
||||||
|
@ -108,7 +110,7 @@ func TestMailbox(t *testing.T) {
|
||||||
})
|
})
|
||||||
tcheck(t, err, "deliver as sent and rejects")
|
tcheck(t, err, "deliver as sent and rejects")
|
||||||
|
|
||||||
err = acc.DeliverDestination(xlog, conf.Destinations["mjl"], &mconsumed, msgFile, true)
|
err = acc.DeliverDestination(xlog, conf.Destinations["mjl"], &mconsumed, msgFile)
|
||||||
tcheck(t, err, "deliver with consume")
|
tcheck(t, err, "deliver with consume")
|
||||||
|
|
||||||
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
|
||||||
|
@ -251,9 +253,11 @@ func TestMailbox(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMessageRuleset(t *testing.T) {
|
func TestMessageRuleset(t *testing.T) {
|
||||||
f, err := os.Open("/dev/null")
|
f, err := CreateMessageTemp("msgruleset")
|
||||||
tcheck(t, err, "open")
|
tcheck(t, err, "creating temp msg file")
|
||||||
|
defer os.Remove(f.Name())
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
msgBuf := []byte(strings.ReplaceAll(`List-ID: <test.mox.example>
|
msgBuf := []byte(strings.ReplaceAll(`List-ID: <test.mox.example>
|
||||||
|
|
||||||
test
|
test
|
||||||
|
|
|
@ -82,10 +82,11 @@ type DirArchiver struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create create name in the file system, in dir.
|
// Create create name in the file system, in dir.
|
||||||
|
// name must always use forwarded slashes.
|
||||||
func (a DirArchiver) Create(name string, size int64, mtime time.Time) (io.WriteCloser, error) {
|
func (a DirArchiver) Create(name string, size int64, mtime time.Time) (io.WriteCloser, error) {
|
||||||
isdir := strings.HasSuffix(name, "/")
|
isdir := strings.HasSuffix(name, "/")
|
||||||
name = strings.TrimSuffix(name, "/")
|
name = strings.TrimSuffix(name, "/")
|
||||||
p := filepath.Join(a.Dir, name)
|
p := filepath.Join(a.Dir, filepath.FromSlash(name))
|
||||||
os.MkdirAll(filepath.Dir(p), 0770)
|
os.MkdirAll(filepath.Dir(p), 0770)
|
||||||
if isdir {
|
if isdir {
|
||||||
return nil, os.Mkdir(p, 0770)
|
return nil, os.Mkdir(p, 0770)
|
||||||
|
@ -213,8 +214,11 @@ func ExportMessages(ctx context.Context, log *mlog.Log, db *bstore.DB, accountDi
|
||||||
var mboxwriter *bufio.Writer
|
var mboxwriter *bufio.Writer
|
||||||
defer func() {
|
defer func() {
|
||||||
if mboxtmp != nil {
|
if mboxtmp != nil {
|
||||||
|
name := mboxtmp.Name()
|
||||||
err := mboxtmp.Close()
|
err := mboxtmp.Close()
|
||||||
log.Check(err, "closing mbox temp file")
|
log.Check(err, "closing mbox temp file")
|
||||||
|
err = os.Remove(name)
|
||||||
|
log.Check(err, "removing mbox temp file", mlog.Field("name", name))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -287,8 +291,11 @@ func ExportMessages(ctx context.Context, log *mlog.Log, db *bstore.DB, accountDi
|
||||||
if err := w.Close(); err != nil {
|
if err := w.Close(); err != nil {
|
||||||
return fmt.Errorf("closing message file: %v", err)
|
return fmt.Errorf("closing message file: %v", err)
|
||||||
}
|
}
|
||||||
|
name := mboxtmp.Name()
|
||||||
err = mboxtmp.Close()
|
err = mboxtmp.Close()
|
||||||
log.Check(err, "closing temporary mbox file")
|
log.Check(err, "closing temporary mbox file")
|
||||||
|
err = os.Remove(name)
|
||||||
|
log.Check(err, "removing temporary mbox file", mlog.Field("path", name))
|
||||||
mboxwriter = nil
|
mboxwriter = nil
|
||||||
mboxtmp = nil
|
mboxtmp = nil
|
||||||
return nil
|
return nil
|
||||||
|
@ -524,10 +531,6 @@ func ExportMessages(ctx context.Context, log *mlog.Log, db *bstore.DB, accountDi
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating temp mbox file: %v", err)
|
return fmt.Errorf("creating temp mbox file: %v", err)
|
||||||
}
|
}
|
||||||
// Remove file immediately, so we are sure we don't leave it around.
|
|
||||||
if err := os.Remove(mboxtmp.Name()); err != nil {
|
|
||||||
return fmt.Errorf("removing temp file just created: %v", err)
|
|
||||||
}
|
|
||||||
mboxwriter = bufio.NewWriter(mboxtmp)
|
mboxwriter = bufio.NewWriter(mboxtmp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ func TestExport(t *testing.T) {
|
||||||
// and maildir/mbox. check there are 2 files in the repo, no errors.txt.
|
// and maildir/mbox. check there are 2 files in the repo, no errors.txt.
|
||||||
|
|
||||||
os.RemoveAll("../testdata/store/data")
|
os.RemoveAll("../testdata/store/data")
|
||||||
mox.ConfigStaticPath = "../testdata/store/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
acc, err := OpenAccount("mjl")
|
acc, err := OpenAccount("mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
|
@ -32,16 +32,17 @@ func TestExport(t *testing.T) {
|
||||||
msgFile, err := CreateMessageTemp("mox-test-export")
|
msgFile, err := CreateMessageTemp("mox-test-export")
|
||||||
tcheck(t, err, "create temp")
|
tcheck(t, err, "create temp")
|
||||||
defer os.Remove(msgFile.Name()) // To be sure.
|
defer os.Remove(msgFile.Name()) // To be sure.
|
||||||
|
defer msgFile.Close()
|
||||||
const msg = "test: test\r\n\r\ntest\r\n"
|
const msg = "test: test\r\n\r\ntest\r\n"
|
||||||
_, err = msgFile.Write([]byte(msg))
|
_, err = msgFile.Write([]byte(msg))
|
||||||
tcheck(t, err, "write message")
|
tcheck(t, err, "write message")
|
||||||
|
|
||||||
m := Message{Received: time.Now(), Size: int64(len(msg))}
|
m := Message{Received: time.Now(), Size: int64(len(msg))}
|
||||||
err = acc.DeliverMailbox(xlog, "Inbox", &m, msgFile, false)
|
err = acc.DeliverMailbox(xlog, "Inbox", &m, msgFile)
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
m = Message{Received: time.Now(), Size: int64(len(msg))}
|
m = Message{Received: time.Now(), Size: int64(len(msg))}
|
||||||
err = acc.DeliverMailbox(xlog, "Trash", &m, msgFile, true)
|
err = acc.DeliverMailbox(xlog, "Trash", &m, msgFile)
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
|
|
||||||
var maildirZip, maildirTar, mboxZip, mboxTar bytes.Buffer
|
var maildirZip, maildirTar, mboxZip, mboxTar bytes.Buffer
|
||||||
|
@ -61,8 +62,8 @@ func TestExport(t *testing.T) {
|
||||||
archive(ZipArchiver{zip.NewWriter(&mboxZip)}, false)
|
archive(ZipArchiver{zip.NewWriter(&mboxZip)}, false)
|
||||||
archive(TarArchiver{tar.NewWriter(&maildirTar)}, true)
|
archive(TarArchiver{tar.NewWriter(&maildirTar)}, true)
|
||||||
archive(TarArchiver{tar.NewWriter(&mboxTar)}, false)
|
archive(TarArchiver{tar.NewWriter(&mboxTar)}, false)
|
||||||
archive(DirArchiver{"../testdata/exportmaildir"}, true)
|
archive(DirArchiver{filepath.FromSlash("../testdata/exportmaildir")}, true)
|
||||||
archive(DirArchiver{"../testdata/exportmbox"}, false)
|
archive(DirArchiver{filepath.FromSlash("../testdata/exportmbox")}, false)
|
||||||
|
|
||||||
if r, err := zip.NewReader(bytes.NewReader(maildirZip.Bytes()), int64(maildirZip.Len())); err != nil {
|
if r, err := zip.NewReader(bytes.NewReader(maildirZip.Bytes()), int64(maildirZip.Len())); err != nil {
|
||||||
t.Fatalf("reading maildir zip: %v", err)
|
t.Fatalf("reading maildir zip: %v", err)
|
||||||
|
@ -115,6 +116,6 @@ func TestExport(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDirFiles("../testdata/exportmaildir", 2)
|
checkDirFiles(filepath.FromSlash("../testdata/exportmaildir"), 2)
|
||||||
checkDirFiles("../testdata/exportmbox", 2)
|
checkDirFiles(filepath.FromSlash("../testdata/exportmbox"), 2)
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,10 +84,11 @@ func (mr *MboxReader) Next() (*Message, *os.File, string, error) {
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
err := os.Remove(f.Name())
|
name := f.Name()
|
||||||
mr.log.Check(err, "removing temporary message file after mbox read error", mlog.Field("path", f.Name()))
|
err := f.Close()
|
||||||
err = f.Close()
|
|
||||||
mr.log.Check(err, "closing temporary message file after mbox read error")
|
mr.log.Check(err, "closing temporary message file after mbox read error")
|
||||||
|
err = os.Remove(name)
|
||||||
|
mr.log.Check(err, "removing temporary message file after mbox read error", mlog.Field("path", name))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -272,10 +273,11 @@ func (mr *MaildirReader) Next() (*Message, *os.File, string, error) {
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
err := os.Remove(f.Name())
|
name := f.Name()
|
||||||
mr.log.Check(err, "removing temporary message file after maildir read error", mlog.Field("path", f.Name()))
|
err := f.Close()
|
||||||
err = f.Close()
|
|
||||||
mr.log.Check(err, "closing temporary message file after maildir read error")
|
mr.log.Check(err, "closing temporary message file after maildir read error")
|
||||||
|
err = os.Remove(name)
|
||||||
|
mr.log.Check(err, "removing temporary message file after maildir read error", mlog.Field("path", name))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -24,15 +24,15 @@ func TestMboxReader(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("next mbox message: %v", err)
|
t.Fatalf("next mbox message: %v", err)
|
||||||
}
|
}
|
||||||
defer mf0.Close()
|
|
||||||
defer os.Remove(mf0.Name())
|
defer os.Remove(mf0.Name())
|
||||||
|
defer mf0.Close()
|
||||||
|
|
||||||
_, mf1, _, err := mr.Next()
|
_, mf1, _, err := mr.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("next mbox message: %v", err)
|
t.Fatalf("next mbox message: %v", err)
|
||||||
}
|
}
|
||||||
defer mf1.Close()
|
|
||||||
defer os.Remove(mf1.Name())
|
defer os.Remove(mf1.Name())
|
||||||
|
defer mf1.Close()
|
||||||
|
|
||||||
_, _, _, err = mr.Next()
|
_, _, _, err = mr.Next()
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
|
@ -62,15 +62,15 @@ func TestMaildirReader(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("next maildir message: %v", err)
|
t.Fatalf("next maildir message: %v", err)
|
||||||
}
|
}
|
||||||
defer mf0.Close()
|
|
||||||
defer os.Remove(mf0.Name())
|
defer os.Remove(mf0.Name())
|
||||||
|
defer mf0.Close()
|
||||||
|
|
||||||
_, mf1, _, err := mr.Next()
|
_, mf1, _, err := mr.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("next maildir message: %v", err)
|
t.Fatalf("next maildir message: %v", err)
|
||||||
}
|
}
|
||||||
defer mf1.Close()
|
|
||||||
defer os.Remove(mf1.Name())
|
defer os.Remove(mf1.Name())
|
||||||
|
defer mf1.Close()
|
||||||
|
|
||||||
_, _, _, err = mr.Next()
|
_, _, _, err = mr.Next()
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
|
|
|
@ -12,7 +12,13 @@ func TestMsgreader(t *testing.T) {
|
||||||
t.Fatalf("expected error for non-existing file, got %s", err)
|
t.Fatalf("expected error for non-existing file, got %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if buf, err := io.ReadAll(&MsgReader{prefix: []byte("hello"), path: "/dev/null", size: int64(len("hello"))}); err != nil {
|
if err := os.WriteFile("emptyfile_test.txt", []byte{}, 0660); err != nil {
|
||||||
|
t.Fatalf("writing emptyfile_test.txt: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove("emptyfile_test.txt")
|
||||||
|
mr := &MsgReader{prefix: []byte("hello"), path: "emptyfile_test.txt", size: int64(len("hello"))}
|
||||||
|
defer mr.Close()
|
||||||
|
if buf, err := io.ReadAll(mr); err != nil {
|
||||||
t.Fatalf("readall: %s", err)
|
t.Fatalf("readall: %s", err)
|
||||||
} else if string(buf) != "hello" {
|
} else if string(buf) != "hello" {
|
||||||
t.Fatalf("got %q, expected %q", buf, "hello")
|
t.Fatalf("got %q, expected %q", buf, "hello")
|
||||||
|
@ -22,7 +28,8 @@ func TestMsgreader(t *testing.T) {
|
||||||
t.Fatalf("writing msgreader_test.txt: %s", err)
|
t.Fatalf("writing msgreader_test.txt: %s", err)
|
||||||
}
|
}
|
||||||
defer os.Remove("msgreader_test.txt")
|
defer os.Remove("msgreader_test.txt")
|
||||||
mr := &MsgReader{prefix: []byte("hello"), path: "msgreader_test.txt", size: int64(len("hello world"))}
|
mr = &MsgReader{prefix: []byte("hello"), path: "msgreader_test.txt", size: int64(len("hello world"))}
|
||||||
|
defer mr.Close()
|
||||||
if buf, err := io.ReadAll(mr); err != nil {
|
if buf, err := io.ReadAll(mr); err != nil {
|
||||||
t.Fatalf("readall: %s", err)
|
t.Fatalf("readall: %s", err)
|
||||||
} else if string(buf) != "hello world" {
|
} else if string(buf) != "hello world" {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -15,7 +16,7 @@ import (
|
||||||
|
|
||||||
func TestThreadingUpgrade(t *testing.T) {
|
func TestThreadingUpgrade(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/store/data")
|
os.RemoveAll("../testdata/store/data")
|
||||||
mox.ConfigStaticPath = "../testdata/store/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
acc, err := OpenAccount("mjl")
|
acc, err := OpenAccount("mjl")
|
||||||
tcheck(t, err, "open account")
|
tcheck(t, err, "open account")
|
||||||
|
@ -32,6 +33,7 @@ func TestThreadingUpgrade(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
f, err := CreateMessageTemp("account-test")
|
f, err := CreateMessageTemp("account-test")
|
||||||
tcheck(t, err, "temp file")
|
tcheck(t, err, "temp file")
|
||||||
|
defer os.Remove(f.Name())
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
s = strings.ReplaceAll(s, "\n", "\r\n")
|
s = strings.ReplaceAll(s, "\n", "\r\n")
|
||||||
|
@ -40,7 +42,7 @@ func TestThreadingUpgrade(t *testing.T) {
|
||||||
MsgPrefix: []byte(s),
|
MsgPrefix: []byte(s),
|
||||||
Received: recv,
|
Received: recv,
|
||||||
}
|
}
|
||||||
err = acc.DeliverMailbox(log, "Inbox", &m, f, true)
|
err = acc.DeliverMailbox(log, "Inbox", &m, f)
|
||||||
tcheck(t, err, "deliver")
|
tcheck(t, err, "deliver")
|
||||||
if expThreadID == 0 {
|
if expThreadID == 0 {
|
||||||
expThreadID = m.ID
|
expThreadID = m.ID
|
||||||
|
|
|
@ -63,7 +63,7 @@ const reportJSON = `{
|
||||||
func TestReport(t *testing.T) {
|
func TestReport(t *testing.T) {
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
mox.Shutdown, mox.ShutdownCancel = context.WithCancel(ctxbg)
|
||||||
mox.ConfigStaticPath = "../testdata/tlsrpt/fake.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/tlsrpt/fake.conf")
|
||||||
mox.Conf.Static.DataDir = "."
|
mox.Conf.Static.DataDir = "."
|
||||||
// Recognize as configured domain.
|
// Recognize as configured domain.
|
||||||
mox.Conf.Dynamic.Domains = map[string]config.Domain{
|
mox.Conf.Dynamic.Domains = map[string]config.Domain{
|
||||||
|
|
3
vendor/github.com/mjl-/adns/Makefile
generated
vendored
3
vendor/github.com/mjl-/adns/Makefile
generated
vendored
|
@ -27,7 +27,8 @@ buildall:
|
||||||
GOOS=illumos GOARCH=amd64 go build
|
GOOS=illumos GOARCH=amd64 go build
|
||||||
GOOS=solaris GOARCH=amd64 go build
|
GOOS=solaris GOARCH=amd64 go build
|
||||||
GOOS=aix GOARCH=ppc64 go build
|
GOOS=aix GOARCH=ppc64 go build
|
||||||
# no windows or plan9 for now
|
GOOS=windows GOARCH=amd64 go build
|
||||||
|
# no plan9 for now
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
gofmt -w -s *.go */*/*.go
|
gofmt -w -s *.go */*/*.go
|
||||||
|
|
65
vendor/github.com/mjl-/adns/dnsconfig_windows.go
generated
vendored
Normal file
65
vendor/github.com/mjl-/adns/dnsconfig_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2022 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package adns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dnsReadConfig(ignoredFilename string) (conf *dnsConfig) {
|
||||||
|
conf = &dnsConfig{
|
||||||
|
ndots: 1,
|
||||||
|
timeout: 5 * time.Second,
|
||||||
|
attempts: 2,
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if len(conf.servers) == 0 {
|
||||||
|
conf.servers = defaultNS
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
aas, err := adapterAddresses()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO(bradfitz): this just collects all the DNS servers on all
|
||||||
|
// the interfaces in some random order. It should order it by
|
||||||
|
// default route, or only use the default route(s) instead.
|
||||||
|
// In practice, however, it mostly works.
|
||||||
|
for _, aa := range aas {
|
||||||
|
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
|
||||||
|
// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
|
||||||
|
if aa.OperStatus != windows.IfOperStatusUp {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sa, err := dns.Address.Sockaddr.Sockaddr()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var ip net.IP
|
||||||
|
switch sa := sa.(type) {
|
||||||
|
case *syscall.SockaddrInet4:
|
||||||
|
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
|
||||||
|
case *syscall.SockaddrInet6:
|
||||||
|
ip = make(net.IP, net.IPv6len)
|
||||||
|
copy(ip, sa.Addr[:])
|
||||||
|
if ip[0] == 0xfe && ip[1] == 0xc0 {
|
||||||
|
// Ignore these fec0/10 ones. Windows seems to
|
||||||
|
// populate them as defaults on its misc rando
|
||||||
|
// interfaces.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Unexpected type.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conf.servers = append(conf.servers, JoinHostPort(ip.String(), "53"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
}
|
43
vendor/github.com/mjl-/adns/interface_windows.go
generated
vendored
Normal file
43
vendor/github.com/mjl-/adns/interface_windows.go
generated
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package adns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// adapterAddresses returns a list of IP adapter and address
|
||||||
|
// structures. The structure contains an IP adapter and flattened
|
||||||
|
// multiple IP addresses including unicast, anycast and multicast
|
||||||
|
// addresses.
|
||||||
|
func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
|
||||||
|
var b []byte
|
||||||
|
l := uint32(15000) // recommended initial size
|
||||||
|
for {
|
||||||
|
b = make([]byte, l)
|
||||||
|
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
|
||||||
|
if err == nil {
|
||||||
|
if l == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW {
|
||||||
|
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
||||||
|
}
|
||||||
|
if l <= uint32(len(b)) {
|
||||||
|
return nil, os.NewSyscallError("getadaptersaddresses", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var aas []*windows.IpAdapterAddresses
|
||||||
|
for aa := (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next {
|
||||||
|
aas = append(aas, aa)
|
||||||
|
}
|
||||||
|
return aas, nil
|
||||||
|
}
|
2
vendor/github.com/mjl-/adns/lookup_unix.go
generated
vendored
2
vendor/github.com/mjl-/adns/lookup_unix.go
generated
vendored
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build unix || wasip1
|
//go:build unix || wasip1 || windows
|
||||||
|
|
||||||
package adns
|
package adns
|
||||||
|
|
||||||
|
|
2
vendor/github.com/mjl-/adns/port_unix.go
generated
vendored
2
vendor/github.com/mjl-/adns/port_unix.go
generated
vendored
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build unix || (js && wasm) || wasip1
|
//go:build unix || (js && wasm) || wasip1 || windows
|
||||||
|
|
||||||
// Read system port mappings from /etc/services
|
// Read system port mappings from /etc/services
|
||||||
|
|
||||||
|
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -11,7 +11,7 @@ github.com/golang/protobuf/ptypes/timestamp
|
||||||
# github.com/matttproud/golang_protobuf_extensions v1.0.1
|
# github.com/matttproud/golang_protobuf_extensions v1.0.1
|
||||||
## explicit
|
## explicit
|
||||||
github.com/matttproud/golang_protobuf_extensions/pbutil
|
github.com/matttproud/golang_protobuf_extensions/pbutil
|
||||||
# github.com/mjl-/adns v0.0.0-20231009145311-e3834995f16c
|
# github.com/mjl-/adns v0.0.0-20231013194548-ea0378d616ab
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/mjl-/adns
|
github.com/mjl-/adns
|
||||||
github.com/mjl-/adns/internal/bytealg
|
github.com/mjl-/adns/internal/bytealg
|
||||||
|
|
|
@ -299,15 +299,13 @@ func Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if tmpf != nil {
|
if tmpf != nil {
|
||||||
|
name := tmpf.Name()
|
||||||
err := tmpf.Close()
|
err := tmpf.Close()
|
||||||
log.Check(err, "closing uploaded file")
|
log.Check(err, "closing uploaded file")
|
||||||
|
err = os.Remove(name)
|
||||||
|
log.Check(err, "removing temporary file", mlog.Field("path", name))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := os.Remove(tmpf.Name()); err != nil {
|
|
||||||
log.Errorx("removing temporary file", err)
|
|
||||||
http.Error(w, "500 - internal server error - "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(tmpf, f); err != nil {
|
if _, err := io.Copy(tmpf, f); err != nil {
|
||||||
log.Errorx("copying import to temporary file", err)
|
log.Errorx("copying import to temporary file", err)
|
||||||
http.Error(w, "500 - internal server error - "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "500 - internal server error - "+err.Error(), http.StatusInternalServerError)
|
||||||
|
@ -319,7 +317,7 @@ func Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "500 - internal server error - "+err.Error(), http.StatusInternalServerError)
|
http.Error(w, "500 - internal server error - "+err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tmpf = nil // importStart is now responsible for closing.
|
tmpf = nil // importStart is now responsible for cleanup.
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]string{"ImportToken": token})
|
_ = json.NewEncoder(w).Encode(map[string]string{"ImportToken": token})
|
||||||
|
|
|
@ -38,7 +38,7 @@ func tcheck(t *testing.T, err error, msg string) {
|
||||||
|
|
||||||
func TestAccount(t *testing.T) {
|
func TestAccount(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/httpaccount/data")
|
os.RemoveAll("../testdata/httpaccount/data")
|
||||||
mox.ConfigStaticPath = "../testdata/httpaccount/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/httpaccount/mox.conf")
|
||||||
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
acc, err := store.OpenAccount("mjl")
|
acc, err := store.OpenAccount("mjl")
|
||||||
|
@ -144,8 +144,8 @@ func TestAccount(t *testing.T) {
|
||||||
t.Fatalf("imported %d messages, expected %d", count, expect)
|
t.Fatalf("imported %d messages, expected %d", count, expect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
testImport("../testdata/importtest.mbox.zip", 2)
|
testImport(filepath.FromSlash("../testdata/importtest.mbox.zip"), 2)
|
||||||
testImport("../testdata/importtest.maildir.tgz", 2)
|
testImport(filepath.FromSlash("../testdata/importtest.maildir.tgz"), 2)
|
||||||
|
|
||||||
// Check there are messages, with the right flags.
|
// Check there are messages, with the right flags.
|
||||||
acc.DB.Read(ctxbg, func(tx *bstore.Tx) error {
|
acc.DB.Read(ctxbg, func(tx *bstore.Tx) error {
|
||||||
|
|
|
@ -198,12 +198,15 @@ type importStep struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// importStart prepare the import and launches the goroutine to actually import.
|
// importStart prepare the import and launches the goroutine to actually import.
|
||||||
// importStart is responsible for closing f.
|
// importStart is responsible for closing f and removing f.
|
||||||
func importStart(log *mlog.Log, accName string, f *os.File, skipMailboxPrefix string) (string, error) {
|
func importStart(log *mlog.Log, accName string, f *os.File, skipMailboxPrefix string) (string, error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
|
name := f.Name()
|
||||||
err := f.Close()
|
err := f.Close()
|
||||||
log.Check(err, "closing uploaded file")
|
log.Check(err, "closing uploaded file")
|
||||||
|
err = os.Remove(name)
|
||||||
|
log.Check(err, "removing uploaded file", mlog.Field("name", name))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -270,7 +273,7 @@ func importStart(log *mlog.Log, accName string, f *os.File, skipMailboxPrefix st
|
||||||
|
|
||||||
log.Info("starting import")
|
log.Info("starting import")
|
||||||
go importMessages(ctx, log.WithCid(mox.Cid()), token, acc, tx, zr, tr, f, skipMailboxPrefix)
|
go importMessages(ctx, log.WithCid(mox.Cid()), token, acc, tx, zr, tr, f, skipMailboxPrefix)
|
||||||
f = nil // importMessages is now responsible for closing.
|
f = nil // importMessages is now responsible for closing and removing.
|
||||||
|
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
@ -313,8 +316,11 @@ func importMessages(ctx context.Context, log *mlog.Log, token string, acc *store
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
name := f.Name()
|
||||||
err := f.Close()
|
err := f.Close()
|
||||||
log.Check(err, "closing uploaded messages file")
|
log.Check(err, "closing uploaded messages file")
|
||||||
|
err = os.Remove(name)
|
||||||
|
log.Check(err, "removing uploaded messages file", mlog.Field("path", name))
|
||||||
|
|
||||||
for _, id := range deliveredIDs {
|
for _, id := range deliveredIDs {
|
||||||
p := acc.MessagePath(id)
|
p := acc.MessagePath(id)
|
||||||
|
@ -482,12 +488,11 @@ func importMessages(ctx context.Context, log *mlog.Log, token string, acc *store
|
||||||
|
|
||||||
xdeliver := func(mb store.Mailbox, m *store.Message, f *os.File, pos string) {
|
xdeliver := func(mb store.Mailbox, m *store.Message, f *os.File, pos string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
name := f.Name()
|
||||||
err := os.Remove(f.Name())
|
err = f.Close()
|
||||||
log.Check(err, "removing temporary message file for delivery")
|
log.Check(err, "closing temporary message file for delivery")
|
||||||
err = f.Close()
|
err := os.Remove(name)
|
||||||
log.Check(err, "closing temporary message file for delivery")
|
log.Check(err, "removing temporary message file for delivery")
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
m.MailboxID = mb.ID
|
m.MailboxID = mb.ID
|
||||||
m.MailboxOrigID = mb.ID
|
m.MailboxOrigID = mb.ID
|
||||||
|
@ -542,11 +547,10 @@ func importMessages(ctx context.Context, log *mlog.Log, token string, acc *store
|
||||||
trainMessage(m, p, pos)
|
trainMessage(m, p, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
const consumeFile = true
|
|
||||||
const sync = false
|
const sync = false
|
||||||
const notrain = true
|
const notrain = true
|
||||||
const nothreads = true
|
const nothreads = true
|
||||||
if err := acc.DeliverMessage(log, tx, m, f, consumeFile, sync, notrain, nothreads); err != nil {
|
if err := acc.DeliverMessage(log, tx, m, f, sync, notrain, nothreads); err != nil {
|
||||||
problemf("delivering message %s: %s (continuing)", pos, err)
|
problemf("delivering message %s: %s (continuing)", pos, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -557,7 +561,6 @@ func importMessages(ctx context.Context, log *mlog.Log, token string, acc *store
|
||||||
prevMailbox = mb.Name
|
prevMailbox = mb.Name
|
||||||
sendEvent("count", importCount{mb.Name, messages[mb.Name]})
|
sendEvent("count", importCount{mb.Name, messages[mb.Name]})
|
||||||
}
|
}
|
||||||
f = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ximportMbox := func(mailbox, filename string, r io.Reader) {
|
ximportMbox := func(mailbox, filename string, r io.Reader) {
|
||||||
|
@ -591,10 +594,11 @@ func importMessages(ctx context.Context, log *mlog.Log, token string, acc *store
|
||||||
ximportcheckf(err, "creating temp message")
|
ximportcheckf(err, "creating temp message")
|
||||||
defer func() {
|
defer func() {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
err := os.Remove(f.Name())
|
name := f.Name()
|
||||||
log.Check(err, "removing temporary file for delivery")
|
|
||||||
err = f.Close()
|
err = f.Close()
|
||||||
log.Check(err, "closing temporary file for delivery")
|
log.Check(err, "closing temporary file for delivery")
|
||||||
|
err := os.Remove(name)
|
||||||
|
log.Check(err, "removing temporary file for delivery", mlog.Field("path", name))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -352,12 +352,11 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
dataFile, err := store.CreateMessageTemp("webmail-submit")
|
dataFile, err := store.CreateMessageTemp("webmail-submit")
|
||||||
xcheckf(ctx, err, "creating temporary file for message")
|
xcheckf(ctx, err, "creating temporary file for message")
|
||||||
defer func() {
|
defer func() {
|
||||||
if dataFile != nil {
|
name := dataFile.Name()
|
||||||
err := dataFile.Close()
|
err := dataFile.Close()
|
||||||
log.Check(err, "closing submit message file")
|
log.Check(err, "closing submit message file")
|
||||||
err = os.Remove(dataFile.Name())
|
err = os.Remove(name)
|
||||||
log.Check(err, "removing temporary submit message file")
|
log.Check(err, "removing temporary submit message file", mlog.Field("name", name))
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// If writing to the message file fails, we abort immediately.
|
// If writing to the message file fails, we abort immediately.
|
||||||
|
@ -669,7 +668,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
Localpart: rcpt.Localpart,
|
Localpart: rcpt.Localpart,
|
||||||
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
IPDomain: dns.IPDomain{Domain: rcpt.Domain},
|
||||||
}
|
}
|
||||||
_, err := queue.Add(ctx, log, reqInfo.AccountName, fromPath, toPath, has8bit, smtputf8, msgSize, messageID, []byte(rcptMsgPrefix), dataFile, nil, false)
|
_, err := queue.Add(ctx, log, reqInfo.AccountName, fromPath, toPath, has8bit, smtputf8, msgSize, messageID, []byte(rcptMsgPrefix), dataFile, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metricSubmission.WithLabelValues("queueerror").Inc()
|
metricSubmission.WithLabelValues("queueerror").Inc()
|
||||||
}
|
}
|
||||||
|
@ -720,11 +719,6 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
sentmb, err := bstore.QueryDB[store.Mailbox](ctx, acc.DB).FilterEqual("Sent", true).Get()
|
sentmb, err := bstore.QueryDB[store.Mailbox](ctx, acc.DB).FilterEqual("Sent", true).Get()
|
||||||
if err == bstore.ErrAbsent {
|
if err == bstore.ErrAbsent {
|
||||||
// There is no mailbox designated as Sent mailbox, so we're done.
|
// There is no mailbox designated as Sent mailbox, so we're done.
|
||||||
err := os.Remove(dataFile.Name())
|
|
||||||
log.Check(err, "removing submitmessage file")
|
|
||||||
err = dataFile.Close()
|
|
||||||
log.Check(err, "closing submitmessage file")
|
|
||||||
dataFile = nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xcheckf(ctx, err, "message submitted to queue, adding to Sent mailbox")
|
xcheckf(ctx, err, "message submitted to queue, adding to Sent mailbox")
|
||||||
|
@ -749,7 +743,7 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
err = tx.Update(&sentmb)
|
err = tx.Update(&sentmb)
|
||||||
xcheckf(ctx, err, "updating sent mailbox for counts")
|
xcheckf(ctx, err, "updating sent mailbox for counts")
|
||||||
|
|
||||||
err = acc.DeliverMessage(log, tx, &sentm, dataFile, true, true, false, false)
|
err = acc.DeliverMessage(log, tx, &sentm, dataFile, true, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metricSubmission.WithLabelValues("storesenterror").Inc()
|
metricSubmission.WithLabelValues("storesenterror").Inc()
|
||||||
metricked = true
|
metricked = true
|
||||||
|
@ -757,10 +751,6 @@ func (w Webmail) MessageSubmit(ctx context.Context, m SubmitMessage) {
|
||||||
xcheckf(ctx, err, "message submitted to queue, appending message to Sent mailbox")
|
xcheckf(ctx, err, "message submitted to queue, appending message to Sent mailbox")
|
||||||
|
|
||||||
changes = append(changes, sentm.ChangeAddUID(), sentmb.ChangeCounts())
|
changes = append(changes, sentm.ChangeAddUID(), sentmb.ChangeCounts())
|
||||||
|
|
||||||
err = dataFile.Close()
|
|
||||||
log.Check(err, "closing submit message file")
|
|
||||||
dataFile = nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
store.BroadcastChanges(acc, changes)
|
store.BroadcastChanges(acc, changes)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ func TestAPI(t *testing.T) {
|
||||||
mox.LimitersInit()
|
mox.LimitersInit()
|
||||||
os.RemoveAll("../testdata/webmail/data")
|
os.RemoveAll("../testdata/webmail/data")
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/webmail/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webmail/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
defer store.Switchboard()()
|
defer store.Switchboard()()
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -23,7 +24,7 @@ import (
|
||||||
func TestView(t *testing.T) {
|
func TestView(t *testing.T) {
|
||||||
os.RemoveAll("../testdata/webmail/data")
|
os.RemoveAll("../testdata/webmail/data")
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/webmail/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webmail/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
defer store.Switchboard()()
|
defer store.Switchboard()()
|
||||||
|
|
||||||
|
|
|
@ -160,8 +160,8 @@ type merged struct {
|
||||||
var webmail = &merged{
|
var webmail = &merged{
|
||||||
fallbackHTML: webmailHTML,
|
fallbackHTML: webmailHTML,
|
||||||
fallbackJS: webmailJS,
|
fallbackJS: webmailJS,
|
||||||
htmlPath: "webmail/webmail.html",
|
htmlPath: filepath.FromSlash("webmail/webmail.html"),
|
||||||
jsPath: "webmail/webmail.js",
|
jsPath: filepath.FromSlash("webmail/webmail.js"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallbackMtime returns a time to use for the Last-Modified header in case we
|
// fallbackMtime returns a time to use for the Last-Modified header in case we
|
||||||
|
@ -733,7 +733,7 @@ func handle(apiHandler http.Handler, w http.ResponseWriter, r *http.Request) {
|
||||||
h.Set("Content-Type", "text/html; charset=utf-8")
|
h.Set("Content-Type", "text/html; charset=utf-8")
|
||||||
h.Set("Cache-Control", "no-cache, max-age=0")
|
h.Set("Cache-Control", "no-cache, max-age=0")
|
||||||
|
|
||||||
path := "webmail/msg.html"
|
path := filepath.FromSlash("webmail/msg.html")
|
||||||
fallback := webmailmsgHTML
|
fallback := webmailmsgHTML
|
||||||
serveContentFallback(log, w, r, path, fallback)
|
serveContentFallback(log, w, r, path, fallback)
|
||||||
|
|
||||||
|
@ -800,7 +800,7 @@ func handle(apiHandler http.Handler, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// We typically return the embedded file, but during development it's handy to load
|
// We typically return the embedded file, but during development it's handy to load
|
||||||
// from disk.
|
// from disk.
|
||||||
path := "webmail/text.html"
|
path := filepath.FromSlash("webmail/text.html")
|
||||||
fallback := webmailtextHTML
|
fallback := webmailtextHTML
|
||||||
serveContentFallback(log, w, r, path, fallback)
|
serveContentFallback(log, w, r, path, fallback)
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -253,10 +254,12 @@ type testmsg struct {
|
||||||
func tdeliver(t *testing.T, acc *store.Account, tm *testmsg) {
|
func tdeliver(t *testing.T, acc *store.Account, tm *testmsg) {
|
||||||
msgFile, err := store.CreateMessageTemp("webmail-test")
|
msgFile, err := store.CreateMessageTemp("webmail-test")
|
||||||
tcheck(t, err, "create message temp")
|
tcheck(t, err, "create message temp")
|
||||||
|
defer os.Remove(msgFile.Name())
|
||||||
|
defer msgFile.Close()
|
||||||
size, err := msgFile.Write(tm.msg.Marshal(t))
|
size, err := msgFile.Write(tm.msg.Marshal(t))
|
||||||
tcheck(t, err, "write message temp")
|
tcheck(t, err, "write message temp")
|
||||||
m := store.Message{Flags: tm.Flags, Keywords: tm.Keywords, Size: int64(size)}
|
m := store.Message{Flags: tm.Flags, Keywords: tm.Keywords, Size: int64(size)}
|
||||||
err = acc.DeliverMailbox(xlog, tm.Mailbox, &m, msgFile, true)
|
err = acc.DeliverMailbox(xlog, tm.Mailbox, &m, msgFile)
|
||||||
tcheck(t, err, "deliver test message")
|
tcheck(t, err, "deliver test message")
|
||||||
err = msgFile.Close()
|
err = msgFile.Close()
|
||||||
tcheck(t, err, "closing test message")
|
tcheck(t, err, "closing test message")
|
||||||
|
@ -272,7 +275,7 @@ func TestWebmail(t *testing.T) {
|
||||||
mox.LimitersInit()
|
mox.LimitersInit()
|
||||||
os.RemoveAll("../testdata/webmail/data")
|
os.RemoveAll("../testdata/webmail/data")
|
||||||
mox.Context = ctxbg
|
mox.Context = ctxbg
|
||||||
mox.ConfigStaticPath = "../testdata/webmail/mox.conf"
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webmail/mox.conf")
|
||||||
mox.MustLoadConfig(true, false)
|
mox.MustLoadConfig(true, false)
|
||||||
defer store.Switchboard()()
|
defer store.Switchboard()()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue