more integration tests: start "mox localserve" and submit a message with smtpclient and with "mox sendmail", check that we receive it

This commit is contained in:
Mechiel Lukkien 2023-07-01 18:48:29 +02:00
parent 7facf9d446
commit 1469b7293e
No known key found for this signature in database
7 changed files with 104 additions and 28 deletions

View file

@ -3,7 +3,7 @@ default: build
build: build:
# build early to catch syntax errors # build early to catch syntax errors
CGO_ENABLED=0 go build CGO_ENABLED=0 go build
CGO_ENABLED=0 go vet -tags integration ./... CGO_ENABLED=0 go vet -tags integration
CGO_ENABLED=0 go vet -tags quickstart quickstart_test.go CGO_ENABLED=0 go vet -tags quickstart quickstart_test.go
./gendoc.sh ./gendoc.sh
(cd http && CGO_ENABLED=0 go run ../vendor/github.com/mjl-/sherpadoc/cmd/sherpadoc/*.go -adjust-function-names none Admin) >http/adminapi.json (cd http && CGO_ENABLED=0 go run ../vendor/github.com/mjl-/sherpadoc/cmd/sherpadoc/*.go -adjust-function-names none Admin) >http/adminapi.json

2
doc.go
View file

@ -329,6 +329,8 @@ during those commands instead of during "data".
usage: mox localserve usage: mox localserve
-dir string -dir string
configuration storage directory (default "$userconfigdir/mox-localserve") configuration storage directory (default "$userconfigdir/mox-localserve")
-ip string
serve on this ip instead of default 127.0.0.1 and ::1. only used when writing configuration, at first launch.
# mox help # mox help

View file

@ -10,6 +10,7 @@ services:
volumes: volumes:
- ./.go:/.go - ./.go:/.go
- ./testdata/integration/resolv.conf:/etc/resolv.conf - ./testdata/integration/resolv.conf:/etc/resolv.conf
- ./testdata/integration/moxsubmit.conf:/etc/moxsubmit.conf
- .:/mox - .:/mox
environment: environment:
GOCACHE: /.go/.cache/go-build GOCACHE: /.go/.cache/go-build
@ -23,6 +24,8 @@ services:
condition: service_healthy condition: service_healthy
postfixmail: postfixmail:
condition: service_healthy condition: service_healthy
localserve:
condition: service_healthy
networks: networks:
mailnet1: mailnet1:
ipv4_address: 172.28.1.10 ipv4_address: 172.28.1.10
@ -53,6 +56,31 @@ services:
mailnet1: mailnet1:
ipv4_address: 172.28.1.20 ipv4_address: 172.28.1.20
localserve:
hostname: localserve.mox1.example
domainname: mox1.example
build:
dockerfile: Dockerfile.moxmail
context: testdata/integration
command: ["sh", "-c", "set -e; chmod o+r /etc/resolv.conf; go run . -- localserve -ip 172.28.1.50"]
volumes:
- ./.go:/.go
- ./testdata/integration/resolv.conf:/etc/resolv.conf
- .:/mox
environment:
GOCACHE: /.go/.cache/go-build
healthcheck:
test: netstat -nlt | grep ':1025 '
interval: 1s
timeout: 1s
retries: 10
depends_on:
dns:
condition: service_healthy
networks:
mailnet1:
ipv4_address: 172.28.1.50
dns: dns:
hostname: dns.example hostname: dns.example
build: build:

View file

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
@ -36,6 +37,7 @@ func tcheck(t *testing.T, err error, msg string) {
// We check if we receive the message. // We check if we receive the message.
func TestDeliver(t *testing.T) { func TestDeliver(t *testing.T) {
mlog.Logfmt = true mlog.Logfmt = true
log := mlog.New("test")
// Remove state. // Remove state.
os.RemoveAll("testdata/integration/data") os.RemoveAll("testdata/integration/data")
@ -79,16 +81,16 @@ func TestDeliver(t *testing.T) {
err error err error
} }
deliver := func(username, desthost, mailfrom, password, rcptto, imapuser string) { testDeliver := func(checkTime bool, imapaddr, imapuser, imappass string, fn func()) {
t.Helper() t.Helper()
// Make IMAP connection, we'll wait for a delivery notification with IDLE. // Make IMAP connection, we'll wait for a delivery notification with IDLE.
imapconn, err := net.Dial("tcp", "moxmail1.mox1.example:143") imapconn, err := net.Dial("tcp", imapaddr)
tcheck(t, err, "dial imap server") tcheck(t, err, "dial imap server")
defer imapconn.Close() defer imapconn.Close()
client, err := imapclient.New(imapconn, false) client, err := imapclient.New(imapconn, false)
tcheck(t, err, "new imapclient") tcheck(t, err, "new imapclient")
_, _, err = client.Login(imapuser, "pass1234") _, _, err = client.Login(imapuser, imappass)
tcheck(t, err, "imap client login") tcheck(t, err, "imap client login")
_, _, err = client.Select("inbox") _, _, err = client.Select("inbox")
tcheck(t, err, "imap select inbox") tcheck(t, err, "imap select inbox")
@ -114,7 +116,27 @@ func TestDeliver(t *testing.T) {
tcheck(t, err, "aborting idle") tcheck(t, err, "aborting idle")
}() }()
conn, err := net.Dial("tcp", desthost+":587") t0 := time.Now()
fn()
// Wait for notification of delivery.
select {
case resp := <-idle:
tcheck(t, resp.err, "idle notification")
_, ok := resp.untagged.(imapclient.UntaggedExists)
if !ok {
t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
}
if d := time.Since(t0); checkTime && d < 1*time.Second {
t.Fatalf("delivery took %v, but should have taken at least 1 second, the first-time sender delay", d)
}
case <-time.After(5 * time.Second):
t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
}
}
submit := func(smtphost, smtpport, mailfrom, password, rcptto string) {
conn, err := net.Dial("tcp", net.JoinHostPort(smtphost, smtpport))
tcheck(t, err, "dial submission") tcheck(t, err, "dial submission")
defer conn.Close() defer conn.Close()
@ -126,30 +148,36 @@ This is the message.
`, mailfrom, rcptto) `, mailfrom, rcptto)
msg = strings.ReplaceAll(msg, "\n", "\r\n") msg = strings.ReplaceAll(msg, "\n", "\r\n")
auth := []sasl.Client{sasl.NewClientPlain(mailfrom, password)} auth := []sasl.Client{sasl.NewClientPlain(mailfrom, password)}
c, err := smtpclient.New(mox.Context, mlog.New("test"), conn, smtpclient.TLSOpportunistic, mox.Conf.Static.HostnameDomain, dns.Domain{ASCII: desthost}, auth) c, err := smtpclient.New(mox.Context, log, conn, smtpclient.TLSOpportunistic, mox.Conf.Static.HostnameDomain, dns.Domain{ASCII: smtphost}, auth)
tcheck(t, err, "smtp hello") tcheck(t, err, "smtp hello")
t0 := time.Now()
err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false) err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false)
tcheck(t, err, "deliver with smtp") tcheck(t, err, "deliver with smtp")
err = c.Close() err = c.Close()
tcheck(t, err, "close smtpclient") tcheck(t, err, "close smtpclient")
// Wait for notification of delivery.
select {
case resp := <-idle:
tcheck(t, resp.err, "idle notification")
_, ok := resp.untagged.(imapclient.UntaggedExists)
if !ok {
t.Fatalf("got idle %#v, expected untagged exists", resp.untagged)
}
if d := time.Since(t0); d < 1*time.Second {
t.Fatalf("delivery took %v, bt should have taken at least 1 second, the first-time sender delay", d)
}
case <-time.After(5 * time.Second):
t.Fatalf("timeout after 5s waiting for IMAP IDLE notification of new message, should take about 1 second")
}
} }
deliver("moxtest1", "moxmail1.mox1.example", "moxtest1@mox1.example", "pass1234", "root@postfix.example", "moxtest1@mox1.example") testDeliver(true, "moxmail1.mox1.example:143", "moxtest1@mox1.example", "pass1234", func() {
deliver("moxtest3", "moxmail2.mox2.example", "moxtest2@mox2.example", "pass1234", "moxtest3@mox3.example", "moxtest3@mox3.example") submit("moxmail1.mox1.example", "587", "moxtest1@mox1.example", "pass1234", "root@postfix.example")
})
testDeliver(true, "moxmail1.mox1.example:143", "moxtest3@mox3.example", "pass1234", func() {
submit("moxmail2.mox2.example", "587", "moxtest2@mox2.example", "pass1234", "moxtest3@mox3.example")
})
testDeliver(false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
submit("localserve.mox1.example", "1587", "mox@localhost", "moxmoxmox", "any@any.example")
})
testDeliver(false, "localserve.mox1.example:1143", "mox@localhost", "moxmoxmox", func() {
cmd := exec.Command("go", "run", ".", "sendmail", "mox@localhost")
const msg = `Subject: test
a message.
`
cmd.Stdin = strings.NewReader(msg)
var out strings.Builder
cmd.Stdout = &out
err := cmd.Run()
log.Print("sendmail", mlog.Field("output", out.String()))
tcheck(t, err, "sendmail")
})
} }

View file

@ -65,8 +65,9 @@ during those commands instead of during "data".
userConfDir = "." userConfDir = "."
} }
var dir string var dir, ip string
c.flag.StringVar(&dir, "dir", filepath.Join(userConfDir, "mox-localserve"), "configuration storage directory") c.flag.StringVar(&dir, "dir", filepath.Join(userConfDir, "mox-localserve"), "configuration storage directory")
c.flag.StringVar(&ip, "ip", "", "serve on this ip instead of default 127.0.0.1 and ::1. only used when writing configuration, at first launch.")
args := c.Parse() args := c.Parse()
if len(args) != 0 { if len(args) != 0 {
c.Usage() c.Usage()
@ -78,7 +79,7 @@ during those commands instead of during "data".
// Load config, creating a new one if needed. // Load config, creating a new one if needed.
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
err := writeLocalConfig(log, dir) err := writeLocalConfig(log, dir, ip)
if err != nil { if err != nil {
log.Fatalx("creating mox localserve config", err, mlog.Field("dir", dir)) log.Fatalx("creating mox localserve config", err, mlog.Field("dir", dir))
} }
@ -86,6 +87,8 @@ during those commands instead of during "data".
log.Fatalx("stat config dir", err, mlog.Field("dir", dir)) log.Fatalx("stat config dir", err, mlog.Field("dir", dir))
} else if err := localLoadConfig(log, dir); err != nil { } else if err := localLoadConfig(log, dir); err != nil {
log.Fatalx("loading mox localserve config (hint: when creating a new config with -dir, the directory must not yet exist)", err, mlog.Field("dir", dir)) log.Fatalx("loading mox localserve config (hint: when creating a new config with -dir, the directory must not yet exist)", err, mlog.Field("dir", dir))
} else if ip != "" {
log.Fatal("can only use -ip when writing a new config file")
} }
if level, ok := mlog.Levels[loglevel]; loglevel != "" && ok { if level, ok := mlog.Levels[loglevel]; loglevel != "" && ok {
@ -181,7 +184,7 @@ during those commands instead of during "data".
} }
} }
func writeLocalConfig(log *mlog.Log, dir string) (rerr error) { func writeLocalConfig(log *mlog.Log, dir, ip string) (rerr error) {
defer func() { defer func() {
x := recover() x := recover()
if x != nil { if x != nil {
@ -257,9 +260,13 @@ func writeLocalConfig(log *mlog.Log, dir string) (rerr error) {
xcheck(err, "writing adminpasswd file") xcheck(err, "writing adminpasswd file")
// Write mox.conf. // Write mox.conf.
ips := []string{"127.0.0.1", "::1"}
if ip != "" {
ips = []string{ip}
}
local := config.Listener{ local := config.Listener{
IPs: []string{"127.0.0.1", "::1"}, IPs: ips,
TLS: &config.TLS{ TLS: &config.TLS{
KeyCerts: []config.KeyCert{ KeyCerts: []config.KeyCert{
{ {

View file

@ -10,6 +10,7 @@ moxmail2.mox2 IN A 172.28.2.10
moxmail3.mox3 IN A 172.28.3.10 moxmail3.mox3 IN A 172.28.3.10
postfixmail.postfix IN A 172.28.1.20 postfixmail.postfix IN A 172.28.1.20
dns IN A 172.28.1.30 dns IN A 172.28.1.30
localserve.mox1 IN A 172.28.1.50
mox1 MX 10 moxmail1.mox1.example. mox1 MX 10 moxmail1.mox1.example.
mox2 MX 10 moxmail2.mox2.example. mox2 MX 10 moxmail2.mox2.example.

10
testdata/integration/moxsubmit.conf vendored Normal file
View file

@ -0,0 +1,10 @@
LocalHostname: localhost
Host: localserve.mox1.example
Port: 1587
TLS: false
STARTTLS: false
Username: mox@localhost
Password: moxmoxmox
AuthMethod: PLAIN
From: mox@localhost
DefaultDestination: mox@localhost