git » chasquid » commit 5eded4e

test: Unify (most) SMTP client calls

author Alberto Bertogli
2024-03-09 19:10:08 UTC
committer Alberto Bertogli
2024-03-12 20:43:21 UTC
parent 7f44db008dcb7dd6b5c257966e0d5c5b79206078

test: Unify (most) SMTP client calls

To send mails, today some tests use msmtp and others our internal smtpc.py.

This works, but msmtp slows down the tests significantly, and smtpc.py
is also not particularly fast, and also has some limitations.

This patch introduces a new SMTP client tool written in Go, and makes
almost all the tests use it.

Some tests still remain on msmtp, mainly for client-check compatibility.
It's likely that this will be moved in later patches to a separate
special-purpose test.

With this patch, integration tests take ~20% less time than before.

.gitignore +1 -0
test/t-02-exim/msmtprc +0 -14
test/t-02-exim/run.sh +8 -8
test/t-02-exim/smtpc.conf +4 -0
test/t-04-aliases/content +1 -0
test/t-04-aliases/msmtprc +0 -14
test/t-04-aliases/run.sh +5 -5
test/t-04-aliases/smtpc.conf +4 -0
test/t-05-null_address/expected_dsr +0 -1
test/t-05-null_address/msmtprc +0 -13
test/t-05-null_address/run.sh +1 -1
test/t-05-null_address/smtpc.conf +4 -0
test/t-06-idna/A/chasquid.conf +1 -0
test/t-06-idna/B/chasquid.conf +1 -0
test/t-06-idna/run.sh +8 -8
test/t-07-smtputf8/config/chasquid.conf +1 -0
test/t-07-smtputf8/run.sh +9 -11
test/t-09-loop/msmtprc +0 -14
test/t-09-loop/run.sh +1 -1
test/t-09-loop/smtpc.conf +4 -0
test/t-10-hooks/config/chasquid.conf +1 -0
test/t-10-hooks/msmtprc +0 -15
test/t-10-hooks/run.sh +8 -8
test/t-10-hooks/smtpc.conf +4 -0
test/t-11-dovecot/content +1 -0
test/t-11-dovecot/msmtprc +0 -33
test/t-11-dovecot/run.sh +7 -7
test/t-11-dovecot/smtpc.conf +4 -0
test/t-13-reload/config/chasquid.conf +1 -0
test/t-13-reload/msmtprc +0 -14
test/t-13-reload/run.sh +3 -3
test/t-13-reload/smtpc.conf +4 -0
test/t-14-tls_tracking/msmtprc +0 -14
test/t-14-tls_tracking/run.sh +1 -1
test/t-14-tls_tracking/smtpc.conf +4 -0
test/t-15-driusan_dkim/config/chasquid.conf +1 -0
test/t-15-driusan_dkim/msmtprc +0 -14
test/t-15-driusan_dkim/run.sh +10 -4
test/t-16-spf/expected_dsn +0 -2
test/t-16-spf/msmtprc +0 -14
test/t-16-spf/run.sh +2 -2
test/t-16-spf/smtpc.conf +4 -0
test/t-17-maillog/msmtprc +0 -28
test/t-17-maillog/run.sh +1 -1
test/t-17-maillog/smtpc.conf +4 -0
test/t-19-dkimpy/config/chasquid.conf +1 -0
test/t-19-dkimpy/msmtprc +0 -14
test/t-19-dkimpy/run.sh +9 -3
test/t-21-dkim/A/chasquid.conf +1 -0
test/t-21-dkim/B/chasquid.conf +1 -0
test/t-21-dkim/run.sh +10 -8
test/util/lib.sh +5 -14
test/util/smtpc.py +0 -43
test/util/smtpc/smtpc.go +142 -0

diff --git a/.gitignore b/.gitignore
index 2c3ea03..408046a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ test/util/generate_cert/generate_cert
 test/util/gocovcat/gocovcat
 test/util/loadgen/loadgen
 test/util/minidns/minidns
+test/util/smtpc/smtpc
 
 # Test binary, generated during coverage tests.
 chasquid.test
diff --git a/test/t-02-exim/msmtprc b/test/t-02-exim/msmtprc
deleted file mode 100644
index 09c75b2..0000000
--- a/test/t-02-exim/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host srv-chasquid
-port 1587
-
-tls on
-tls_trust_file config/certs/srv-chasquid/fullchain.pem
-
-from user@srv-chasquid
-
-auth on
-user user@srv-chasquid
-password secretpassword
-
diff --git a/test/t-02-exim/run.sh b/test/t-02-exim/run.sh
index 314c5d9..2248e0f 100755
--- a/test/t-02-exim/run.sh
+++ b/test/t-02-exim/run.sh
@@ -10,9 +10,9 @@
 #     someone@srv-chasquid.
 #
 # Test:
-#   msmtp --> chasquid --> exim --> chasquid --> local delivery
+#   smtpc --> chasquid --> exim --> chasquid --> local delivery
 #
-#   msmtp will auth as user@srv-chasquid to chasquid, and send an email with
+#   smtpc will auth as user@srv-chasquid to chasquid, and send an email with
 #   recipient someone@srv-exim.
 #
 #   chasquid will deliver the mail to exim.
@@ -28,15 +28,15 @@ set -e
 init
 check_hostaliases
 
-if ! .exim4/exim4 --version > /dev/null; then
-	skip "exim4 binary at .exim4/exim4 is not functional"
-fi
-
 # Create a temporary directory for exim4 to use, and generate the exim4
 # config based on the template.
 mkdir -p .exim4
 EXIMDIR="$PWD/.exim4" envsubst < config/exim4.in > .exim4/config
 
