Compare commits

...

27 Commits

Author SHA1 Message Date
missytake
9262146e52 test: report if rate limit from last test was still active 2023-10-17 20:51:39 +02:00
missytake
020ae872f0 test: test that there is no internal limit (xfail for now) 2023-10-17 20:51:39 +02:00
missytake
051fe6ec9f postfix: add simple rate limiting without allow list or leaky bucket, also for internal mail 2023-10-17 20:51:39 +02:00
missytake
75fe5a236a add test for postfix rate limiting 2023-10-17 20:51:39 +02:00
missytake
f333226abe dictproxy: make NOCREATE_FILE a constant; log warning if creating account fails 2023-10-17 20:03:18 +02:00
missytake
45fe8a668b tests: pass CLI arguments to pytest, don't run chatmails tests in weird subdir 2023-10-17 20:03:18 +02:00
missytake
beac91159d tests: install doveadm for being able to run dictproxy tests 2023-10-17 20:03:18 +02:00
missytake
75e7c85e61 tests: adjust to dictproxy, test /tmp/nocreate 2023-10-17 20:03:18 +02:00
missytake
040b7a74a6 doveauth: don't create users if /tmp/nocreate exists 2023-10-17 20:03:18 +02:00
missytake
0138e59355 doveauth: removed doveauth.py from the project 2023-10-17 20:03:18 +02:00
holger krekel
fffbdc10c3 minimize capabilities 2023-10-17 01:22:19 +02:00
holger krekel
3419e359c8 create build venv in chatmaild/venv 2023-10-17 00:35:07 +02:00
holger krekel
0b051f8154 move deploy.py file and revamp README 2023-10-17 00:35:07 +02:00
link2xt
5936f7a3be postfix: disable virtual and local MDAs
We do not need them, all mails are delivered to Dovecot over LMTP.
2023-10-16 22:21:39 +00:00
link2xt
983ffa6236 Disable acmetool redirector 2023-10-16 22:12:45 +00:00
holger krekel
b74fde2a9f add postfix instrumented debugging 2023-10-16 23:38:02 +02:00
holger krekel
e176595f1f add global debug flag and instrument dovecot with it 2023-10-16 23:38:02 +02:00
link2xt
179c79a052 Allow to send securejoin 2023-10-16 21:15:56 +00:00
link2xt
408da296f1 test.sh: do not run slow tests by default 2023-10-16 20:13:41 +00: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
24 changed files with 422 additions and 186 deletions

View File

