diff --git a/Makefile b/Makefile index 56c3cb3..88f7090 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ default: build build: # build early to catch syntax errors 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 ./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 diff --git a/doc.go b/doc.go index 2817d6f..f8e93c9 100644 --- a/doc.go +++ b/doc.go @@ -329,6 +329,8 @@ during those commands instead of during "data". usage: mox localserve -dir string 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 diff --git a/docker-compose-integration.yml b/docker-compose-integration.yml index 52ff11f..0a466c5 100644 --- a/docker-compose-integration.yml +++ b/docker-compose-integration.yml @@ -10,6 +10,7 @@ services: volumes: - ./.go:/.go - ./testdata/integration/resolv.conf:/etc/resolv.conf + - ./testdata/integration/moxsubmit.conf:/etc/moxsubmit.conf - .:/mox environment: GOCACHE: /.go/.cache/go-build @@ -23,6 +24,8 @@ services: condition: service_healthy postfixmail: condition: service_healthy + localserve: + condition: service_healthy networks: mailnet1: ipv4_address: 172.28.1.10 @@ -53,6 +56,31 @@ services: mailnet1: 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: hostname: dns.example build: diff --git a/integration_test.go b/integration_test.go index 2140569..6b03f70 100644 --- a/integration_test.go +++ b/integration_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -36,6 +37,7 @@ func tcheck(t *testing.T, err error, msg string) { // We check if we receive the message. func TestDeliver(t *testing.T) { mlog.Logfmt = true + log := mlog.New("test") // Remove state. os.RemoveAll("testdata/integration/data") @@ -79,16 +81,16 @@ func TestDeliver(t *testing.T) { err error } - deliver := func(username, desthost, mailfrom, password, rcptto, imapuser string) { + testDeliver := func(checkTime bool, imapaddr, imapuser, imappass string, fn func()) { t.Helper() // 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") defer imapconn.Close() client, err := imapclient.New(imapconn, false) tcheck(t, err, "new imapclient") - _, _, err = client.Login(imapuser, "pass1234") + _, _, err = client.Login(imapuser, imappass) tcheck(t, err, "imap client login") _, _, err = client.Select("inbox") tcheck(t, err, "imap select inbox") @@ -114,7 +116,27 @@ func TestDeliver(t *testing.T) { 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") defer conn.Close() @@ -126,30 +148,36 @@ This is the message. `, mailfrom, rcptto) msg = strings.ReplaceAll(msg, "\n", "\r\n") 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") - t0 := time.Now() err = c.Deliver(mox.Context, mailfrom, rcptto, int64(len(msg)), strings.NewReader(msg), false, false) tcheck(t, err, "deliver with smtp") err = c.Close() 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") - deliver("moxtest3", "moxmail2.mox2.example", "moxtest2@mox2.example", "pass1234", "moxtest3@mox3.example", "moxtest3@mox3.example") + testDeliver(true, "moxmail1.mox1.example:143", "moxtest1@mox1.example", "pass1234", func() { + 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") + }) } diff --git a/localserve.go b/localserve.go index dce6979..7af283f 100644 --- a/localserve.go +++ b/localserve.go @@ -65,8 +65,9 @@ during those commands instead of during "data". userConfDir = "." } - var dir string + var dir, ip string 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() if len(args) != 0 { c.Usage() @@ -78,7 +79,7 @@ during those commands instead of during "data". // Load config, creating a new one if needed. if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { - err := writeLocalConfig(log, dir) + err := writeLocalConfig(log, dir, ip) if err != nil { 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)) } 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)) + } else if ip != "" { + log.Fatal("can only use -ip when writing a new config file") } 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() { x := recover() if x != nil { @@ -257,9 +260,13 @@ func writeLocalConfig(log *mlog.Log, dir string) (rerr error) { xcheck(err, "writing adminpasswd file") // Write mox.conf. + ips := []string{"127.0.0.1", "::1"} + if ip != "" { + ips = []string{ip} + } local := config.Listener{ - IPs: []string{"127.0.0.1", "::1"}, + IPs: ips, TLS: &config.TLS{ KeyCerts: []config.KeyCert{ { diff --git a/testdata/integration/example.zone b/testdata/integration/example.zone index 06ab800..a190b0d 100644 --- a/testdata/integration/example.zone +++ b/testdata/integration/example.zone @@ -10,6 +10,7 @@ moxmail2.mox2 IN A 172.28.2.10 moxmail3.mox3 IN A 172.28.3.10 postfixmail.postfix IN A 172.28.1.20 dns IN A 172.28.1.30 +localserve.mox1 IN A 172.28.1.50 mox1 MX 10 moxmail1.mox1.example. mox2 MX 10 moxmail2.mox2.example. diff --git a/testdata/integration/moxsubmit.conf b/testdata/integration/moxsubmit.conf new file mode 100644 index 0000000..7b24a75 --- /dev/null +++ b/testdata/integration/moxsubmit.conf @@ -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