mirror of
https://github.com/mjl-/mox.git
synced 2024-12-27 08:53:48 +03:00
8804d6b60e
the imap & smtp servers now allow logging in with tls client authentication and the "external" sasl authentication mechanism. email clients like thunderbird, fairemail, k9, macos mail implement it. this seems to be the most secure among the authentication mechanism commonly implemented by clients. a useful property is that an account can have a separate tls public key for each device/email client. with tls client cert auth, authentication is also bound to the tls connection. a mitm cannot pass the credentials on to another tls connection, similar to scram-*-plus. though part of scram-*-plus is that clients verify that the server knows the client credentials. for tls client auth with imap, we send a "preauth" untagged message by default. that puts the connection in authenticated state. given the imap connection state machine, further authentication commands are not allowed. some clients don't recognize the preauth message, and try to authenticate anyway, which fails. a tls public key has a config option to disable preauth, keeping new connections in unauthenticated state, to work with such email clients. for smtp (submission), we don't require an explicit auth command. both for imap and smtp, we allow a client to authenticate with another mechanism than "external". in that case, credentials are verified, and have to be for the same account as the tls client auth, but the adress can be another one than the login address configured with the tls public key. only the public key is used to identify the account that is authenticating. we ignore the rest of the certificate. expiration dates, names, constraints, etc are not verified. no certificate authorities are involved. users can upload their own (minimal) certificate. the account web interface shows openssl commands you can run to generate a private key, minimal cert, and a p12 file (the format that email clients seem to like...) containing both private key and certificate. the imapclient & smtpclient packages can now also use tls client auth. and so does "mox sendmail", either with a pem file with private key and certificate, or with just an ed25519 private key. there are new subcommands "mox config tlspubkey ..." for adding/removing/listing tls public keys from the cli, by the admin.
512 lines
12 KiB
Go
512 lines
12 KiB
Go
//go:build !integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
cryptorand "crypto/rand"
|
|
"crypto/x509"
|
|
"flag"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mjl-/mox/config"
|
|
"github.com/mjl-/mox/dmarcdb"
|
|
"github.com/mjl-/mox/dns"
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/mox-"
|
|
"github.com/mjl-/mox/mtastsdb"
|
|
"github.com/mjl-/mox/queue"
|
|
"github.com/mjl-/mox/smtp"
|
|
"github.com/mjl-/mox/store"
|
|
"github.com/mjl-/mox/tlsrptdb"
|
|
)
|
|
|
|
var ctxbg = context.Background()
|
|
var pkglog = mlog.New("ctl", nil)
|
|
|
|
func tcheck(t *testing.T, err error, errmsg string) {
|
|
if err != nil {
|
|
t.Helper()
|
|
t.Fatalf("%s: %v", errmsg, err)
|
|
}
|
|
}
|
|
|
|
// TestCtl executes commands through ctl. This tests at least the protocols (who
|
|
// sends when/what) is tested. We often don't check the actual results, but
|
|
// unhandled errors would cause a panic.
|
|
func TestCtl(t *testing.T) {
|
|
os.RemoveAll("testdata/ctl/data")
|
|
mox.ConfigStaticPath = filepath.FromSlash("testdata/ctl/mox.conf")
|
|
mox.ConfigDynamicPath = filepath.FromSlash("testdata/ctl/domains.conf")
|
|
if errs := mox.LoadConfig(ctxbg, pkglog, true, false); len(errs) > 0 {
|
|
t.Fatalf("loading mox config: %v", errs)
|
|
}
|
|
defer store.Switchboard()()
|
|
|
|
err := queue.Init()
|
|
tcheck(t, err, "queue init")
|
|
defer queue.Shutdown()
|
|
|
|
err = store.Init(ctxbg)
|
|
tcheck(t, err, "store init")
|
|
defer store.Close()
|
|
|
|
testctl := func(fn func(clientctl *ctl)) {
|
|
t.Helper()
|
|
|
|
cconn, sconn := net.Pipe()
|
|
clientctl := ctl{conn: cconn, log: pkglog}
|
|
serverctl := ctl{conn: sconn, log: pkglog}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
servectlcmd(ctxbg, &serverctl, func() {})
|
|
close(done)
|
|
}()
|
|
fn(&clientctl)
|
|
cconn.Close()
|
|
<-done
|
|
sconn.Close()
|
|
}
|
|
|
|
// "deliver"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdDeliver(ctl, "mjl@mox.example")
|
|
})
|
|
|
|
// "setaccountpassword"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdSetaccountpassword(ctl, "mjl", "test4321")
|
|
})
|
|
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesList(ctl)
|
|
})
|
|
|
|
// All messages.
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesAdd(ctl, "", "", "")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesAdd(ctl, "mjl", "", "")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesAdd(ctl, "", "☺.mox.example", "")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesAdd(ctl, "mox", "☺.mox.example", "example.com")
|
|
})
|
|
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesRemove(ctl, 1)
|
|
})
|
|
|
|
// Queue a message to list/change/dump.
|
|
msg := "Subject: subject\r\n\r\nbody\r\n"
|
|
msgFile, err := store.CreateMessageTemp(pkglog, "queuedump-test")
|
|
tcheck(t, err, "temp file")
|
|
_, err = msgFile.Write([]byte(msg))
|
|
tcheck(t, err, "write message")
|
|
_, err = msgFile.Seek(0, 0)
|
|
tcheck(t, err, "rewind message")
|
|
defer os.Remove(msgFile.Name())
|
|
defer msgFile.Close()
|
|
addr, err := smtp.ParseAddress("mjl@mox.example")
|
|
tcheck(t, err, "parse address")
|
|
qml := []queue.Msg{queue.MakeMsg(addr.Path(), addr.Path(), false, false, int64(len(msg)), "<random@localhost>", nil, nil, time.Now(), "subject")}
|
|
queue.Add(ctxbg, pkglog, "mjl", msgFile, qml...)
|
|
qmid := qml[0].ID
|
|
|
|
// Has entries now.
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesList(ctl)
|
|
})
|
|
|
|
// "queuelist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueList(ctl, queue.Filter{}, queue.Sort{})
|
|
})
|
|
|
|
// "queueholdset"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldSet(ctl, queue.Filter{}, true)
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldSet(ctl, queue.Filter{}, false)
|
|
})
|
|
|
|
// "queueschedule"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSchedule(ctl, queue.Filter{}, true, time.Minute)
|
|
})
|
|
|
|
// "queuetransport"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueTransport(ctl, queue.Filter{}, "socks")
|
|
})
|
|
|
|
// "queuerequiretls"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueRequireTLS(ctl, queue.Filter{}, nil)
|
|
})
|
|
|
|
// "queuedump"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueDump(ctl, fmt.Sprintf("%d", qmid))
|
|
})
|
|
|
|
// "queuefail"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueFail(ctl, queue.Filter{})
|
|
})
|
|
|
|
// "queuedrop"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueDrop(ctl, queue.Filter{})
|
|
})
|
|
|
|
// "queueholdruleslist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesList(ctl)
|
|
})
|
|
|
|
// "queueholdrulesadd"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesAdd(ctl, "mjl", "", "")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesAdd(ctl, "mjl", "localhost", "")
|
|
})
|
|
|
|
// "queueholdrulesremove"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesRemove(ctl, 2)
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHoldrulesList(ctl)
|
|
})
|
|
|
|
// "queuesuppresslist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSuppressList(ctl, "mjl")
|
|
})
|
|
|
|
// "queuesuppressadd"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSuppressAdd(ctl, "mjl", "base@localhost")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSuppressAdd(ctl, "mjl", "other@localhost")
|
|
})
|
|
|
|
// "queuesuppresslookup"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSuppressLookup(ctl, "mjl", "base@localhost")
|
|
})
|
|
|
|
// "queuesuppressremove"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSuppressRemove(ctl, "mjl", "base@localhost")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueSuppressList(ctl, "mjl")
|
|
})
|
|
|
|
// "queueretiredlist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueRetiredList(ctl, queue.RetiredFilter{}, queue.RetiredSort{})
|
|
})
|
|
|
|
// "queueretiredprint"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueRetiredPrint(ctl, "1")
|
|
})
|
|
|
|
// "queuehooklist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHookList(ctl, queue.HookFilter{}, queue.HookSort{})
|
|
})
|
|
|
|
// "queuehookschedule"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHookSchedule(ctl, queue.HookFilter{}, true, time.Minute)
|
|
})
|
|
|
|
// "queuehookprint"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHookPrint(ctl, "1")
|
|
})
|
|
|
|
// "queuehookcancel"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHookCancel(ctl, queue.HookFilter{})
|
|
})
|
|
|
|
// "queuehookretiredlist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHookRetiredList(ctl, queue.HookRetiredFilter{}, queue.HookRetiredSort{})
|
|
})
|
|
|
|
// "queuehookretiredprint"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdQueueHookRetiredPrint(ctl, "1")
|
|
})
|
|
|
|
// "importmbox"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdImport(ctl, true, "mjl", "inbox", "testdata/importtest.mbox")
|
|
})
|
|
|
|
// "importmaildir"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdImport(ctl, false, "mjl", "inbox", "testdata/importtest.maildir")
|
|
})
|
|
|
|
// "domainadd"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigDomainAdd(ctl, dns.Domain{ASCII: "mox2.example"}, "mjl", "")
|
|
})
|
|
|
|
// "accountadd"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAccountAdd(ctl, "mjl2", "mjl2@mox2.example")
|
|
})
|
|
|
|
// "addressadd"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAddressAdd(ctl, "mjl3@mox2.example", "mjl2")
|
|
})
|
|
|
|
// Add a message.
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdDeliver(ctl, "mjl3@mox2.example")
|
|
})
|
|
// "retrain", retrain junk filter.
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdRetrain(ctl, "mjl2")
|
|
})
|
|
|
|
// "addressrm"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAddressRemove(ctl, "mjl3@mox2.example")
|
|
})
|
|
|
|
// "accountrm"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAccountRemove(ctl, "mjl2")
|
|
})
|
|
|
|
// "domainrm"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigDomainRemove(ctl, dns.Domain{ASCII: "mox2.example"})
|
|
})
|
|
|
|
// "aliasadd"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasAdd(ctl, "support@mox.example", config.Alias{Addresses: []string{"mjl@mox.example"}})
|
|
})
|
|
|
|
// "aliaslist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasList(ctl, "mox.example")
|
|
})
|
|
|
|
// "aliasprint"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasPrint(ctl, "support@mox.example")
|
|
})
|
|
|
|
// "aliasupdate"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasUpdate(ctl, "support@mox.example", "true", "true", "true")
|
|
})
|
|
|
|
// "aliasaddaddr"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasAddaddr(ctl, "support@mox.example", []string{"mjl2@mox.example"})
|
|
})
|
|
|
|
// "aliasrmaddr"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasRmaddr(ctl, "support@mox.example", []string{"mjl2@mox.example"})
|
|
})
|
|
|
|
// "aliasrm"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigAliasRemove(ctl, "support@mox.example")
|
|
})
|
|
|
|
// accounttlspubkeyadd
|
|
certDER := fakeCert(t)
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigTlspubkeyAdd(ctl, "mjl@mox.example", "testkey", false, certDER)
|
|
})
|
|
|
|
// "accounttlspubkeylist"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigTlspubkeyList(ctl, "")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigTlspubkeyList(ctl, "mjl")
|
|
})
|
|
|
|
tpkl, err := store.TLSPublicKeyList(ctxbg, "")
|
|
tcheck(t, err, "list tls public keys")
|
|
if len(tpkl) != 1 {
|
|
t.Fatalf("got %d tls public keys, expected 1", len(tpkl))
|
|
}
|
|
fingerprint := tpkl[0].Fingerprint
|
|
|
|
// "accounttlspubkeyget"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigTlspubkeyGet(ctl, fingerprint)
|
|
})
|
|
|
|
// "accounttlspubkeyrm"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdConfigTlspubkeyRemove(ctl, fingerprint)
|
|
})
|
|
|
|
tpkl, err = store.TLSPublicKeyList(ctxbg, "")
|
|
tcheck(t, err, "list tls public keys")
|
|
if len(tpkl) != 0 {
|
|
t.Fatalf("got %d tls public keys, expected 0", len(tpkl))
|
|
}
|
|
|
|
// "loglevels"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdLoglevels(ctl)
|
|
})
|
|
|
|
// "setloglevels"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdSetLoglevels(ctl, "", "debug")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdSetLoglevels(ctl, "smtpserver", "debug")
|
|
})
|
|
|
|
// Export data, import it again
|
|
xcmdExport(true, false, []string{filepath.FromSlash("testdata/ctl/data/tmp/export/mbox/"), filepath.FromSlash("testdata/ctl/data/accounts/mjl")}, &cmd{log: pkglog})
|
|
xcmdExport(false, false, []string{filepath.FromSlash("testdata/ctl/data/tmp/export/maildir/"), filepath.FromSlash("testdata/ctl/data/accounts/mjl")}, &cmd{log: pkglog})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdImport(ctl, true, "mjl", "inbox", filepath.FromSlash("testdata/ctl/data/tmp/export/mbox/Inbox.mbox"))
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdImport(ctl, false, "mjl", "inbox", filepath.FromSlash("testdata/ctl/data/tmp/export/maildir/Inbox"))
|
|
})
|
|
|
|
// "recalculatemailboxcounts"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdRecalculateMailboxCounts(ctl, "mjl")
|
|
})
|
|
|
|
// "fixmsgsize"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdFixmsgsize(ctl, "mjl")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
acc, err := store.OpenAccount(ctl.log, "mjl")
|
|
tcheck(t, err, "open account")
|
|
defer func() {
|
|
acc.Close()
|
|
acc.CheckClosed()
|
|
}()
|
|
|
|
content := []byte("Subject: hi\r\n\r\nbody\r\n")
|
|
|
|
deliver := func(m *store.Message) {
|
|
t.Helper()
|
|
m.Size = int64(len(content))
|
|
msgf, err := store.CreateMessageTemp(ctl.log, "ctltest")
|
|
tcheck(t, err, "create temp file")
|
|
defer os.Remove(msgf.Name())
|
|
defer msgf.Close()
|
|
_, err = msgf.Write(content)
|
|
tcheck(t, err, "write message file")
|
|
err = acc.DeliverMailbox(ctl.log, "Inbox", m, msgf)
|
|
tcheck(t, err, "deliver message")
|
|
}
|
|
|
|
var msgBadSize store.Message
|
|
deliver(&msgBadSize)
|
|
|
|
msgBadSize.Size = 1
|
|
err = acc.DB.Update(ctxbg, &msgBadSize)
|
|
tcheck(t, err, "update message to bad size")
|
|
mb := store.Mailbox{ID: msgBadSize.MailboxID}
|
|
err = acc.DB.Get(ctxbg, &mb)
|
|
tcheck(t, err, "get db")
|
|
mb.Size -= int64(len(content))
|
|
mb.Size += 1
|
|
err = acc.DB.Update(ctxbg, &mb)
|
|
tcheck(t, err, "update mailbox size")
|
|
|
|
// Fix up the size.
|
|
ctlcmdFixmsgsize(ctl, "")
|
|
|
|
err = acc.DB.Get(ctxbg, &msgBadSize)
|
|
tcheck(t, err, "get message")
|
|
if msgBadSize.Size != int64(len(content)) {
|
|
t.Fatalf("after fixing, message size is %d, should be %d", msgBadSize.Size, len(content))
|
|
}
|
|
})
|
|
|
|
// "reparse"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdReparse(ctl, "mjl")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdReparse(ctl, "")
|
|
})
|
|
|
|
// "reassignthreads"
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdReassignthreads(ctl, "mjl")
|
|
})
|
|
testctl(func(ctl *ctl) {
|
|
ctlcmdReassignthreads(ctl, "")
|
|
})
|
|
|
|
// "backup", backup account.
|
|
err = dmarcdb.Init()
|
|
tcheck(t, err, "dmarcdb init")
|
|
defer dmarcdb.Close()
|
|
err = mtastsdb.Init(false)
|
|
tcheck(t, err, "mtastsdb init")
|
|
defer mtastsdb.Close()
|
|
err = tlsrptdb.Init()
|
|
tcheck(t, err, "tlsrptdb init")
|
|
defer tlsrptdb.Close()
|
|
testctl(func(ctl *ctl) {
|
|
os.RemoveAll("testdata/ctl/data/tmp/backup-data")
|
|
err := os.WriteFile("testdata/ctl/data/receivedid.key", make([]byte, 16), 0600)
|
|
tcheck(t, err, "writing receivedid.key")
|
|
ctlcmdBackup(ctl, filepath.FromSlash("testdata/ctl/data/tmp/backup-data"), false)
|
|
})
|
|
|
|
// Verify the backup.
|
|
xcmd := cmd{
|
|
flag: flag.NewFlagSet("", flag.ExitOnError),
|
|
flagArgs: []string{filepath.FromSlash("testdata/ctl/data/tmp/backup-data")},
|
|
}
|
|
cmdVerifydata(&xcmd)
|
|
}
|
|
|
|
func fakeCert(t *testing.T) []byte {
|
|
t.Helper()
|
|
seed := make([]byte, ed25519.SeedSize)
|
|
privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1), // Required field...
|
|
}
|
|
localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
|
|
tcheck(t, err, "making certificate")
|
|
return localCertBuf
|
|
}
|