@@ -1,60 +1,40 @@
# Chat Mail server configuration # Chat Mail server configuration
This package deploys Postfix and Dovecot servers, including OpenDKIM for DKIM signing. This repository setups a ready-to-go chatmail instance
comprised of a minimal setup of the battle-tested
Postfix uses Dovecot for authentication as described in <https://www.postfix.org/SASL_README.html#server_dovecot> [postfix smtp server](https://www.postfix.org) and [dovecot imap server](https://www.dovecot.org).
## Getting started ## Getting started
prepare: 1. prepare your local system:
pip install -e chatmail-infra scripts/init.sh
2. set environment variable to the chatmail domain you want to setup:
export CHATMAIL_DOMAIN=c1.testrun.org # replace with your host
3. run the deploy of the chat mail instance:
scripts/deploy.sh
then run with pyinfra command line tool: ## Running tests and benchmarks (offline and online)
CHATMAIL_DOMAIN=c1.testrun.org pyinfra --ssh-user root c1.testrun.org deploy.py 1. Set `CHATMAIL_SSH` so that `ssh root@$CHATMAIL_SSH` allows
to login to the chatmail instance server.
2. To run local and online tests:
## Structure (wip) scripts/test.sh
```
# package doveauth tool and deploy chatmail server to a envvar-specified ssh-reachable host 3. To run benchmarks against your chatmail instance:
deploy.py
# chatmail pyinfra deploy package scripts/bench.sh
chatmail-pyinfra
pyproject.toml
chatmail/__init__ ...
# doveauth tool used by dovecot's auth mechanism on the host system ## Running tests (offline and online)
doveauth
README.md
pyproject.toml
doveauth.py
test_doveauth.py
# lmtp server to block (outgoing) unencrypted messages
filtermail
README.md
pyproject.toml
....
# online tests (after deploy)
online-tests # runnable via pytest
# scripts for setup/development/deployment
scripts/
init.sh # create venv/other perequires
deploy.sh # run pyinfra based deploy of everything
test.sh # run all local and online tests
``` ```
## Dovecot/Postfix configuration ## Dovecot/Postfix configuration
### Ports ### Ports
@@ -64,4 +44,16 @@ Dovecot listens on ports 143(imap) and 993 (imaps).
## DNS ## DNS
For DKIM you must add a DNS entry as in /etc/opendkim/selector.txt (where selector is the opendkim_selector configured in the chatmail inventory). For DKIM you must add a DNS entry as found in /etc/opendkim/selector.txt on your chatmail instance.
The above `scripts/deploy.sh` prints out the DKIM selector and DNS entry you
need to setup with your DNS provider.
## Emergency Commands
If you need to stop account creation,
e.g. because some script is wildly creating accounts,
just run `touch /tmp/nocreate`.
You can remove the file
as soon as the attacker was banned
by different means.

View File

@@ -10,7 +10,6 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
doveauth = "chatmaild.doveauth:main"
doveauth-dictproxy = "chatmaild.dictproxy:main" doveauth-dictproxy = "chatmaild.dictproxy:main"
filtermail = "chatmaild.filtermail:main" filtermail = "chatmaild.filtermail:main"

View File

@@ -1,3 +1,4 @@
import logging
import os import os
import sys import sys
import json import json
@@ -11,6 +12,8 @@ import subprocess
from .database import Database from .database import Database
NOCREATE_FILE = "/etc/chatmail-nocreate"
def encrypt_password(password: str): def encrypt_password(password: str):
password = password.encode("ascii") password = password.encode("ascii")
@@ -27,6 +30,9 @@ def encrypt_password(password: str):
def create_user(db, user, password): def create_user(db, user, password):
if os.path.exists(NOCREATE_FILE):
logging.warning(f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation.")
return
with db.write_transaction() as conn: with db.write_transaction() as conn:
conn.create_user(user, password) conn.create_user(user, password)
return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password) return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
@@ -95,7 +101,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

@@ -1,65 +0,0 @@
#!/usr/bin/env python3
import base64
import sys
from .database import Database
def get_user_data(db, user):
with db.read_connection() as conn:
result = conn.get_user(user)
if result:
result["uid"] = "vmail"
result["gid"] = "vmail"
return result
def create_user(db, user, password):
with db.write_transaction() as conn:
conn.create_user(user, password)
return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
def verify_user(db, user, password):
userdata = get_user_data(db, user)
if userdata:
if userdata.get("password") == password:
userdata["status"] = "ok"
else:
userdata["status"] = "fail"
else:
userdata = create_user(db, user, password)
userdata["status"] = "ok"
return userdata
def lookup_user(db, user):
userdata = get_user_data(db, user)
if userdata:
userdata["status"] = "ok"
else:
userdata["status"] = "fail"
return userdata
def dump_result(res):
for key, value in res.items():
print(f"{key}={value}")
def main():
db = Database("/home/vmail/passdb.sqlite")
if sys.argv[1] == "hexauth":
login = base64.b16decode(sys.argv[2]).decode()
password = base64.b16decode(sys.argv[3]).decode()
res = verify_user(db, login, password)
dump_result(res)
elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2]).decode()
res = lookup_user(db, login)
dump_result(res)
if __name__ == "__main__":
main()

View File

@@ -9,9 +9,8 @@ from aiosmtpd.controller import UnixSocketController
from smtplib import SMTP as SMTPClient from smtplib import SMTP as SMTPClient
def check_encrypted(content): def check_encrypted(message):
"""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") != "...":
@@ -47,7 +46,8 @@ class ExampleHandler:
valid_recipients = [] valid_recipients = []
mail_encrypted = check_encrypted(envelope.content) message = BytesParser(policy=policy.default).parsebytes(envelope.content)
mail_encrypted = check_encrypted(message)
res = [] res = []
for recipient in envelope.rcpt_tos: for recipient in envelope.rcpt_tos:
@@ -68,7 +68,13 @@ 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,7 +1,9 @@
import os
import pytest import pytest
from .dictproxy import get_user_data import chatmaild.dictproxy
from .doveauth import verify_user from .dictproxy import get_user_data, lookup_passdb
from .database import Database, DBError from .database import Database, DBError
@@ -13,16 +15,31 @@ def db(tmpdir):
def test_basic(db): def test_basic(db):
verify_user(db, "link2xt@c1.testrun.org", "asdf") chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE):
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
lookup_passdb(db, "link2xt@c1.testrun.org", "asdf")
data = get_user_data(db, "link2xt@c1.testrun.org") data = get_user_data(db, "link2xt@c1.testrun.org")
assert data assert data
def test_verify_or_create(db): def test_dont_overwrite_password_on_wrong_login(db):
res = verify_user(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983") """Test that logging in with a different password doesn't create a new user"""
assert res["status"] == "ok" res = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
res = verify_user(db, "newuser1@something.org", "kajdlqweqwe") assert res["password"]
assert res["status"] == "fail" res2 = lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
# this function always returns a password hash, which is actually compared by dovecot.
assert res["password"] == res2["password"]
def test_nocreate_file(db):
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f:
f.write("")
assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE)
lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
assert not get_user_data(db, "newuser1@something.org")
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
def test_db_version(db): def test_db_version(db):

View File

@@ -1,12 +1,16 @@
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():
assert not check_encrypted(b"foo") def check_encrypted_bstr(content):
message = BytesParser(policy=policy.default).parsebytes(content)
return check_encrypted(message)
assert not check_encrypted( assert not check_encrypted_bstr(b"foo")
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?=",
@@ -36,7 +40,7 @@ def test_filtermail():
).encode() ).encode()
) )
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?=",
@@ -67,7 +71,7 @@ def test_filtermail():
) )
# https://xkcd.com/1181/ # https://xkcd.com/1181/
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?=",
@@ -99,7 +103,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert check_encrypted( assert check_encrypted_bstr(
"\r\n".join( "\r\n".join(
[ [
"Subject: ...", "Subject: ...",
@@ -172,7 +176,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert not check_encrypted( assert not check_encrypted_bstr(
"\r\n".join( "\r\n".join(
[ [
"Subject: Buy Penis Enlargement at www.malicious-domain.com", "Subject: Buy Penis Enlargement at www.malicious-domain.com",
@@ -245,7 +249,7 @@ def test_filtermail():
).encode() ).encode()
) )
assert not check_encrypted( assert not check_encrypted_bstr(
"\r\n".join( "\r\n".join(
[ [
"Subject: Message opened", "Subject: Message opened",

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(
@@ -215,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

@@ -50,8 +50,8 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
systemd.service( systemd.service(
name="Setup acmetool-redirector service", name="Setup acmetool-redirector service",
service="acmetool-redirector.service", service="acmetool-redirector.service",
running=True, running=False,
enabled=True, enabled=False,
restarted=service_file.changed, restarted=service_file.changed,
) )

View File

@@ -4,13 +4,23 @@ 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
# these are the capabilities Delta Chat cares about actually
# so let's keep the network overhead per login small
# https://github.com/deltachat/deltachat-core-rust/blob/master/src/imap/capabilities.rs
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE
# Authentication for system users. # Authentication for system users.
passdb { passdb {

View File

@@ -29,6 +29,9 @@ myhostname = {{ config.domain_name }}
alias_maps = hash:/etc/aliases alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases alias_database = hash:/etc/aliases
# hard limit, also on internal messages
smtpd_client_message_rate_limit = 80
# Postfix does not deliver mail for any domain by itself. # Postfix does not deliver mail for any domain by itself.
# Primary domain is listed in `virtual_mailbox_domains` instead # Primary domain is listed in `virtual_mailbox_domains` instead
# and handed over to Dovecot. # and handed over to Dovecot.

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
@@ -66,8 +70,6 @@ showq unix n - y - - showq
error unix - - y - - error error unix - - y - - error
retry unix - - y - - error retry unix - - y - - error
discard unix - - y - - discard discard unix - - y - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - y - - lmtp lmtp unix - - y - - lmtp
anvil unix - - y - 1 anvil anvil unix - - y - 1 anvil
scache unix - - y - 1 scache scache unix - - y - 1 scache

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

@@ -1,4 +1,5 @@
import pytest import pytest
import smtplib
def test_login_basic_functioning(imap_or_smtp, gencreds, lp): def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
@@ -17,7 +18,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")
@@ -34,3 +35,178 @@ def test_login_same_password(imap_or_smtp, gencreds):
imap_or_smtp.login(user1, password1) imap_or_smtp.login(user1, password1)
imap_or_smtp.connect() imap_or_smtp.connect()
imap_or_smtp.login(user2, password1) imap_or_smtp.login(user2, password1)
@pytest.mark.xfail(reason="Only rate limit is internal as well now")
def test_no_internal_rate_limit(smtp, gencreds):
"""Test that there is no rate limit between accounts on the same chatmail server."""
user, password = gencreds()
to_addr, = gencreds()
smtp.connect()
smtp.login(user, password)
mail = "\r\n".join(
[
"Subject: ...",
f"From: <{user}>",
f"To: <{to_addr}>",
"Date: Sun, 15 Oct 2023 16:43:21 +0000",
"Message-ID: <Mr.UVyJWZmkCKM.hGzNc6glBE_@c2.testrun.org>",
"In-Reply-To: <Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"\t<Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
"Chat-Version: 1.0",
f"Autocrypt: addr={user}; prefer-encrypt=mutual;",
"\tkeydata=xjMEZSwWjhYJKwYBBAHaRw8BAQdAQBEhqeJh0GueHB6kF/DUQqYCxARNBVokg/AzT+7LqH",
"\trNFzxiYXJiYXpAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUsFo4CGwMECwkIBwYVCAkKCwID",
"\tFgIBFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX9A4AEAnHWHp49eBCMHK5t66gYPiW",
"\tXQuB1mwUjzGfYWB+0RXUoA/0xcQ3FbUNlGKW7Blp6eMFfViv6Mv2d3kNSXACB6nmcMzjgEZSwWjhIK",
"\tKwYBBAGXVQEFAQEHQBpY5L2M1XHo0uxf8SX1wNLBp/OVvidoWHQF2Jz+kJsUAwEIB8J4BBgWCAAgBQ",
"\tJlLBaOAhsMFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX/INgEA37AJaNvruYsJVanP",
"\tIXnYw4CKd55UAwl8Zcy+M2diAbkA/0fHHcGV4r78hpbbL1Os52DPOdqYQRauIeJUeG+G6bQO",
"MIME-Version: 1.0",
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";',
'\tboundary="YFrteb74qSXmggbOxZL9dRnhymywAi"',
"",
"",
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
"Content-Description: PGP/MIME version identification",
"Content-Type: application/pgp-encrypted",
"",
"Version: 1",
"",
"",
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
"Content-Description: OpenPGP encrypted message",
'Content-Disposition: inline; filename="encrypted.asc";',
'Content-Type: application/octet-stream; name="encrypted.asc"',
"",
"-----BEGIN PGP MESSAGE-----",
"",
"wU4DhW3gBZ/VvCYSAQdA8bMs2spwbKdGjVsL1ByPkNrqD7frpB73maeL6I6SzDYg",
"O5G53tv339RdKq3WRcCtEEvxjHlUx2XNwXzC04BpmfvBTgNfPUyLDzjXnxIBB0Ae",
"8ymwGvXMCCimHXN0Dg8Ui62KOi03h0UgheoHWovJSCDF4CKre/xtFr3nL7lq/PKI",
"JsjVNz7/RK9FSXF6WwfONtLCyQGEuVAsB/KXfCBEyfKhaMwGHvhujRidGW5uV1no",
"lMGl3ODmo29Lgeu2uSE7EpJRZoe6hU6ddmBkqxax61ZtkaFlGFFpdo2K8balNNdz",
"ZsJ/9mmI9x3oOJ4/l1nhQbUO9ADbs7gJhFdV5Qkp30b5fCI7bU+aoe1ccBbLe/WM",
"YUty1PqcuQT7XjA+XmYuL261tvW8pBetT+i33/E2d8PzzYt2IuK9qeevyS+yxdwA",
"kfwejFWzzsUlJaDxs1x4XOxkMgSj+jo+g12dFOb7fyClsAnq23iDb8AuaT/BScAI",
"+lO+gher69+6LmM7VGHLG5k762J1jTaQCaKt1s8TAWV99Eo4491vL6fyvk3l/Cfg",
"RXSwiWFgj19Pn0Rq7CD9v22UE2vdUMBTcV4aw79mClk1YQ23jbF0y5DCjPdJ62Zo",
"tskBgFt3NoWV80jZ76zIBLrrjLwCCll8JjJtFwSkt2GX5RFBsVa4A8IDht9RtEk7",
"rrHgbSZQfkauEi/mH3/6CDZoLqSHudUZ7d4MaJwun1TkFYGe2ORwGJd4OBj3oGJp",
"H8YBwCpk///L/fKjX0Gg3M8nrpM4wrRFhPKidAgO/kcm25X4+ZHlVkWBTCt5RWKI",
"fHh6oLDZCqCfcgMkE1KKmwfIHaUkhq5BPRigwy6i5dh1DM4+1UCLh3dxzVbqE9b9",
"61NB19nXdRtDA2sOUnj9ve6m/wEPyCb6/zBQZqvCBYb1/AjdXpUrFT+DbpfyxaXN",
"XfhDVb5mNqNM/IVj0V5fvTc6vOfYbzQtPm10H+FdWWfb+rJRfyC3MA2w2IqstFe3",
"w3bu2iE6CQvSqRvge+ZqLKt/NqYwOURiUmpuklbl3kPJ97+mfKWoiqk8Iz1VY+bb",
"NMUC7aoGv+jcoj+WS6PYO8N6BeRVUUB3ZJSf8nzjgxm1/BcM+UD3BPrlhT11ODRs",
"baifGbprMWwt3dhb8cQgRT8GPdpO1OsDkzL6iikMjLHWWiA99GV6ruiHsIPw6boW",
"A6/uSOskbDHOROotKmddGTBd0iiHXAoQsJFt1ZjUkt6EHrgWs+GAvrvKpXs1mrz8",
"uj3GwEFrHS+Xuf2UDgpszYT3hI2cL/kUtGakVR7m7vVMZqXBUbZdGAEb1PZNPwsI",
"E4aMK02+EVB+tSN4Fzj99N2YD0inVYt+oPjr2tHhUS6aSGBNS/48Ki47DOg4Sxkn",
"lkOWnEbCD+XTnbDd",
"=agR5",
"-----END PGP MESSAGE-----",
"",
"",
"--YFrteb74qSXmggbOxZL9dRnhymywAi--",
"",
"",
]
).encode()
for i in range(100):
print("Sending mail", str(i))
smtp.conn.sendmail(user, to_addr, mail)
def test_exceed_rate_limit(smtp, gencreds):
"""Test that the outbound rate limit is exceeded if we send a lot of messages at once."""
user, password = gencreds()
smtp.connect()
smtp.login(user, password)
to_addr = "foobar@example.org"
mail = "\r\n".join(
[
"Subject: ...",
f"From: <{user}>",
f"To: <{to_addr}>",
"Date: Sun, 15 Oct 2023 16:43:21 +0000",
"Message-ID: <Mr.UVyJWZmkCKM.hGzNc6glBE_@c2.testrun.org>",
"In-Reply-To: <Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"\t<Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
"Chat-Version: 1.0",
f"Autocrypt: addr={user}; prefer-encrypt=mutual;",
"\tkeydata=xjMEZSwWjhYJKwYBBAHaRw8BAQdAQBEhqeJh0GueHB6kF/DUQqYCxARNBVokg/AzT+7LqH",
"\trNFzxiYXJiYXpAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUsFo4CGwMECwkIBwYVCAkKCwID",
"\tFgIBFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX9A4AEAnHWHp49eBCMHK5t66gYPiW",
"\tXQuB1mwUjzGfYWB+0RXUoA/0xcQ3FbUNlGKW7Blp6eMFfViv6Mv2d3kNSXACB6nmcMzjgEZSwWjhIK",
"\tKwYBBAGXVQEFAQEHQBpY5L2M1XHo0uxf8SX1wNLBp/OVvidoWHQF2Jz+kJsUAwEIB8J4BBgWCAAgBQ",
"\tJlLBaOAhsMFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX/INgEA37AJaNvruYsJVanP",
"\tIXnYw4CKd55UAwl8Zcy+M2diAbkA/0fHHcGV4r78hpbbL1Os52DPOdqYQRauIeJUeG+G6bQO",
"MIME-Version: 1.0",
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";',
'\tboundary="YFrteb74qSXmggbOxZL9dRnhymywAi"',
"",
"",
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
"Content-Description: PGP/MIME version identification",
"Content-Type: application/pgp-encrypted",
"",
"Version: 1",
"",
"",
"--YFrteb74qSXmggbOxZL9dRnhymywAi",
"Content-Description: OpenPGP encrypted message",
'Content-Disposition: inline; filename="encrypted.asc";',
'Content-Type: application/octet-stream; name="encrypted.asc"',
"",
"-----BEGIN PGP MESSAGE-----",
"",
"wU4DhW3gBZ/VvCYSAQdA8bMs2spwbKdGjVsL1ByPkNrqD7frpB73maeL6I6SzDYg",
"O5G53tv339RdKq3WRcCtEEvxjHlUx2XNwXzC04BpmfvBTgNfPUyLDzjXnxIBB0Ae",
"8ymwGvXMCCimHXN0Dg8Ui62KOi03h0UgheoHWovJSCDF4CKre/xtFr3nL7lq/PKI",
"JsjVNz7/RK9FSXF6WwfONtLCyQGEuVAsB/KXfCBEyfKhaMwGHvhujRidGW5uV1no",
"lMGl3ODmo29Lgeu2uSE7EpJRZoe6hU6ddmBkqxax61ZtkaFlGFFpdo2K8balNNdz",
"ZsJ/9mmI9x3oOJ4/l1nhQbUO9ADbs7gJhFdV5Qkp30b5fCI7bU+aoe1ccBbLe/WM",
"YUty1PqcuQT7XjA+XmYuL261tvW8pBetT+i33/E2d8PzzYt2IuK9qeevyS+yxdwA",
"kfwejFWzzsUlJaDxs1x4XOxkMgSj+jo+g12dFOb7fyClsAnq23iDb8AuaT/BScAI",
"+lO+gher69+6LmM7VGHLG5k762J1jTaQCaKt1s8TAWV99Eo4491vL6fyvk3l/Cfg",
"RXSwiWFgj19Pn0Rq7CD9v22UE2vdUMBTcV4aw79mClk1YQ23jbF0y5DCjPdJ62Zo",
"tskBgFt3NoWV80jZ76zIBLrrjLwCCll8JjJtFwSkt2GX5RFBsVa4A8IDht9RtEk7",
"rrHgbSZQfkauEi/mH3/6CDZoLqSHudUZ7d4MaJwun1TkFYGe2ORwGJd4OBj3oGJp",
"H8YBwCpk///L/fKjX0Gg3M8nrpM4wrRFhPKidAgO/kcm25X4+ZHlVkWBTCt5RWKI",
"fHh6oLDZCqCfcgMkE1KKmwfIHaUkhq5BPRigwy6i5dh1DM4+1UCLh3dxzVbqE9b9",
"61NB19nXdRtDA2sOUnj9ve6m/wEPyCb6/zBQZqvCBYb1/AjdXpUrFT+DbpfyxaXN",
"XfhDVb5mNqNM/IVj0V5fvTc6vOfYbzQtPm10H+FdWWfb+rJRfyC3MA2w2IqstFe3",
"w3bu2iE6CQvSqRvge+ZqLKt/NqYwOURiUmpuklbl3kPJ97+mfKWoiqk8Iz1VY+bb",
"NMUC7aoGv+jcoj+WS6PYO8N6BeRVUUB3ZJSf8nzjgxm1/BcM+UD3BPrlhT11ODRs",
"baifGbprMWwt3dhb8cQgRT8GPdpO1OsDkzL6iikMjLHWWiA99GV6ruiHsIPw6boW",
"A6/uSOskbDHOROotKmddGTBd0iiHXAoQsJFt1ZjUkt6EHrgWs+GAvrvKpXs1mrz8",
"uj3GwEFrHS+Xuf2UDgpszYT3hI2cL/kUtGakVR7m7vVMZqXBUbZdGAEb1PZNPwsI",
"E4aMK02+EVB+tSN4Fzj99N2YD0inVYt+oPjr2tHhUS6aSGBNS/48Ki47DOg4Sxkn",
"lkOWnEbCD+XTnbDd",
"=agR5",
"-----END PGP MESSAGE-----",
"",
"",
"--YFrteb74qSXmggbOxZL9dRnhymywAi--",
"",
"",
]
).encode()
for i in range(100):
print("Sending mail", str(i))
try:
smtp.conn.sendmail(user, to_addr, mail)
except smtplib.SMTPSenderRefused as e:
if i == 0:
pytest.fail(f"rate limit was exceeded too early with msg {i} - maybe wait a minute before testing?")
if i < 41:
pytest.fail(f"rate limit was exceeded too early with msg {i}")
assert e.smtp_code == 450
assert b'4.7.1 Error: too much mail from' in e.smtp_error
return
pytest.fail("Rate limit was not exceeded")

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
@@ -68,3 +67,17 @@ 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

@@ -8,12 +8,6 @@
- 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")

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

@@ -2,8 +2,9 @@
: ${CHATMAIL_DOMAIN:=c1.testrun.org} : ${CHATMAIL_DOMAIN:=c1.testrun.org}
export CHATMAIL_DOMAIN export CHATMAIL_DOMAIN
venv/bin/python3 -m build -n --sdist chatmaild --outdir dist chatmaild/venv/bin/python3 -m build -n --sdist chatmaild --outdir dist
deploy-chatmail/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" deploy.py deploy-chatmail/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" \
deploy-chatmail/src/deploy_chatmail/deploy.py
rm -r dist/ rm -r dist/

View File

@@ -6,12 +6,9 @@ deploy-chatmail/venv/bin/pip install -e deploy-chatmail
deploy-chatmail/venv/bin/pip install -e chatmaild deploy-chatmail/venv/bin/pip install -e chatmaild
python3 -m venv chatmaild/venv python3 -m venv chatmaild/venv
chatmaild/venv/bin/pip install pytest sudo apt install -y dovecot-core && sudo systemctl disable --now dovecot
chatmaild/venv/bin/pip install --upgrade pytest build 'setuptools>=68'
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
venv/bin/pip install build
venv/bin/pip install 'setuptools>=68'

View File

@@ -2,7 +2,7 @@
set -e set -e
: ${CHATMAIL_DOMAIN:=c1.testrun.org} : ${CHATMAIL_DOMAIN:=c1.testrun.org}
: ${CHATMAIL_SSH_HOST:=$CHATMAIL_DOMAIN} : ${CHATMAIL_SSH:=$CHATMAIL_DOMAIN}
rsync -avz . "root@$CHATMAIL_SSH_HOST:/root/chatmail" --exclude='/.git' --filter="dir-merge,- .gitignore" rsync -avz . "root@$CHATMAIL_SSH:/root/chatmail" --exclude='/.git' --filter="dir-merge,- .gitignore"
ssh "root@$CHATMAIL_SSH_HOST" "cd /root/chatmail; apt install -y python3-venv; python3 -m venv venv; venv/bin/pip install pyinfra build; venv/bin/python3 -m build -n --sdist chatmaild --outdir dist; venv/bin/pip install -e ./deploy-chatmail -e ./chatmaild; export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN; venv/bin/pyinfra @local deploy.py" ssh "root@$CHATMAIL_SSH" "cd /root/chatmail; apt install -y python3-venv; python3 -m venv venv; venv/bin/pip install pyinfra build; venv/bin/python3 -m build -n --sdist chatmaild --outdir dist; venv/bin/pip install -e ./deploy-chatmail -e ./chatmaild; export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN; venv/bin/pyinfra @local deploy.py"

View File

@@ -1,7 +1,3 @@
#!/bin/bash #!/bin/bash
set -e chatmaild/venv/bin/pytest chatmaild/ $@
pushd chatmaild/src/chatmaild online-tests/venv/bin/pytest online-tests/ -vrx --durations=5 $@
../../venv/bin/pytest
popd
online-tests/venv/bin/pytest online-tests/ -vrx --durations=5 --slow