Compare commits

..

15 Commits

Author SHA1 Message Date
holger krekel
b441891d1f add postfix instrumented debugging 2023-10-16 22:38:36 +02:00
holger krekel
1c37d1f59b add global debug flag and instrument dovecot with it 2023-10-16 22:29:40 +02:00
missytake
192238567b add some initial benchmarks
Co-Authored-By: holger krekel <holger@merlinux.eu>
2023-10-16 21:51:53 +02:00
holger krekel
c35e485510 an empty message in the handler means EOF 2023-10-16 21:49:56 +02:00
holger krekel
1bac4b5b46 generalize remotelog to "remote" and offer remote.iter_output method 2023-10-16 20:49:30 +02:00
holger krekel
63a7ad82ff fix capturing of logging to capture postfix better 2023-10-16 20:49:30 +02:00
holger krekel
37ef3f13b4 fix bugs 2023-10-16 20:49:30 +02:00
holger krekel
9dfd0ceb5a simplify and speedup multi-chatmail instance support 2023-10-16 20:49:30 +02:00
holger krekel
55c58e3c7a add support for using a second chatmail server 2023-10-16 20:49:30 +02:00
holger krekel
c2692c7e92 introduce remotelog fixture for capturing systemd-unit logs 2023-10-16 20:49:30 +02:00
missytake
ea5eccf377 plan: seen messages should be expunged, too 2023-10-16 17:56:51 +02:00
missytake
c9ecf24b3e dovecot: expunge seen messages older than 40 days each night 2023-10-16 17:56:51 +02:00
holger krekel
b943f24587 apply nami's suggestions (chatmail SSH env var, running --slow in test.sh) 2023-10-16 17:52:08 +02:00
holger krekel
df00333a19 also show the chatmail instance prominently in the test header 2023-10-16 17:52:08 +02:00
holger krekel
4fc63461fb - introduce pytest.mark.slow marker and "--slow" CLI option
- refactor login tests to allow running them against both imap/smtp
2023-10-16 17:52:08 +02:00
14 changed files with 139 additions and 32 deletions

View File

@@ -52,6 +52,7 @@ scripts/
init.sh # create venv/other perequires init.sh # create venv/other perequires
deploy.sh # run pyinfra based deploy of everything deploy.sh # run pyinfra based deploy of everything
test.sh # run all local and online tests test.sh # run all local and online tests
bench.sh # run performance benchmark tests
``` ```

View File

@@ -95,7 +95,7 @@ def main():
while True: while True:
msg = self.rfile.readline().strip().decode() msg = self.rfile.readline().strip().decode()
if not msg: if not msg:
continue break
res = handle_dovecot_request(msg, db) res = handle_dovecot_request(msg, db)
if res: if res:
print(f"sending result: {res!r}", file=sys.stderr) print(f"sending result: {res!r}", file=sys.stderr)

View File