+if ! .exim4/exim4 -C "$PWD/.exim4/config" --version > /dev/null; then
+	skip "exim4 binary at .exim4/exim4 is not functional"
+fi
+
 # Build with the DNS override, so we can fake DNS records.
 export GOTAGS="dnsoverride"
 
@@ -62,8 +62,8 @@ wait_until_ready 9053
 .exim4/exim4 -bd -d -C "$PWD/.exim4/config" > .exim4/log 2>&1 &
 wait_until_ready 2025
 
-# msmtp will use chasquid to send an email to someone@srv-exim.
-run_msmtp someone@srv-exim < content
+# smtpc will use chasquid to send an email to someone@srv-exim.
+smtpc someone@srv-exim < content
 
 wait_for_file .mail/someone@srv-chasquid
 
diff --git a/test/t-02-exim/smtpc.conf b/test/t-02-exim/smtpc.conf
new file mode 100644
index 0000000..c9fb836
--- /dev/null
+++ b/test/t-02-exim/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/srv-chasquid/fullchain.pem
+user user@srv-chasquid
+password secretpassword
diff --git a/test/t-04-aliases/content b/test/t-04-aliases/content
index 76a8b16..5492206 100644
--- a/test/t-04-aliases/content
+++ b/test/t-04-aliases/content
@@ -1,3 +1,4 @@
+From: user@testserver
 Subject: Prueba desde el test
 
 Crece desde el test el futuro
