maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1//go:build integration && (darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris)
  2// +build integration
  3// +build darwin dragonfly freebsd linux netbsd openbsd solaris
  4
  5/*
  6Maddy Mail Server - Composable all-in-one email server.
  7Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  8
  9This program is free software: you can redistribute it and/or modify
 10it under the terms of the GNU General Public License as published by
 11the Free Software Foundation, either version 3 of the License, or
 12(at your option) any later version.
 13
 14This program is distributed in the hope that it will be useful,
 15but WITHOUT ANY WARRANTY; without even the implied warranty of
 16MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 17GNU General Public License for more details.
 18
 19You should have received a copy of the GNU General Public License
 20along with this program.  If not, see <https://www.gnu.org/licenses/>.
 21*/
 22
 23// only posix systems ^
 24
 25package tests_test
 26
 27import (
 28	"bufio"
 29	"errors"
 30	"flag"
 31	"io/ioutil"
 32	"os"
 33	"os/exec"
 34	"os/user"
 35	"path/filepath"
 36	"strings"
 37	"syscall"
 38	"testing"
 39	"time"
 40
 41	"github.com/foxcpp/maddy/tests"
 42)
 43
 44var DovecotExecutable string
 45
 46func init() {
 47	flag.StringVar(&DovecotExecutable, "integration.dovecot", "dovecot", "path to dovecot executable for interop tests")
 48}
 49
 50const dovecotConf = `base_dir = $ROOT/run/
 51state_dir = $ROOT/lib/
 52log_path = /dev/stderr
 53ssl = no
 54
 55default_internal_user = $USER
 56default_internal_group = $GROUP
 57default_login_user = $USER
 58
 59passdb {
 60	driver = passwd-file
 61	args = $ROOT/passwd
 62}
 63
 64userdb {
 65	driver = passwd-file
 66	args = $ROOT/passwd
 67}
 68
 69service auth {
 70	unix_listener auth {
 71		mode = 0666
 72	}
 73}
 74
 75# Dovecot refuses to start without protocols, so we need to give it one.
 76protocols = imap
 77
 78service imap-login {
 79	chroot =
 80	inet_listener imap {
 81		address = 127.0.0.1
 82		port = 0
 83	}
 84}
 85
 86service anvil {
 87	chroot =
 88}
 89
 90# Turn on debugging information, to help troubleshooting issues.
 91auth_verbose = yes
 92auth_debug = yes
 93auth_debug_passwords = yes
 94auth_verbose_passwords = yes
 95mail_debug = yes
 96`
 97
 98const dovecotPasswd = `tester:{plain}123456:1000:1000::/home/user`
 99
100func runDovecot(t *testing.T) (string, *exec.Cmd) {
101	dovecotExec, err := exec.LookPath(DovecotExecutable)
102	if err != nil {
103		if errors.Is(err, exec.ErrNotFound) {
104			t.Skip("No Dovecot executable found, skipping interop. tests")
105		}
106		t.Fatal(err)
107	}
108
109	tempDir := t.TempDir()
110
111	curUser, err := user.Current()
112	if err != nil {
113		t.Fatal(err)
114	}
115	curGroup, err := user.LookupGroupId(curUser.Gid)
116	if err != nil {
117		t.Fatal(err)
118	}
119
120	dovecotConf := strings.NewReplacer(
121		"$ROOT", tempDir,
122		"$USER", curUser.Username,
123		"$GROUP", curGroup.Name).Replace(dovecotConf)
124	err = ioutil.WriteFile(filepath.Join(tempDir, "dovecot.conf"), []byte(dovecotConf), os.ModePerm)
125	if err != nil {
126		t.Fatal(err)
127	}
128	err = ioutil.WriteFile(filepath.Join(tempDir, "passwd"), []byte(dovecotPasswd), os.ModePerm)
129	if err != nil {
130		t.Fatal(err)
131	}
132
133	cmd := exec.Command(dovecotExec, "-F", "-c", filepath.Join(tempDir, "dovecot.conf"))
134	stderr, err := cmd.StderrPipe()
135	if err != nil {
136		t.Fatal(err)
137	}
138
139	if err := cmd.Start(); err != nil {
140		t.Fatal(err)
141	}
142
143	ready := make(chan struct{}, 1)
144
145	go func() {
146		scnr := bufio.NewScanner(stderr)
147		for scnr.Scan() {
148			line := scnr.Text()
149
150			// One of messages printed near completing initialization.
151			if strings.Contains(line, "starting up for imap") {
152				time.Sleep(500*time.Millisecond)
153				ready <- struct{}{}
154			}
155
156			t.Log("dovecot:", line)
157		}
158		if err := scnr.Err(); err != nil {
159			t.Log("stderr I/O error:", err)
160		}
161	}()
162
163	<-ready
164
165	return tempDir, cmd
166}
167
168func cleanDovecot(t *testing.T, tempDir string, cmd *exec.Cmd) {
169	cmd.Process.Signal(syscall.SIGTERM)
170	if !t.Failed() {
171		os.RemoveAll(tempDir)
172	} else {
173		t.Log("Dovecot directory is not deleted:", tempDir)
174	}
175}
176
177func TestDovecotSASLClient(tt *testing.T) {
178	tt.Parallel()
179
180	dovecotDir, cmd := runDovecot(tt)
181	defer cleanDovecot(tt, dovecotDir, cmd)
182
183	t := tests.NewT(tt)
184	t.DNS(nil)
185	t.Port("smtp")
186	t.Env("DOVECOT_SASL_SOCK=" + filepath.Join(dovecotDir, "run", "auth-client"))
187	t.Config(`
188		smtp tcp://127.0.0.1:{env:TEST_PORT_smtp} {
189			hostname mx.maddy.test
190			tls off
191			auth dovecot_sasl unix://{env:DOVECOT_SASL_SOCK}
192			deliver_to dummy
193		}`)
194	t.Run(1)
195	defer t.Close()
196
197	c := t.Conn("smtp")
198	defer c.Close()
199	c.SMTPNegotation("localhost", nil, nil)
200	c.Writeln("AUTH PLAIN AHRlc3QAMTIzNDU2") // 0x00 test 0x00 123456 (invalid user)
201	c.ExpectPattern("535 *")
202	c.Writeln("AUTH PLAIN AHRlc3RlcgAxMjM0NQ==") // 0x00 tester 0x00 12345 (invalid password)
203	c.ExpectPattern("535 *")
204	c.Writeln("AUTH PLAIN AHRlc3RlcgAxMjM0NTY=") // 0x00 tester 0x00 123456
205	c.ExpectPattern("235 *")
206}