@@ -110,7 +110,7 @@ def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
return need_restart return need_restart
def _configure_postfix(domain: str) -> bool: def _configure_postfix(domain: str, debug: bool = False) -> bool:
"""Configures Postfix SMTP server.""" """Configures Postfix SMTP server."""
need_restart = False need_restart = False
@@ -124,21 +124,20 @@ def _configure_postfix(domain: str) -> bool:
) )
need_restart |= main_config.changed need_restart |= main_config.changed
master_config = files.put( master_config = files.template(
src=importlib.resources.files(__package__) src=importlib.resources.files(__package__).joinpath("postfix/master.cf.j2"),
.joinpath("postfix/master.cf")
.open("rb"),
dest="/etc/postfix/master.cf", dest="/etc/postfix/master.cf",
user="root", user="root",
group="root", group="root",
mode="644", mode="644",
debug=debug,
) )
need_restart |= master_config.changed need_restart |= master_config.changed
return need_restart return need_restart
def _configure_dovecot(mail_server: str) -> bool: def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
"""Configures Dovecot IMAP server.""" """Configures Dovecot IMAP server."""
need_restart = False need_restart = False
@@ -149,6 +148,7 @@ def _configure_dovecot(mail_server: str) -> bool:
group="root", group="root",
mode="644", mode="644",
config={"hostname": mail_server}, config={"hostname": mail_server},
debug=debug,
) )
need_restart |= main_config.changed need_restart |= main_config.changed
auth_config = files.put( auth_config = files.put(
@@ -160,6 +160,16 @@ def _configure_dovecot(mail_server: str) -> bool:
) )
need_restart |= auth_config.changed need_restart |= auth_config.changed
files.put(
src=importlib.resources.files(__package__)
.joinpath("dovecot/expunge.cron")
.open("rb"),
dest="/etc/cron.d/expunge",
user="root",
group="root",
mode="644",
)
return need_restart return need_restart
@@ -205,8 +215,9 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
) )
_install_chatmaild() _install_chatmaild()
dovecot_need_restart = _configure_dovecot(mail_server) debug = False
postfix_need_restart = _configure_postfix(mail_domain) dovecot_need_restart = _configure_dovecot(mail_server, debug=debug)
postfix_need_restart = _configure_postfix(mail_domain, debug=debug)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector) opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
systemd.service( systemd.service(

View File

@@ -4,13 +4,17 @@ protocols = imap lmtp
auth_mechanisms = plain auth_mechanisms = plain
{% if debug == true %}
auth_verbose = yes auth_verbose = yes
auth_debug = yes auth_debug = yes
auth_debug_passwords = yes auth_debug_passwords = yes
auth_verbose_passwords = plain auth_verbose_passwords = plain
auth_cache_size = 100M auth_cache_size = 100M
mail_plugins = quota
mail_debug = yes mail_debug = yes
{% endif %}
mail_plugins = quota
# Authentication for system users. # Authentication for system users.
passdb { passdb {

View File

@@ -0,0 +1,4 @@
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE 40d INBOX
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE 40d Deltachat
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE 40d Trash
2 30 * * * dovecot doveadm purge -A

View File

@@ -9,7 +9,11 @@
# service type private unpriv chroot wakeup maxproc command + args # service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100) # (yes) (yes) (no) (never) (100)
# ========================================================================== # ==========================================================================
smtp inet n - y - - smtpd -v {% if debug == true %}
smtp inet n - y - - smtpd -v
{% else %}
smtp inet n - y - - smtpd
{% endif %}
#smtp inet n - y - 1 postscreen #smtp inet n - y - 1 postscreen
#smtpd pass - - y - - smtpd #smtpd pass - - y - - smtpd
#dnsblog unix - - y - 0 dnsblog #dnsblog unix - - y - 0 dnsblog

34
online-tests/benchmark.py Normal file
View File

@@ -0,0 +1,34 @@
def test_tls_serialized_connect(benchmark, imap_or_smtp):
def connect():
imap_or_smtp.connect()
benchmark(connect)
def test_login(benchmark, imap_or_smtp, gencreds):
cls = imap_or_smtp.__class__
conns = []
for i in range(20):
conn = cls(imap_or_smtp.host)
conn.connect()
conns.append(conn)
def login():
conn = conns.pop()
conn.login(*gencreds())
benchmark(login)
def test_send_and_receive_10(benchmark, cmfactory, lp):
"""send many messages between two accounts"""
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
def send_10_receive_all():
for i in range(10):
chat.send_text(f"hello {i}")
for i in range(10):
ac2.wait_next_incoming_message()
benchmark(send_10_receive_all)

View File

@@ -30,13 +30,23 @@ def maildomain():
@pytest.fixture @pytest.fixture
def chatmail_ssh(maildomain): def sshdomain(maildomain):
domain = os.environ.get("CHATMAIL_SSH") return os.environ.get("CHATMAIL_SSH", maildomain)
@pytest.fixture
def maildomain2():
domain = os.environ.get("CHATMAIL_DOMAIN2")
if not domain: if not domain:
domain = maildomain pytest.skip("set CHATMAIL_DOMAIN2 to a ssh-reachable chatmail instance")
return domain return domain
@pytest.fixture
def sshdomain2(maildomain2):
return os.environ.get("CHATMAIL_SSH2", maildomain2)
def pytest_report_header(): def pytest_report_header():
domain = os.environ.get("CHATMAIL_DOMAIN") domain = os.environ.get("CHATMAIL_DOMAIN")
if domain: if domain:
@@ -51,6 +61,8 @@ def imap(maildomain):
class ImapConn: class ImapConn:
AuthError = imaplib.IMAP4.error AuthError = imaplib.IMAP4.error
logcmd = "journalctl -f -u dovecot"
name = "dovecot"
def __init__(self, host): def __init__(self, host):
self.host = host self.host = host
@@ -71,6 +83,8 @@ def smtp(maildomain):
class SmtpConn: class SmtpConn:
AuthError = smtplib.SMTPAuthenticationError AuthError = smtplib.SMTPAuthenticationError
logcmd = "journalctl -f -t postfix/smtpd -t postfix/smtp -t postfix/lmtp"
name = "postfix"
def __init__(self, host): def __init__(self, host):
self.host = host self.host = host
@@ -94,16 +108,17 @@ def gencreds(maildomain):
count = itertools.count() count = itertools.count()
next(count) next(count)
def gen(): def gen(domain=None):
domain = domain if domain else maildomain
while 1: while 1:
num = next(count) num = next(count)
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join(random.choices(alphanumeric, k=10)) user = "".join(random.choices(alphanumeric, k=10))
user = f"ac{num}_{user}" user = f"ac{num}_{user}"
password = "".join(random.choices(alphanumeric, k=10)) password = "".join(random.choices(alphanumeric, k=10))
yield f"{user}@{maildomain}", f"{password}" yield f"{user}@{domain}", f"{password}"
return lambda: next(gen()) return lambda domain=None: next(gen(domain))
# #
@@ -118,12 +133,13 @@ class ChatmailTestProcess:
def __init__(self, pytestconfig, maildomain, gencreds): def __init__(self, pytestconfig, maildomain, gencreds):
self.pytestconfig = pytestconfig self.pytestconfig = pytestconfig
self.maildomain = maildomain self.maildomain = maildomain
assert "." in self.maildomain, maildomain
self.gencreds = gencreds self.gencreds = gencreds
self._addr2files = {} self._addr2files = {}
def get_liveconfig_producer(self): def get_liveconfig_producer(self):
while 1: while 1:
user, password = self.gencreds() user, password = self.gencreds(self.maildomain)
config = { config = {
"addr": user, "addr": user,
"mail_pw": password, "mail_pw": password,
@@ -141,13 +157,21 @@ class ChatmailTestProcess:
@pytest.fixture @pytest.fixture
def cmfactory(request, maildomain, gencreds, tmpdir, data): def cmfactory(request, gencreds, tmpdir, data, maildomain):
# cloned from deltachat.testplugin.amfactory # cloned from deltachat.testplugin.amfactory
pytest.importorskip("deltachat") pytest.importorskip("deltachat")
from deltachat.testplugin import ACFactory from deltachat.testplugin import ACFactory
testproc = ChatmailTestProcess(request.config, maildomain, gencreds) testproc = ChatmailTestProcess(request.config, maildomain, gencreds)
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testproc, data=data) am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testproc, data=data)
# nb. a bit hacky
# would probably be better if deltachat's test machinery grows native support
def switch_maildomain(maildomain2):
am.testprocess.maildomain = maildomain2
am.switch_maildomain = switch_maildomain
yield am yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed: if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
if testproc.pytestconfig.getoption("--extra-info"): if testproc.pytestconfig.getoption("--extra-info"):
@@ -158,13 +182,20 @@ def cmfactory(request, maildomain, gencreds, tmpdir, data):
@pytest.fixture @pytest.fixture
def dovelogreader(chatmail_ssh): def remote(sshdomain):
def remote_reader(): return Remote(sshdomain)
popen = subprocess.Popen(
["ssh", f"root@{chatmail_ssh}", "journalctl -f -u dovecot"],
class Remote:
def __init__(self, sshdomain):
self.sshdomain = sshdomain
def iter_output(self, logcmd=""):
getjournal = f"journalctl -f" if not logcmd else logcmd
self.popen = subprocess.Popen(
["ssh", f"root@{self.sshdomain}", getjournal],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
) )
while 1: while 1:
yield popen.stdout.readline() line = self.popen.stdout.readline()
yield line.decode().strip().lower()
return remote_reader

View File

@@ -0,0 +1,15 @@
def test_remote(remote, imap_or_smtp):
lineproducer = remote.iter_output(imap_or_smtp.logcmd)
imap_or_smtp.connect()
assert imap_or_smtp.name in next(lineproducer)
def test_use_two_chatmailservers(cmfactory, maildomain2):
ac1 = cmfactory.new_online_configuring_account(cache=False)
cmfactory.switch_maildomain(maildomain2)
ac2 = cmfactory.new_online_configuring_account(cache=False)
cmfactory.bring_accounts_online()
cmfactory.get_accepted_chat(ac1, ac2)
domain1 = ac1.get_config("addr").split("@")[1]
domain2 = ac2.get_config("addr").split("@")[1]
assert domain1 != domain2

View File

@@ -17,7 +17,7 @@ def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
imap_or_smtp.connect() imap_or_smtp.connect()
lp.sec("success") lp.sec("success")
lp.sec("reconnect and verify wrong password fails {user} ") lp.sec(f"reconnect and verify wrong password fails {user} ")
imap_or_smtp.connect() imap_or_smtp.connect()
with pytest.raises(imap_or_smtp.AuthError): with pytest.raises(imap_or_smtp.AuthError):
imap_or_smtp.login(user, password + "wrong") imap_or_smtp.login(user, password + "wrong")

View File

@@ -19,7 +19,7 @@ class TestEndToEndDeltaChat:
assert msg2.text == "message0" assert msg2.text == "message0"
@pytest.mark.slow @pytest.mark.slow
def test_exceed_quota(self, cmfactory, lp, tmpdir, dovelogreader): def test_exceed_quota(self, cmfactory, lp, tmpdir, remote):
"""This is a very slow test as it needs to upload >100MB of mail data """This is a very slow test as it needs to upload >100MB of mail data
before quota is exceeded, and thus depends on the speed of the upload. before quota is exceeded, and thus depends on the speed of the upload.
""" """
@@ -48,8 +48,7 @@ class TestEndToEndDeltaChat:
addr = ac2.get_config("addr").lower() addr = ac2.get_config("addr").lower()
saved_ok = 0 saved_ok = 0
for line in dovelogreader(): for line in remote.iter_output("journalctl -f -u dovecot"):
line = line.decode().lower().strip()
if addr not in line: if addr not in line:
# print(line) # print(line)
continue continue

View File

@@ -3,7 +3,7 @@
## Dovecot goals/steps ## Dovecot goals/steps
- automatic expiry of messages older than M days - automatic expiry of messages older than M days
- delete unconditionally messages older than 40 days - also expunge unread messages
- limit: configure max-connections per account - limit: configure max-connections per account

4
scripts/bench.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
online-tests/venv/bin/pytest online-tests/benchmark.py -vrx

View File

@@ -10,7 +10,7 @@ chatmaild/venv/bin/pip install pytest
chatmaild/venv/bin/pip install -e chatmaild chatmaild/venv/bin/pip install -e chatmaild
python3 -m venv online-tests/venv python3 -m venv online-tests/venv
online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat pytest-benchmark
python3 -m venv venv python3 -m venv venv
venv/bin/pip install build venv/bin/pip install build