diff --git a/test/t-04-aliases/msmtprc b/test/t-04-aliases/msmtprc
deleted file mode 100644
index 8d191e1..0000000
--- a/test/t-04-aliases/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from user@testserver
-
-auth on
-user user@testserver
-password secretpassword
-
diff --git a/test/t-04-aliases/run.sh b/test/t-04-aliases/run.sh
index 100fed9..0b82887 100755
--- a/test/t-04-aliases/run.sh
+++ b/test/t-04-aliases/run.sh
@@ -14,7 +14,7 @@ chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
 wait_until_ready 1025
 
 function send_and_check() {
-	run_msmtp "$1@testserver" < content
+	smtpc "$1@testserver" < content
 	shift
 	for i in "$@"; do
 		wait_for_file ".mail/$i@testserver"
@@ -42,7 +42,7 @@ send_and_check añil+blah azul índigo
 
 # Test the pipe alias separately.
 rm -f .data/pipe_alias_worked
-run_msmtp tubo@testserver < content
+smtpc tubo@testserver < content
 wait_for_file .data/pipe_alias_worked
 mail_diff content .data/pipe_alias_worked
 
@@ -58,17 +58,17 @@ send_and_check vic.uña+abc uña
 
 # Test the pipe alias separately.
 rm -f .data/pipe_alias_worked
-run_msmtp ñandú@testserver < content
+smtpc ñandú@testserver < content
 wait_for_file .data/pipe_alias_worked
 mail_diff content .data/pipe_alias_worked
 
 # Test when alias-resolve exits with an error
-if run_msmtp roto@testserver < content 2> .logs/msmtp.out; then
+if smtpc roto@testserver < content 2> .logs/smtpc.out; then
 	fail "expected delivery to roto@ to fail, but succeeded"
 fi
 
 # Test a non-existent alias.
-if run_msmtp nono@testserver < content 2> .logs/msmtp.out; then
+if smtpc nono@testserver < content 2> .logs/smtpc.out; then
 	fail "expected delivery to nono@ to fail, but succeeded"
 fi
 
diff --git a/test/t-04-aliases/smtpc.conf b/test/t-04-aliases/smtpc.conf
new file mode 100644
index 0000000..c912b55
--- /dev/null
+++ b/test/t-04-aliases/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/testserver/fullchain.pem
+user user@testserver
+password secretpassword
diff --git a/test/t-05-null_address/expected_dsr b/test/t-05-null_address/expected_dsr
index 05958b3..a179b23 100644
--- a/test/t-05-null_address/expected_dsr
+++ b/test/t-05-null_address/expected_dsr
@@ -54,7 +54,6 @@ Received: from localhost
 	tls *
 	(over *
 	; *
-Date: *
 From: Mailer daemon <somewhere@báratro>
 Subject: I've come to haunt you
 Message-Id: <booooo>
diff --git a/test/t-05-null_address/msmtprc b/test/t-05-null_address/msmtprc
deleted file mode 100644
index 9322c92..0000000
--- a/test/t-05-null_address/msmtprc
+++ /dev/null
@@ -1,13 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from user@testserver
-
-auth on
-user user@testserver
-password secretpassword
diff --git a/test/t-05-null_address/run.sh b/test/t-05-null_address/run.sh
index 3638ac1..ebfab3d 100755
--- a/test/t-05-null_address/run.sh
+++ b/test/t-05-null_address/run.sh
@@ -22,7 +22,7 @@ rm -f .mail/user@testserver
 
 
 # Test that we get mail back for a failed delivery
-run_msmtp fail@testserver < content
+smtpc fail@testserver < content
 wait_for_file .mail/user@testserver
 mail_diff expected_dsr .mail/user@testserver
 
diff --git a/test/t-05-null_address/smtpc.conf b/test/t-05-null_address/smtpc.conf
new file mode 100644
index 0000000..c912b55
--- /dev/null
+++ b/test/t-05-null_address/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/testserver/fullchain.pem
+user user@testserver
+password secretpassword
diff --git a/test/t-06-idna/A/chasquid.conf b/test/t-06-idna/A/chasquid.conf
index 6f0db35..5b54c64 100644
--- a/test/t-06-idna/A/chasquid.conf
+++ b/test/t-06-idna/A/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-06-idna/B/chasquid.conf b/test/t-06-idna/B/chasquid.conf
index ef7a0da..e572536 100644
--- a/test/t-06-idna/B/chasquid.conf
+++ b/test/t-06-idna/B/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":2025"
 submission_address: ":2587"
+submission_over_tls_address: ":2465"
 monitoring_address: ":2099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-06-idna/run.sh b/test/t-06-idna/run.sh
index bc6c1bd..c3fd903 100755
--- a/test/t-06-idna/run.sh
+++ b/test/t-06-idna/run.sh
@@ -8,8 +8,6 @@ check_hostaliases
 
 rm -rf .data-A .data-B .mail
 
-skip_if_python_is_too_old
-
 # Build with the DNS override, so we can fake DNS records.
 export GOTAGS="dnsoverride"
 
@@ -37,20 +35,22 @@ chasquid -v=2 --logfile=.logs-B/chasquid.log --config_dir=B \
 	--testing__dns_addr=127.0.0.1:9053 \
 	--testing__outgoing_smtp_port=1025 &
 
-wait_until_ready 1025
-wait_until_ready 2025
+wait_until_ready 1465
+wait_until_ready 2465
 wait_until_ready 9053
 
 # Send from A to B.
-smtpc.py --server=localhost:1025 --user=nadaA@nadaA --password=nadaA \
-	< from_A_to_B
+smtpc --addr=localhost:1465 --user=nadaA@nadaA --password=nadaA \
+	--server_cert=A/certs/srv-ñ/fullchain.pem \
+	pingüino@srv-ü < from_A_to_B
 
 wait_for_file .mail/pingüino@srv-ü
 mail_diff from_A_to_B .mail/pingüino@srv-ü
 
 # Send from B to A.
-smtpc.py --server=localhost:2025 --user=nadaB@nadaB --password=nadaB \
-	< from_B_to_A
+smtpc --addr=localhost:2465 --user=nadaB@nadaB --password=nadaB \
+	--server_cert=B/certs/srv-ü/fullchain.pem \
+	ñangapirí@srv-ñ < from_B_to_A
 
 wait_for_file .mail/ñangapirí@srv-ñ
 mail_diff from_B_to_A .mail/ñangapirí@srv-ñ
diff --git a/test/t-07-smtputf8/config/chasquid.conf b/test/t-07-smtputf8/config/chasquid.conf
index 2da8942..cf76e8a 100644
--- a/test/t-07-smtputf8/config/chasquid.conf
+++ b/test/t-07-smtputf8/config/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-07-smtputf8/run.sh b/test/t-07-smtputf8/run.sh
index 5ca6d5c..54aee70 100755
--- a/test/t-07-smtputf8/run.sh
+++ b/test/t-07-smtputf8/run.sh
@@ -9,25 +9,23 @@ set -e
 
 init
 
-skip_if_python_is_too_old
-
 generate_certs_for ñoños
 
 # Intentionally have a config directory for upper case; this should be
 # normalized to lowercase internally (and match the cert accordingly).
+add_user ñandú@ñoñOS araño
 add_user ñangapirí@ñoñOS antaño
 
-# Python doesn't support UTF8 for auth, use an ascii user and domain.
-add_user nada@nada nada
-
 mkdir -p .logs
 chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
-wait_until_ready 1025
-
-# The envelope from and to are taken from the content, and use a mix of upper
-# and lower case.
-smtpc.py --server=localhost:1025 --user=nada@nada --password=nada \
-	< content
+wait_until_ready 1465
+
+# Use a mix of upper and lower case in the from, to, and username, to check
+# normalization is well handled end-to-end.
+smtpc --addr=localhost:1465 \
+	--server_cert=config/certs/ñoños/fullchain.pem \
+	--user=ñanDÚ@ñoños --password=araño \
+	Ñangapirí@Ñoños < content
 
 # The MDA should see the normalized users and domains, in lower case.
 wait_for_file .mail/ñangapirí@ñoños
diff --git a/test/t-09-loop/msmtprc b/test/t-09-loop/msmtprc
deleted file mode 100644
index a46c7eb..0000000
--- a/test/t-09-loop/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host srv-A
-port 1587
-
-tls on
-tls_trust_file A/certs/srv-A/fullchain.pem
-
-from userA@srv-A
-
-auth on
-user userA@srv-A
-password userA
-
diff --git a/test/t-09-loop/run.sh b/test/t-09-loop/run.sh
index 21561b9..8cfc04a 100755
--- a/test/t-09-loop/run.sh
+++ b/test/t-09-loop/run.sh
@@ -40,7 +40,7 @@ wait_until_ready 1025
 wait_until_ready 2025
 wait_until_ready 9053
 
-run_msmtp aliasB@srv-B < content
+smtpc aliasB@srv-B < content
 
 # Get some of the debugging pages, for troubleshooting, and to make sure they
 # work reasonably well.
diff --git a/test/t-09-loop/smtpc.conf b/test/t-09-loop/smtpc.conf
new file mode 100644
index 0000000..d451be8
--- /dev/null
+++ b/test/t-09-loop/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert A/certs/srv-A/fullchain.pem
+user userA@srv-A
+password userA
diff --git a/test/t-10-hooks/config/chasquid.conf b/test/t-10-hooks/config/chasquid.conf
index 2da8942..cf76e8a 100644
--- a/test/t-10-hooks/config/chasquid.conf
+++ b/test/t-10-hooks/config/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-10-hooks/msmtprc b/test/t-10-hooks/msmtprc
deleted file mode 100644
index 0c00237..0000000
--- a/test/t-10-hooks/msmtprc
+++ /dev/null
@@ -1,15 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from user@testserver
-
-auth on
-user user@testserver
-password secretpassword
-
-logfile .logs/msmtp
diff --git a/test/t-10-hooks/run.sh b/test/t-10-hooks/run.sh
index a29aebc..eecc3fa 100755
--- a/test/t-10-hooks/run.sh
+++ b/test/t-10-hooks/run.sh
@@ -18,7 +18,7 @@ wait_until_ready 1025
 
 cp config/hooks/post-data.good config/hooks/post-data
 
-run_msmtp someone@testserver < content
+smtpc someone@testserver < content
 
 wait_for_file .mail/someone@testserver
 
@@ -51,20 +51,20 @@ check "SPF_PASS=0"
 
 # Check that failures in the script result in failing delivery.
 # Transient failure.
-if run_msmtp blockme@testserver < content 2>/dev/null; then
+if smtpc blockme@testserver < content >.logs/smtpc.log 2>&1; then
 	fail "ERROR: hook did not block email as expected"
 fi
-if ! tail -n 1 .logs/msmtp | grep -q "smtpstatus=451"; then
-	tail -n 1 .logs/msmtp
+if ! grep -q "451 ¡No pasarán!" .logs/smtpc.log; then
+	cat .logs/smtpc.log
 	fail "ERROR: transient hook error not returned correctly"
 fi
 
 # Permanent failure.
-if run_msmtp permanent@testserver < content 2>/dev/null; then
+if smtpc permanent@testserver < content >.logs/smtpc.log 2>&1; then
 	fail "ERROR: hook did not block email as expected"
 fi
-if ! tail -n 1 .logs/msmtp | grep -q "smtpstatus=554"; then
-	tail -n 1 .logs/msmtp
+if ! grep -q "554 Nos hacemos la permanente" .logs/smtpc.log; then
+	cat .logs/smtpc.log
 	fail "ERROR: permanent hook error not returned correctly"
 fi
 
@@ -72,7 +72,7 @@ fi
 for i in config/hooks/post-data.bad*; do
 	cp "$i" config/hooks/post-data
 
-	run_msmtp someone@testserver < content
+	smtpc someone@testserver < content
 	wait_for_file .mail/someone@testserver
 	mail_diff content .mail/someone@testserver
 done
diff --git a/test/t-10-hooks/smtpc.conf b/test/t-10-hooks/smtpc.conf
new file mode 100644
index 0000000..c912b55
--- /dev/null
+++ b/test/t-10-hooks/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/testserver/fullchain.pem
+user user@testserver
+password secretpassword
diff --git a/test/t-11-dovecot/content b/test/t-11-dovecot/content
index 76a8b16..6a4d7ec 100644
--- a/test/t-11-dovecot/content
+++ b/test/t-11-dovecot/content
@@ -1,3 +1,4 @@
+From: user@srv
 Subject: Prueba desde el test
 
 Crece desde el test el futuro
diff --git a/test/t-11-dovecot/msmtprc b/test/t-11-dovecot/msmtprc
deleted file mode 100644
index 54b7338..0000000
--- a/test/t-11-dovecot/msmtprc
+++ /dev/null
@@ -1,33 +0,0 @@
-account default
-
-host srv
-port 1587
-
-tls on
-tls_trust_file config/certs/srv/fullchain.pem
-
-from user@srv
-
-auth on
-user user@srv
-password password
-
-account smtpport : default
-port 1025
-
-account subm_tls : default
-port 1465
-tls_starttls off
-
-account baduser : default
-user unknownuser@srv
-password secretpassword
-
-account badpasswd : default
-user user@srv
-password badsecretpassword
-
-account naked : default
-from naked@srv
-user naked
-password gun
diff --git a/test/t-11-dovecot/run.sh b/test/t-11-dovecot/run.sh
index 51fbeb8..7fda6de 100755
--- a/test/t-11-dovecot/run.sh
+++ b/test/t-11-dovecot/run.sh
@@ -52,33 +52,33 @@ chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
 wait_until_ready 1025
 
 # Send an email as "user@srv" successfully.
-run_msmtp user@srv < content
+smtpc user@srv < content
 wait_for_file .mail/user@srv
 mail_diff content .mail/user@srv
 
 # Send an email as "naked" successfully.
 rm .mail/user@srv
-run_msmtp -a naked user@srv < content
+smtpc --user=naked --password=gun --from=naked@srv user@srv < content
 wait_for_file .mail/user@srv
 mail_diff content .mail/user@srv
 
 # Send an email to the "naked" user successfully.
-run_msmtp naked@srv < content
+smtpc naked@srv < content
 wait_for_file .mail/naked@srv
 mail_diff content .mail/naked@srv
 
 # Fail to send to nobody@srv (user does not exist).
-if run_msmtp nobody@srv < content 2> /dev/null; then
+if smtpc nobody@srv < content 2> /dev/null; then
 	fail "successfully sent an email to a non-existent user"
 fi
 
-# Fail to send from baduser@srv (user does not exist).
-if run_msmtp -a baduser user@srv < content 2> /dev/null; then
+# Fail to send from unknownuser@srv (user does not exist).
+if smtpc --user=unknownuser@srv user@srv < content 2> /dev/null; then
 	fail "successfully sent an email with a bad user"
 fi
 
 # Fail to send with an incorrect password.
-if run_msmtp -a badpasswd user@srv < content 2> /dev/null; then
+if smtpc --password=badpasswd user@srv < content 2> /dev/null; then
 	fail "successfully sent an email with a bad password"
 fi
 
diff --git a/test/t-11-dovecot/smtpc.conf b/test/t-11-dovecot/smtpc.conf
new file mode 100644
index 0000000..6d65738
--- /dev/null
+++ b/test/t-11-dovecot/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/srv/fullchain.pem
+user user@srv
+password password
diff --git a/test/t-13-reload/config/chasquid.conf b/test/t-13-reload/config/chasquid.conf
index 2da8942..cf76e8a 100644
--- a/test/t-13-reload/config/chasquid.conf
+++ b/test/t-13-reload/config/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-13-reload/msmtprc b/test/t-13-reload/msmtprc
deleted file mode 100644
index f5e3076..0000000
--- a/test/t-13-reload/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from someone@testserver
-
-auth on
-user someone@testserver
-password password222
-
diff --git a/test/t-13-reload/run.sh b/test/t-13-reload/run.sh
index 00db0ed..0fcb2fc 100755
--- a/test/t-13-reload/run.sh
+++ b/test/t-13-reload/run.sh
@@ -22,7 +22,7 @@ chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config \
 wait_until_ready 1025
 
 # First, check that delivery fails with the "wrong" password.
-if run_msmtp someone@testserver < content 2>/dev/null; then
+if smtpc someone@testserver < content 2>/dev/null; then
 	fail "success using the wrong password"
 fi
 
@@ -32,7 +32,7 @@ chasquid-util-user-add someone@testserver password222
 echo "analias: someone" > config/domains/testserver/aliases
 sleep 0.2
 
-run_msmtp analias@testserver < content
+smtpc analias@testserver < content
 wait_for_file .mail/someone@testserver
 
 
@@ -50,7 +50,7 @@ sleep 0.2
 
 # Send another mail.
 rm .mail/someone@testserver
-run_msmtp analias@testserver < content
+smtpc analias@testserver < content
 wait_for_file .mail/someone@testserver
 
 # Check there are new entries.
diff --git a/test/t-13-reload/smtpc.conf b/test/t-13-reload/smtpc.conf
new file mode 100644
index 0000000..c555788
--- /dev/null
+++ b/test/t-13-reload/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/testserver/fullchain.pem
+user someone@testserver
+password password222
diff --git a/test/t-14-tls_tracking/msmtprc b/test/t-14-tls_tracking/msmtprc
deleted file mode 100644
index a46c7eb..0000000
--- a/test/t-14-tls_tracking/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host srv-A
-port 1587
-
-tls on
-tls_trust_file A/certs/srv-A/fullchain.pem
-
-from userA@srv-A
-
-auth on
-user userA@srv-A
-password userA
-
diff --git a/test/t-14-tls_tracking/run.sh b/test/t-14-tls_tracking/run.sh
index 95af651..e48b1d5 100755
--- a/test/t-14-tls_tracking/run.sh
+++ b/test/t-14-tls_tracking/run.sh
@@ -45,7 +45,7 @@ wait_until_ready 1025
 wait_until_ready 2025
 wait_until_ready 9053
 
-run_msmtp userB@srv-B < content
+smtpc userB@srv-B < content
 
 wait_for_file .mail/userb@srv-b
 mail_diff content .mail/userb@srv-b
diff --git a/test/t-14-tls_tracking/smtpc.conf b/test/t-14-tls_tracking/smtpc.conf
new file mode 100644
index 0000000..d451be8
--- /dev/null
+++ b/test/t-14-tls_tracking/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert A/certs/srv-A/fullchain.pem
+user userA@srv-A
+password userA
diff --git a/test/t-15-driusan_dkim/config/chasquid.conf b/test/t-15-driusan_dkim/config/chasquid.conf
index 2da8942..cf76e8a 100644
--- a/test/t-15-driusan_dkim/config/chasquid.conf
+++ b/test/t-15-driusan_dkim/config/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-15-driusan_dkim/msmtprc b/test/t-15-driusan_dkim/msmtprc
deleted file mode 100644
index 8d191e1..0000000
--- a/test/t-15-driusan_dkim/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from user@testserver
-
-auth on
-user user@testserver
-password secretpassword
-
diff --git a/test/t-15-driusan_dkim/run.sh b/test/t-15-driusan_dkim/run.sh
index 0c23eb7..7074eee 100755
--- a/test/t-15-driusan_dkim/run.sh
+++ b/test/t-15-driusan_dkim/run.sh
@@ -23,11 +23,14 @@ add_user someone@testserver secretpassword
 
 mkdir -p .logs
 chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config &
-wait_until_ready 1025
+wait_until_ready 1465
 
 # Authenticated: user@testserver -> someone@testserver
 # Should be signed.
-run_msmtp someone@testserver < content
+smtpc --addr=localhost:1465 \
+	--server_cert=config/certs/testserver/fullchain.pem \
+	--user=user@testserver --password=secretpassword \
+	someone@testserver < content
 wait_for_file .mail/someone@testserver
 mail_diff content .mail/someone@testserver
 grep -q "DKIM-Signature:" .mail/someone@testserver
@@ -40,11 +43,14 @@ dkimverify -txt .dkimcerts/dns.txt < .mail/someone@testserver
 tail -n +2 .mail/someone@testserver > .signed_content
 
 # Not authenticated: someone@testserver -> someone@testserver
-smtpc.py --server=localhost:1025 < .signed_content
+smtpc --addr=localhost:1025 \
+	--from=someone@testserver someone@testserver < .signed_content
 
 # Check that the signature fails on modified content.
 echo "Added content, invalid and not signed" >> .signed_content
-if smtpc.py --server=localhost:1025 < .signed_content 2> /dev/null; then
+if smtpc --addr=localhost:1025 \
+	--from=someone@testserver someone@testserver < .signed_content \
+	> /dev/null 2>&1 ; then
 	fail "DKIM verification succeeded on modified content"
 fi
 
diff --git a/test/t-16-spf/expected_dsn b/test/t-16-spf/expected_dsn
index 0d3df60..840632b 100644
--- a/test/t-16-spf/expected_dsn
+++ b/test/t-16-spf/expected_dsn
@@ -54,8 +54,6 @@ Received: from localhost
 	tls *
 	(over *
 	; *
-From: userA@srv-A
-Date: *
 Subject: Prueba desde el test
 
 Crece desde el test el futuro
diff --git a/test/t-16-spf/msmtprc b/test/t-16-spf/msmtprc
deleted file mode 100644
index a46c7eb..0000000
--- a/test/t-16-spf/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host srv-A
-port 1587
-
-tls on
-tls_trust_file A/certs/srv-A/fullchain.pem
-
-from userA@srv-A
-
-auth on
-user userA@srv-A
-password userA
-
diff --git a/test/t-16-spf/run.sh b/test/t-16-spf/run.sh
index 6a93d43..3e58844 100755
--- a/test/t-16-spf/run.sh
+++ b/test/t-16-spf/run.sh
@@ -54,7 +54,7 @@ function launch_minidns() {
 
 # T0: Successful.
 launch_minidns zones.t0
-run_msmtp userB@srv-B < content
+smtpc userB@srv-B < content
 wait_for_file .mail/userb@srv-b
 mail_diff content .mail/userb@srv-b
 
@@ -62,7 +62,7 @@ mail_diff content .mail/userb@srv-b
 # Check that userA got a DSN about it.
 rm .mail/*
 launch_minidns zones.t1
-run_msmtp userB@srv-B < content
+smtpc userB@srv-B < content
 wait_for_file .mail/usera@srv-a
 mail_diff expected_dsn .mail/usera@srv-a
 
diff --git a/test/t-16-spf/smtpc.conf b/test/t-16-spf/smtpc.conf
new file mode 100644
index 0000000..d451be8
--- /dev/null
+++ b/test/t-16-spf/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert A/certs/srv-A/fullchain.pem
+user userA@srv-A
+password userA
diff --git a/test/t-17-maillog/msmtprc b/test/t-17-maillog/msmtprc
deleted file mode 100644
index eed8751..0000000
--- a/test/t-17-maillog/msmtprc
+++ /dev/null
@@ -1,28 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from user@testserver
-
-auth on
-user user@testserver
-password secretpassword
-
-account smtpport : default
-port 1025
-
-account subm_tls : default
-port 1465
-tls_starttls off
-
-account baduser : default
-user unknownuser@testserver
-password secretpassword
-
-account badpasswd : default
-user user@testserver
-password badsecretpassword
diff --git a/test/t-17-maillog/run.sh b/test/t-17-maillog/run.sh
index 43dd1be..ca9026c 100755
--- a/test/t-17-maillog/run.sh
+++ b/test/t-17-maillog/run.sh
@@ -20,7 +20,7 @@ function send_one() {
 		> .logs/stdout 2> .logs/stderr &
 	wait_until_ready 1025
 
-	run_msmtp someone@testserver < content
+	smtpc someone@testserver < content
 	wait_for_file .mail/someone@testserver
 	mail_diff content .mail/someone@testserver
 
diff --git a/test/t-17-maillog/smtpc.conf b/test/t-17-maillog/smtpc.conf
new file mode 100644
index 0000000..c912b55
--- /dev/null
+++ b/test/t-17-maillog/smtpc.conf
@@ -0,0 +1,4 @@
+addr localhost:1465
+server_cert config/certs/testserver/fullchain.pem
+user user@testserver
+password secretpassword
diff --git a/test/t-19-dkimpy/config/chasquid.conf b/test/t-19-dkimpy/config/chasquid.conf
index 2da8942..cf76e8a 100644
--- a/test/t-19-dkimpy/config/chasquid.conf
+++ b/test/t-19-dkimpy/config/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-19-dkimpy/msmtprc b/test/t-19-dkimpy/msmtprc
deleted file mode 100644
index 8d191e1..0000000
--- a/test/t-19-dkimpy/msmtprc
+++ /dev/null
@@ -1,14 +0,0 @@
-account default
-
-host testserver
-port 1587
-
-tls on
-tls_trust_file config/certs/testserver/fullchain.pem
-
-from user@testserver
-
-auth on
-user user@testserver
-password secretpassword
-
diff --git a/test/t-19-dkimpy/run.sh b/test/t-19-dkimpy/run.sh
index 937d2a2..7d882b3 100755
--- a/test/t-19-dkimpy/run.sh
+++ b/test/t-19-dkimpy/run.sh
@@ -47,7 +47,10 @@ wait_until_ready 1025
 
 # Authenticated: user@testserver -> someone@testserver
 # Should be signed.
-run_msmtp someone@testserver < content
+smtpc --addr=localhost:1465 \
+	--server_cert=config/certs/testserver/fullchain.pem \
+	--user=user@testserver --password=secretpassword \
+	someone@testserver < content
 wait_for_file .mail/someone@testserver
 mail_diff content .mail/someone@testserver
 if ! grep -q "DKIM-Signature:" .mail/someone@testserver; then
@@ -65,11 +68,14 @@ dkimverify -txt .dkimcerts/private.dns < .mail/someone@testserver
 tail -n +2 .mail/someone@testserver > .signed_content
 
 # Not authenticated: someone@testserver -> someone@testserver
-smtpc.py --server=localhost:1025 < .signed_content
+smtpc --addr=localhost:1025 \
+	--from=someone@testserver someone@testserver < .signed_content
 
 # Check that the signature fails on modified content.
 echo "Added content, invalid and not signed" >> .signed_content
-if smtpc.py --server=localhost:1025 < .signed_content 2> /dev/null; then
+if smtpc --addr=localhost:1025 \
+	--from=someone@testserver someone@testserver < .signed_content \
+	> /dev/null 2>&1 ; then
 	fail "DKIM verification succeeded on modified content"
 fi
 
diff --git a/test/t-21-dkim/A/chasquid.conf b/test/t-21-dkim/A/chasquid.conf
index 6c6e987..3120200 100644
--- a/test/t-21-dkim/A/chasquid.conf
+++ b/test/t-21-dkim/A/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":1025"
 submission_address: ":1587"
+submission_over_tls_address: ":1465"
 monitoring_address: ":1099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-21-dkim/B/chasquid.conf b/test/t-21-dkim/B/chasquid.conf
index 2e37697..7cd0d88 100644
--- a/test/t-21-dkim/B/chasquid.conf
+++ b/test/t-21-dkim/B/chasquid.conf
@@ -1,5 +1,6 @@
 smtp_address: ":2025"
 submission_address: ":2587"
+submission_over_tls_address: ":2465"
 monitoring_address: ":2099"
 
 mail_delivery_agent_bin: "test-mda"
diff --git a/test/t-21-dkim/run.sh b/test/t-21-dkim/run.sh
index c395dce..c4f289d 100755
--- a/test/t-21-dkim/run.sh
+++ b/test/t-21-dkim/run.sh
@@ -8,8 +8,6 @@ check_hostaliases
 
 rm -rf .data-A .data-B .mail
 
-skip_if_python_is_too_old
-
 # Build with the DNS override, so we can fake DNS records.
 export GOTAGS="dnsoverride"
 
@@ -45,20 +43,24 @@ chasquid -v=2 --logfile=.logs-B/chasquid.log --config_dir=B \
 	--testing__dns_addr=127.0.0.1:9053 \
 	--testing__outgoing_smtp_port=1025 &
 
-wait_until_ready 1025
-wait_until_ready 2025
+wait_until_ready 1465
+wait_until_ready 2465
 wait_until_ready 9053
 
 # Send from A to B.
-smtpc.py --server=localhost:1025 --user=user-a@srv-a --password=nadaA \
-	< from_A_to_B
+smtpc --addr=localhost:1465 \
+	--server_cert=A/certs/srv-A/fullchain.pem \
+	--user=user-a@srv-a --password=nadaA \
+	user-b@srv-b < from_A_to_B
 
 wait_for_file .mail/user-b@srv-b
 mail_diff from_A_to_B.expected .mail/user-b@srv-b
 
 # Send from B to A.
-smtpc.py --server=localhost:2025 --user=user-b@srv-b --password=nadaB \
-	< from_B_to_A
+smtpc --addr=localhost:2465 \
+	--server_cert=B/certs/srv-B/fullchain.pem \
+	--user=user-b@srv-b --password=nadaB \
+	user-a@srv-a < from_B_to_A
 
 wait_for_file .mail/user-a@srv-a
 mail_diff from_B_to_A.expected .mail/user-a@srv-a
diff --git a/test/util/lib.sh b/test/util/lib.sh
index 3cdba0a..14239e7 100644
--- a/test/util/lib.sh
+++ b/test/util/lib.sh
@@ -120,10 +120,6 @@ function run_msmtp() {
 		"${UTILDIR}/.msmtp-bin" -C msmtprc "$@"
 }
 
-function smtpc.py() {
-	"${UTILDIR}/smtpc.py" "$@"
-}
-
 function mail_diff() {
 	"${UTILDIR}/mail_diff" "$@"
 }
@@ -158,6 +154,11 @@ function fexp() {
 	"${UTILDIR}/fexp/fexp" "$@"
 }
 
+function smtpc() {
+	go-build-cached "${UTILDIR}/smtpc/"
+	"${UTILDIR}/smtpc/smtpc" "$@"
+}
+
 function timeout() {
 	MYPID=$$
 	(
@@ -232,16 +233,6 @@ function generate_certs_for() {
 	cp -p "${CACHEDIR}/$1"/* "${CONFDIR}/certs/$1/"
 }
 
-# Check the Python version, and skip if it's too old.
-# This will check against the version required for smtpc.py.
-function skip_if_python_is_too_old() {
-	# We need Python >= 3.5 to be able to use SMTPUTF8.
-	check='import sys; sys.exit(0 if sys.version_info >= (3, 5) else 1)'
-	if ! python3 -c "${check}" > /dev/null 2>&1; then
-		skip "python3 >= 3.5 not available"
-	fi
-}
-
 function chasquid_ram_peak() {
 	# Find the pid of the daemon, which we expect is running on the
 	# background somewhere within our current session.
diff --git a/test/util/smtpc.py b/test/util/smtpc.py
deleted file mode 100755
index bca1c13..0000000
--- a/test/util/smtpc.py
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env python3
-#
-# Simple SMTP client for testing purposes.
-
-import argparse
-import email.parser
-import email.policy
-import re
-import smtplib
-import sys
-
-ap = argparse.ArgumentParser()
-ap.add_argument("--server", help="SMTP server to connect to")
-ap.add_argument("--user", help="Username to use in SMTP AUTH")
-ap.add_argument("--password", help="Password to use in SMTP AUTH")
-args = ap.parse_args()
-
-# Parse the email using the "default" policy, which is not really the default.
-# If unspecified, compat32 is used, which does not support UTF8.
-rawmsg = sys.stdin.buffer.read()
-msg = email.parser.Parser(policy=email.policy.default).parsestr(
-        rawmsg.decode('utf8'))
-
-s = smtplib.SMTP(args.server)
-s.starttls()
-if args.user:
-    s.login(args.user, args.password)
-
-# Send the raw message, not parsed, because the parser does not handle some
-# corner cases that well (for example, DKIM-Signature headers get mime-encoded
-# incorrectly).
-# Replace \n with \r\n, which is normally done by the library, but will not do
-# it in this case because we are giving it bytes and not a string (which we
-# cannot do because it tries to incorrectly escape the headers).
-crlfmsg = re.sub(br'(?:\r\n|\n|\r(?!\n))', b"\r\n", rawmsg)
-
-s.sendmail(
-        from_addr=msg['from'], to_addrs=msg.get_all('to'),
-        msg=crlfmsg,
-        mail_options=['SMTPUTF8'])
-s.quit()
-
-
diff --git a/test/util/smtpc/smtpc.go b/test/util/smtpc/smtpc.go
new file mode 100644
index 0000000..9b85312
--- /dev/null
+++ b/test/util/smtpc/smtpc.go
@@ -0,0 +1,142 @@
+package main
+
+import (
+	"bytes"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+	"flag"
+	"io"
+	"net"
+	"net/smtp"
+	"os"
+	"strings"
+)
+
+var (
+	addr = flag.String("addr", "", "Address of the SMTP server")
+
+	user     = flag.String("user", "", "Username to use in SMTP AUTH")
+	password = flag.String("password", "", "Password to use in SMTP AUTH")
+
+	from = flag.String("from", "", "From address to use in the message")
+
+	serverCert = flag.String("server_cert", "",
+		"Path to the server certificate to expect")
+
+	confPath = flag.String("c", "smtpc.conf",
+		"Path to the configuration file")
+)
+
+func main() {
+	flag.Parse()
+	loadConfig()
+
+	// Read message from stdin.
+	rawMsg, err := io.ReadAll(os.Stdin)
+	notnil(err)
+
+	// RCPT TO from the command line.
+	tos := make([]string, len(flag.Args()))
+	for i, to := range flag.Args() {
+		tos[i] = to
+	}
+
+	// Connect to the server.
+	var conn net.Conn
+	if *serverCert != "" {
+		cert := loadCert(*serverCert)
+		rootCAs := x509.NewCertPool()
+
+		rootCAs.AddCert(cert)
+		tlsConfig := &tls.Config{
+			ServerName: cert.DNSNames[0],
+			RootCAs:    rootCAs,
+		}
+
+		conn, err = tls.Dial("tcp", *addr, tlsConfig)
+		defer conn.Close()
+	} else {
+		conn, err = net.Dial("tcp", *addr)
+	}
+	notnil(err)
+
+	// Send the message.
+	client, err := smtp.NewClient(conn, *addr)
+	notnil(err)
+
+	if *user != "" {
+		auth := smtp.PlainAuth("", *user, *password, *addr)
+		err = client.Auth(auth)
+		notnil(err)
+	}
+
+	if *from == "" {
+		*from = *user
+	}
+	err = client.Mail(*from)
+	notnil(err)
+
+	for _, to := range tos {
+		err = client.Rcpt(to)
+		notnil(err)
+	}
+
+	w, err := client.Data()
+	notnil(err)
+	_, err = io.Copy(w, bytes.NewReader(rawMsg))
+	notnil(err)
+	err = w.Close()
+	notnil(err)
+
+	err = client.Quit()
+	notnil(err)
+}
+
+func loadConfig() {
+	data, err := os.ReadFile(*confPath)
+	if errors.Is(err, os.ErrNotExist) {
+		return
+	}
+	notnil(err)
+
+	for _, line := range strings.Split(string(data), "\n") {
+		k, v, ok := strings.Cut(line, " ")
+		if !ok {
+			continue
+		}
+
+		k = strings.TrimSpace(k)
+
+		// Set the flag but only if it wasn't already set.
+		// Command-line flags take precedence.
+		isSet := false
+		flag.Visit(func(f *flag.Flag) {
+			if f.Name == k {
+				isSet = true
+			}
+		})
+		if !isSet {
+			flag.Lookup(k).Value.Set(strings.TrimSpace(v))
+		}
+	}
+}
+
+func loadCert(path string) *x509.Certificate {
+	data, err := os.ReadFile(path)
+	notnil(err)
+
+	block, _ := pem.Decode(data)
+
+	cert, err := x509.ParseCertificate(block.Bytes)
+	notnil(err)
+
+	return cert
+}
+
+func notnil(err error) {
+	if err != nil {
+		panic(err)
+	}
+}