Compare commits

..

3 Commits

Author SHA1 Message Date
holger krekel
0885d8f72e apply nami's suggestions (chatmail SSH env var, running --slow in test.sh) 2023-10-16 17:48:15 +02:00
holger krekel
bb567b4fb7 also show the chatmail instance prominently in the test header 2023-10-16 17:09:45 +02:00
holger krekel
fdb01d269c - introduce pytest.mark.slow marker and "--slow" CLI option
- refactor login tests to allow running them against both imap/smtp
2023-10-16 17:00:39 +02:00
15 changed files with 42 additions and 158 deletions

View File

@@ -52,7 +52,6 @@ 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:
break continue
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

@@ -9,8 +9,9 @@ from aiosmtpd.controller import UnixSocketController
from smtplib import SMTP as SMTPClient from smtplib import SMTP as SMTPClient
def check_encrypted(message): def check_encrypted(content):
"""Check that the message is an OpenPGP-encrypted message.""" """Check that the message is an OpenPGP-encrypted message."""
message = BytesParser(policy=policy.default).parsebytes(content)
if not message.is_multipart(): if not message.is_multipart():
return False return False
if message.get("subject") != "...": if message.get("subject") != "...":
@@ -46,8 +47,7 @@ class ExampleHandler:
valid_recipients = [] valid_recipients = []
message = BytesParser(policy=policy.default).parsebytes(envelope.content) mail_encrypted = check_encrypted(envelope.content)
mail_encrypted = check_encrypted(message)
res = [] res = []
for recipient in envelope.rcpt_tos: for recipient in envelope.rcpt_tos:
@@ -68,13 +68,7 @@ class ExampleHandler:
continue continue
is_outgoing = recipient_local_domain[1] != my_local_domain[1] is_outgoing = recipient_local_domain[1] != my_local_domain[1]
if is_outgoing and not mail_encrypted:
if (
is_outgoing
and not mail_encrypted
and message.get("secure-join") != "vc-request"
and message.get("secure-join") != "vg-request"
):
res += ["500 Outgoing mail must be encrypted"] res += ["500 Outgoing mail must be encrypted"]
continue continue

View File

@@ -1,16 +1,12 @@
import pytest
from .filtermail import check_encrypted from .filtermail import check_encrypted
from email.parser import BytesParser
from email import policy
def test_filtermail(): def test_filtermail():
def check_encrypted_bstr(content): assert not check_encrypted(b"foo")
message = BytesParser(policy=policy.default).parsebytes(content)
return check_encrypted(message)
assert not check_encrypted_bstr(b"foo") assert not check_encrypted(
assert not check_encrypted_bstr(
"\r\n".join( "\r\n".join(
[ [
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=", "Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
@@ -40,7 +36,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert not check_encrypted_bstr( assert not check_encrypted(
"\r\n".join( "\r\n".join(
[ [
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=", "Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
@@ -71,7 +67,7 @@ def test_filtermail():
) )
# https://xkcd.com/1181/ # https://xkcd.com/1181/
assert not check_encrypted_bstr( assert not check_encrypted(
"\r\n".join( "\r\n".join(
[ [
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=", "Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
@@ -103,7 +99,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert check_encrypted_bstr( assert check_encrypted(
"\r\n".join( "\r\n".join(
[ [
"Subject: ...", "Subject: ...",
@@ -176,7 +172,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert not check_encrypted_bstr( assert not check_encrypted(
"\r\n".join( "\r\n".join(
[ [
"Subject: Buy Penis Enlargement at www.malicious-domain.com", "Subject: Buy Penis Enlargement at www.malicious-domain.com",
@@ -249,7 +245,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert not check_encrypted_bstr( assert not check_encrypted(
"\r\n".join( "\r\n".join(
[ [
"Subject: Message opened", "Subject: Message opened",

View File

@@ -160,16 +160,6 @@ 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

View File

@@ -1,4 +0,0 @@
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

@@ -1,34 +0,0 @@
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,23 +30,13 @@ def maildomain():
@pytest.fixture @pytest.fixture
def sshdomain(maildomain): def chatmail_ssh(maildomain):
return os.environ.get("CHATMAIL_SSH", maildomain) domain = os.environ.get("CHATMAIL_SSH")
@pytest.fixture
def maildomain2():
domain = os.environ.get("CHATMAIL_DOMAIN2")
if not domain: if not domain:
pytest.skip("set CHATMAIL_DOMAIN2 to a ssh-reachable chatmail instance") domain = maildomain
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:
@@ -61,8 +51,6 @@ 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
@@ -83,8 +71,6 @@ 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
@@ -108,17 +94,16 @@ def gencreds(maildomain):
count = itertools.count() count = itertools.count()
next(count) next(count)
def gen(domain=None): def gen():
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}@{domain}", f"{password}" yield f"{user}@{maildomain}", f"{password}"
return lambda domain=None: next(gen(domain)) return lambda: next(gen())
# #
@@ -133,13 +118,12 @@ 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(self.maildomain) user, password = self.gencreds()
config = { config = {
"addr": user, "addr": user,
"mail_pw": password, "mail_pw": password,
@@ -157,21 +141,13 @@ class ChatmailTestProcess:
@pytest.fixture @pytest.fixture
def cmfactory(request, gencreds, tmpdir, data, maildomain): def cmfactory(request, maildomain, gencreds, tmpdir, data):
# 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"):
@@ -182,20 +158,13 @@ def cmfactory(request, gencreds, tmpdir, data, maildomain):
@pytest.fixture @pytest.fixture
def remote(sshdomain): def dovelogreader(chatmail_ssh):
return Remote(sshdomain) def remote_reader():
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:
line = self.popen.stdout.readline() yield popen.stdout.readline()
yield line.decode().strip().lower()
return remote_reader

View File

@@ -1,15 +0,0 @@
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(f"reconnect and verify wrong password fails {user} ") lp.sec("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, remote): def test_exceed_quota(self, cmfactory, lp, tmpdir, dovelogreader):
"""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,7 +48,8 @@ class TestEndToEndDeltaChat:
addr = ac2.get_config("addr").lower() addr = ac2.get_config("addr").lower()
saved_ok = 0 saved_ok = 0
for line in remote.iter_output("journalctl -f -u dovecot"): for line in dovelogreader():
line = line.decode().lower().strip()
if addr not in line: if addr not in line:
# print(line) # print(line)
continue continue
@@ -67,17 +68,3 @@ class TestEndToEndDeltaChat:
break break
pytest.fail("sending succeeded although messages should exceed quota") pytest.fail("sending succeeded although messages should exceed quota")
def test_securejoin(self, cmfactory, lp, 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()
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
qr = ac1.get_setup_contact_qr()
lp.sec("ac2: start QR-code based setup contact protocol")
ch = ac2.qr_setup_contact(qr)
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)

View File

@@ -3,11 +3,17 @@
## Dovecot goals/steps ## Dovecot goals/steps
- automatic expiry of messages older than M days - automatic expiry of messages older than M days
- also expunge unread messages - delete unconditionally messages older than 40 days
- limit: configure max-connections per account - limit: configure max-connections per account
## Filtermail
- (alex, Only allow (outgoing) mails if secure-join or autocrypt-pgp-encrypted format.
TODO: mime-parse mails and check/add tests
## nami: send out rate limit / rspamd ## nami: send out rate limit / rspamd
- basic outgoing send rate/limits (depending on "account-rating") - basic outgoing send rate/limits (depending on "account-rating")

View File

@@ -1,4 +0,0 @@
#!/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 pytest-benchmark online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat
python3 -m venv venv python3 -m venv venv
venv/bin/pip install build venv/bin/pip install build

View File

@@ -4,4 +4,4 @@ pushd chatmaild/src/chatmaild
../../venv/bin/pytest ../../venv/bin/pytest
popd popd
online-tests/venv/bin/pytest online-tests/ -vrx --durations=5 online-tests/venv/bin/pytest online-tests/ -vrx --durations=5 --slow