Compare commits

...

85 Commits

Author SHA1 Message Date
link2xt
20d9947573 Allow to send securejoin 2023-10-16 20:38:28 +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
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
holger krekel
00af333694 test works by logging into remote machine and checking the dovecot quota log 2023-10-16 15:19:29 +02:00
holger krekel
c9fd133942 improved test but still not doing what it should 2023-10-16 15:19:29 +02:00
holger krekel
caed6a3754 some fixes but still not quite running through 2023-10-16 15:19:29 +02:00
holger krekel
f71d372491 add a quota test (inspired by nami's #21 ) and try to get postfix/dovecot to implement the limit and the test to pass (it doesn't yet) 2023-10-16 15:19:29 +02:00
missytake
6debf11f6f sieve is not installed and we don't need it 2023-10-16 15:19:29 +02:00
holger krekel
60e1671062 make quota work 2023-10-16 15:19:29 +02:00
missytake
3c57155c40 fix: typo in postfix/master.cf 2023-10-16 01:31:03 +02:00
link2xt
cf1be90115 Switch from BLF-CRYPT to SHA512-CRYPT 2023-10-15 21:42:14 +00:00
link2xt
5781d3b04e Make scripts/measure_tls_and_logins.py executable 2023-10-15 21:42:14 +00:00
link2xt
862b09d268 dovecot: enable authentication cache 2023-10-15 21:42:14 +00:00
link2xt
9b438a7a96 Test different users logging in with the same password 2023-10-15 21:42:14 +00:00
link2xt
a107fb3cca Avoid reusing accounts between tests
Add time as a prefix.
2023-10-15 21:42:14 +00:00
link2xt
2a59cd4702 Do not apt update more than once a day 2023-10-15 20:31:13 +00:00
link2xt
57df5e254c Require that encrypted messages have "..." as a Subject 2023-10-15 19:10:25 +00:00
missytake
8cef4d7119 test: assert you can't just write suspicious things into subject 2023-10-15 19:10:25 +00:00
link2xt
20b7af9d71 filtermail: more robust check_encrypted() 2023-10-15 19:10:14 +00:00
missytake
df6ec4bd6d fix: chatmaild build + deployment 2023-10-15 20:08:00 +02:00
holger krekel
ade18aab7b refine script 2023-10-15 19:14:18 +02:00
holger krekel
a61f1fbf39 add capability getter for CHATMAIL_DOMAIN 2023-10-15 19:11:32 +02:00
holger krekel
26e4e1d9be refine measure script, update plan 2023-10-15 19:03:38 +02:00
holger krekel
a0e1d9e4d7 add a measurement for login/tls 2023-10-15 18:55:36 +02:00
link2xt
b05154b818 Remove chatmaild merging from the plan 2023-10-15 16:09:29 +00:00
link2xt
aab0a1f992 Update chatmaild README 2023-10-15 16:08:58 +00:00
link2xt
40ad67dc20 Merge doveauth and filtermail folders 2023-10-15 16:07:26 +00:00
link2xt
b548a8ddbd Merge doveauth and filtermail into chatmaild 2023-10-15 15:57:36 +00:00
link2xt
262eb36a5c Rename chatmail-pyinfra into deploy-chatmail 2023-10-15 15:40:17 +00:00
holger krekel
bd152c4a4e updated plan after joint nami,alex,holger session with adb around 2023-10-15 17:42:13 +02:00
link2xt
08a88d0fb3 dictproxy: log to stderr 2023-10-15 12:59:59 +00:00
link2xt
23145cad28 Encrypt the passwords in the database
There is also no need to compare the passwords manually,
dovecot does it for us.
2023-10-15 14:49:44 +02:00
missytake
735ccbc1f2 fix: remote-deploy.sh needs python3-venv 2023-10-15 12:07:55 +02:00
missytake
247eb55886 doveauth: switch from lua authentication to dict authentication
Co-Authored-By: holger krekel <holger@merlinux.eu>
Co-Authored-By: link2xt <link2xt@testrun.org>
2023-10-15 01:13:13 +00:00
link2xt
f85e4cdbd5 Add scripts/remote-deploy.sh
It is faster than deploying over SSH,
23 seconds vs 40 seconds here.
2023-10-15 01:06:38 +00:00
link2xt
1d7ebfa7a5 Do not build wheels and use a single dist/ directory (#11)
Only sdists are used for deployment.
2023-10-14 23:35:06 +00:00
missytake
f98f08f8f0 filtermail: daemon-reload systemd service on pyinfra deploy 2023-10-15 00:52:51 +02:00
link2xt
c9dc32bd10 Add filtermail 2023-10-14 21:52:47 +00:00
missytake
e061d98cfc doveauth: ; in sqlite statements not necessary 2023-10-14 18:39:01 +02:00
missytake
a9669d5c0f tests: test DB version 2023-10-14 18:39:01 +02:00
missytake
1520b3d567 doveauth: remove config table, read dbversion from PRAGMA instead #8 2023-10-14 18:39:01 +02:00
missytake
704ad72753 doveauth: add importable to __init__.py 2023-10-14 18:39:01 +02:00
missytake
6d590103ee tests: move database initialization to fixture 2023-10-14 18:39:01 +02:00
holger krekel
8217dc6f01 fix formatting 2023-10-14 14:34:54 +02:00
holger krekel
802f67cf54 fix formatting 2023-10-14 14:34:27 +02:00
holger krekel
a1e82a9969 some renaming and adding a pytest.ini 2023-10-14 14:34:12 +02:00
holger krekel
8d3e2af303 adapt init 2023-10-14 14:32:22 +02:00
holger krekel
369a0f8783 add basic delta chat tests 2023-10-14 14:32:22 +02:00
holger krekel
33000e18c0 fix/merge test files 2023-10-14 14:32:22 +02:00
holger krekel
397eed65a7 merge accidental test files 2023-10-14 14:32:22 +02:00
holger krekel
c8b593f5e2 let mail connection setting come from CHATMAIL_DOMAIN env 2023-10-14 14:32:22 +02:00
holger krekel
6003c9294d add tests 2023-10-14 14:32:22 +02:00
holger krekel
1742ee07c8 add smtp tests and fix scripts 2023-10-14 14:32:22 +02:00
holger krekel
5cd54026a8 refactor dovecot tests, move online tests one level up 2023-10-14 14:32:22 +02:00
missytake
290933e8b2 plan: persistence is achieved 2023-10-14 10:41:56 +02:00
missytake
d758b4c078 dovecot: run auth-worker as vmail user 2023-10-14 10:41:56 +02:00
missytake
552135317d doveauth: adjust pytest for persistent database 2023-10-14 10:41:56 +02:00
missytake
f940a962cc doveauth: integrate sqlite database 2023-10-14 10:41:56 +02:00
missytake
7eeb777ed9 doveauth: add sqlite database to persist accounts 2023-10-14 10:41:56 +02:00
missytake
ae2ee84db2 part of plan was resolved 2023-10-13 21:13:53 +02:00
missytake
69b9df9480 add comment about installing doveauth system-wide 2023-10-13 21:12:56 +02:00
missytake
4ebec75d95 apply suggestion about pathlib 2023-10-13 21:12:56 +02:00
link2xt
453910c57e Remove hardcoded domain from doveauth.py 2023-10-13 21:12:56 +02:00
link2xt
dd9b33907a Log the lookup command in doveauth.lua 2023-10-13 21:12:56 +02:00
missytake
716b8169f8 fix lint issues 2023-10-13 21:12:56 +02:00
missytake
6a6255b6d0 script to run all tests from repository root 2023-10-13 21:12:56 +02:00
missytake
fbda0fb53c install doveauth system-wide via pip 2023-10-13 21:12:56 +02:00
missytake
01f350fa0b make doveauth tests pass again 2023-10-13 21:12:56 +02:00
missytake
93a84617a8 add doveauth entrypoint for lua 2023-10-13 21:12:56 +02:00
link2xt
3b0037dc3a scripts/deploy.sh: allow to set $CHATMAIL_DOMAIN externally 2023-10-13 17:29:41 +00:00
missytake
9dfd0ee979 don't run deploy on import 2023-10-13 18:36:15 +02:00
missytake
344e799a51 move doveauth scripts to its own python project 2023-10-13 18:36:15 +02:00
missytake
556d9d37a4 added doveauth python project and README 2023-10-13 18:36:15 +02:00
44 changed files with 1406 additions and 263 deletions

View File

@@ -27,15 +27,11 @@ chatmail-pyinfra
pyproject.toml
chatmail/__init__ ...
# tests against the deployed system
tests/test_online_test.py
# doveauth tool used by dovecot's auth mechanism on the host system
doveauth
README.md
pyproject.toml
doveauth.py
doveauth.lua
test_doveauth.py
# lmtp server to block (outgoing) unencrypted messages
@@ -44,12 +40,19 @@ filtermail
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
bench.sh # run performance benchmark tests
```

View File

@@ -1,57 +0,0 @@
-- Escape shell argument by hex encoding it and wrapping in quotes.
function escape(data)
b16 = data:gsub(".", function(char) return string.format("%2X", char:byte()) end)
return ("'"..b16.."'")
end
-- call out to python program to actually manage authentication for dovecot
function chatctl_verify(user, password)
local cmd = "python3 /home/vmail/chatctl hexauth "..escape(user).." "..escape(password)
print("executing: "..cmd)
local handle = io.popen(cmd)
local result = handle:read("*a")
handle:close()
return split_chatctl(result)
end
function chatctl_lookup(user)
assert(user)
local handle = io.popen("python3 /home/vmail/chatctl hexlookup "..escape(user))
local result = handle:read("*a")
handle:close()
return split_chatctl(result)
end
function get_extra_dovecot_output(res)
return {home=res.home, uid=res.uid, gid=res.gid}
end
function auth_password_verify(request, password)
local res = chatctl_verify(request.user, password)
-- request:log_error("auth_password_verify "..request.user.." "..password)
if res.status == "ok" then
local extra = get_extra_dovecot_output(res)
return dovecot.auth.PASSDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
end
function auth_userdb_lookup(request)
local res = chatctl_lookup(request.user)
if res.status == "ok" then
return dovecot.auth.USERDB_RESULT_OK, get_extra_dovecot_output(res)
end
return dovecot.auth.USERDB_RESULT_USER_UNKNOWN, "no such user"
end
function split_chatctl(output)
local ret = {}
for key, value in output:gmatch "(%w+)%s*=%s*(%w+)" do
ret[key] = value
end
return ret
end

View File

@@ -1,78 +0,0 @@
require "doveauth"
-- simulate dovecot defined result codes
dovecot = {
auth = {
PASSDB_RESULT_OK="PASSWORD-OK",
PASSDB_RESULT_PASSWORD_MISMATCH="PASSWORD-MISMATCH",
USERDB_RESULT_OK="USERDB-OK",
USERDB_RESULT_USER_UNKNOWN="USERDB-UNKNOWN"
}
}
-- Tests for testing the lua<->python interaction
function test_password_verify_ok(user, password)
local res, extra = auth_password_verify({user=user}, password)
assert(res==dovecot.auth.PASSDB_RESULT_OK)
assert(extra.uid == "vmail")
assert(extra.gid == "vmail")
-- assert(extra.homedir == "/home/vmail/link2xt")
print("OK test_password_verify_ok "..user.." "..password)
end
function test_password_verify_mismatch(user, password)
local res = auth_password_verify({user=user}, password)
assert(res == dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH)
print("OK test_password_verify_mismatch "..user.." "..password)
end
function test_userdb_lookup_ok(user)
local res, extra = auth_userdb_lookup({user=user})
assert(extra.uid == "vmail")
assert(extra.gid == "vmail")
assert(res == dovecot.auth.USERDB_RESULT_OK)
print("OK test_userdb_lookup_ok "..user)
end
function test_userdb_lookup_mismatch(user)
local res, extra = auth_userdb_lookup({user=user})
assert(res == dovecot.auth.USERDB_RESULT_USER_UNKNOWN)
print("OK test_userdb_lookup_mismatch "..user)
end
function test_passdb_lookup_ok(user)
local res, extra = auth_passdb_lookup({user=user})
assert(extra.uid == "vmail")
assert(extra.gid == "vmail")
assert(res == dovecot.auth.PASSDB_RESULT_OK)
print("OK test_passdb_lookup_ok "..user)
end
function test_passdb_lookup_mismatch(user)
local res, extra = auth_passdb_lookup({user=user})
assert(res == dovecot.auth.PASSDB_RESULT_USER_UNKNOWN)
print("OK test_passdb_lookup_mismatch "..user)
end
function test_split_chatctl()
local res = split_chatctl("a=3 b=4\nc=5")
assert(res["a"] == "3")
assert(res["b"] == "4")
assert(res["c"] == "5")
print("OK test_split_chatctl")
end
test_split_chatctl()
test_password_verify_ok("link2xt@c1.testrun.org", "Ahyei6ie")
test_password_verify_mismatch("link2xt@c1.testrun.org", "Aqwlek")
test_userdb_lookup_ok("link2xt@c1.testrun.org")
test_userdb_lookup_mismatch("wlekqjlew@xyz.org")
-- probably not needed by dovecot?
-- test_passdb_lookup_ok("link2xt@c1.testrun.org")
-- test_passdb_lookup_mismatch("llqkwjelqwe@xyz.org")

View File

@@ -1,23 +0,0 @@
import subprocess
import pytest
from doveauth import get_user_data, verify_user
def test_basic():
data = get_user_data("link2xt@c1.testrun.org")
assert data
@pytest.mark.xfail(reason="no persistence yet")
def test_verify_or_create():
res = verify_user("newuser1@something.org", "kajdlkajsldk12l3kj1983")
assert res["status"] == "ok"
res = verify_user("newuser1@something.org", "kajdlqweqwe")
assert res["status"] == "fail"
def test_lua_integration(request):
p = request.fspath.dirpath("test_doveauth.lua")
proc = subprocess.run(["lua", str(p)])
assert proc.returncode == 0

View File

@@ -1,28 +0,0 @@
import pytest
import imaplib
@pytest.fixture
def conn():
return connect("c1.testrun.org")
def login(conn, user, password):
print("trying to login", user, password)
conn.login(user, password)
def connect(host):
print(f"connecting to {host}")
conn = imaplib.IMAP4_SSL(host)
return conn
def test_login_ok(conn):
login(conn, "link2xt@c1.testrun.org", "Ahyei6ie")
def test_login_fail(conn):
with pytest.raises(imaplib.IMAP4.error) as excinfo:
login(conn, "link2xt@c1.testrun.org", "qweqwe")
assert "AUTHENTICATIONFAILED" in str(excinfo)

5
chatmaild/README.md Normal file
View File

@@ -0,0 +1,5 @@
# chatmaild
chatmaild provides dovecot autentication
to create dovecot users on login
and mail filtering.

35
chatmaild/pyproject.toml Normal file
View File

@@ -0,0 +1,35 @@
[build-system]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "chatmaild"
version = "0.1"
dependencies = [
"aiosmtpd"
]
[project.scripts]
doveauth = "chatmaild.doveauth:main"
doveauth-dictproxy = "chatmaild.dictproxy:main"
filtermail = "chatmaild.filtermail:main"
[tool.pytest.ini_options]
addopts = "-v -ra --strict-markers"
[tool.tox]
legacy_tox_ini = """
[tox]
isolated_build = true
envlist = lint
[testenv:lint]
skipdist = True
skip_install = True
deps =
ruff
black
commands =
black --quiet --check --diff src/
ruff src/
"""

View File

View File

@@ -0,0 +1,140 @@
import sqlite3
import contextlib
import time
from pathlib import Path
class DBError(Exception):
"""error during an operation on the database."""
class Connection:
def __init__(self, sqlconn, write):
self._sqlconn = sqlconn
self._write = write
def close(self):
self._sqlconn.close()
def commit(self):
self._sqlconn.commit()
def rollback(self):
self._sqlconn.rollback()
def execute(self, query, params=()):
cur = self.cursor()
try:
cur.execute(query, params)
except sqlite3.IntegrityError as e:
raise DBError(e)
return cur
def cursor(self):
return self._sqlconn.cursor()
def create_user(self, addr: str, password: str):
"""Create a row in the users table."""
self.execute("PRAGMA foreign_keys=on")
q = """INSERT INTO users (addr, password, last_login)
VALUES (?, ?, ?)"""
self.execute(q, (addr, password, int(time.time())))
def get_user(self, addr: str) -> {}:
"""Get a row from the users table."""
q = "SELECT addr, password, last_login from users WHERE addr = ?"
row = self._sqlconn.execute(q, (addr,)).fetchone()
result = {}
if row:
result = dict(
user=row[0],
password=row[1],
last_login=row[2],
)
return result
class Database:
def __init__(self, path: str):
self.path = Path(path)
self.ensure_tables()
def _get_connection(
self, write=False, transaction=False, closing=False
) -> Connection:
# we let the database serialize all writers at connection time
# to play it very safe (we don't have massive amounts of writes).
mode = "ro"
if write:
mode = "rw"
if not self.path.exists():
mode = "rwc"
uri = "file:%s?mode=%s" % (self.path, mode)
sqlconn = sqlite3.connect(
uri,
timeout=60,
isolation_level=None if transaction else "DEFERRED",
uri=True,
)
# Enable Write-Ahead Logging to avoid readers blocking writers and vice versa.
if write:
sqlconn.execute("PRAGMA journal_mode=wal")
if transaction:
start_time = time.time()
while 1:
try:
sqlconn.execute("begin immediate")
break
except sqlite3.OperationalError:
# another thread may be writing, give it a chance to finish
time.sleep(0.1)
if time.time() - start_time > 5:
# if it takes this long, something is wrong
raise
conn = Connection(sqlconn, write=write)
if closing:
conn = contextlib.closing(conn)
return conn
@contextlib.contextmanager
def write_transaction(self):
conn = self._get_connection(closing=False, write=True, transaction=True)
try:
yield conn
except Exception:
conn.rollback()
conn.close()
raise
else:
conn.commit()
conn.close()
def read_connection(self, closing=True) -> Connection:
return self._get_connection(closing=closing, write=False)
def get_schema_version(self) -> int:
with self.read_connection() as conn:
dbversion = conn.execute("PRAGMA user_version").fetchone()[0]
return dbversion
CURRENT_DBVERSION = 1
def ensure_tables(self):
with self.write_transaction() as conn:
if self.get_schema_version() > 1:
raise DBError(
"version is %s; downgrading schema is not supported"
% (self.get_schema_version(),)
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS users (
addr TEXT PRIMARY KEY,
password TEXT,
last_login INTEGER
)
""",
)
conn.execute("PRAGMA user_version=%s" % (self.CURRENT_DBVERSION,))

View File

@@ -0,0 +1,115 @@
import os
import sys
import json
from socketserver import (
UnixStreamServer,
StreamRequestHandler,
ThreadingMixIn,
)
import pwd
import subprocess
from .database import Database
def encrypt_password(password: str):
password = password.encode("ascii")
# https://doc.dovecot.org/configuration_manual/authentication/password_schemes/
process = subprocess.Popen(
["doveadm", "pw", "-s", "SHA512-CRYPT"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
stdout_data, _stderr_data = process.communicate(
input=password + b"\n" + password + b"\n"
)
return stdout_data.decode("ascii").strip()
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 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 lookup_userdb(db, user):
return get_user_data(db, user)
def lookup_passdb(db, user, password):
userdata = get_user_data(db, user)
if not userdata:
return create_user(db, user, encrypt_password(password))
userdata["password"] = userdata["password"].strip()
return userdata
def handle_dovecot_request(msg, db):
print(f"received msg: {msg!r}", file=sys.stderr)
short_command = msg[0]
if short_command == "L": # LOOKUP
parts = msg[1:].split("\t")
keyname, user = parts[:2]
namespace, type, *args = keyname.split("/")
reply_command = "F"
res = ""
if namespace == "shared":
if type == "userdb":
res = lookup_userdb(db, user)
if res:
reply_command = "O"
else:
reply_command = "N"
elif type == "passdb":
res = lookup_passdb(db, user, password=args[0])
if res:
reply_command = "O"
else:
reply_command = "N"
print(f"res: {res!r}", file=sys.stderr)
json_res = json.dumps(res) if res else ""
return f"{reply_command}{json_res}\n"
return None
class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
pass
def main():
socket = sys.argv[1]
passwd_entry = pwd.getpwnam(sys.argv[2])
db = Database(sys.argv[3])
class Handler(StreamRequestHandler):
def handle(self):
while True:
msg = self.rfile.readline().strip().decode()
if not msg:
break
res = handle_dovecot_request(msg, db)
if res:
print(f"sending result: {res!r}", file=sys.stderr)
self.wfile.write(res.encode("ascii"))
self.wfile.flush()
try:
os.unlink(socket)
except FileNotFoundError:
pass
with ThreadedUnixStreamServer(socket, Handler) as server:
os.chown(socket, uid=passwd_entry.pw_uid, gid=passwd_entry.pw_gid)
try:
server.serve_forever()
except KeyboardInterrupt:
pass

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Dict authentication proxy for dovecot
[Service]
ExecStart=/usr/local/bin/doveauth-dictproxy /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -2,37 +2,40 @@
import base64
import sys
def get_user_data(user):
if user == "link2xt@c1.testrun.org":
return dict(
uid="vmail",
gid="vmail",
password="Ahyei6ie",
)
return {}
from .database import Database
def create_user(user, password):
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(user, password):
userdata = get_user_data(user)
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(user, password)
userdata = create_user(db, user, password)
userdata["status"] = "ok"
return userdata
def lookup_user(user):
userdata = get_user_data(user)
def lookup_user(db, user):
userdata = get_user_data(db, user)
if userdata:
userdata["status"] = "ok"
else:
@@ -45,13 +48,18 @@ def dump_result(res):
print(f"{key}={value}")
if __name__ == "__main__":
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(login, password)
res = verify_user(db, login, password)
dump_result(res)
elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2]).decode()
res = lookup_user(login)
res = lookup_user(db, login)
dump_result(res)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
import asyncio
import logging
from email.parser import BytesParser
from email import policy
from aiosmtpd.lmtp import LMTP
from aiosmtpd.controller import UnixSocketController
from smtplib import SMTP as SMTPClient
def check_encrypted(message):
"""Check that the message is an OpenPGP-encrypted message."""
if not message.is_multipart():
return False
if message.get("subject") != "...":
return False
if message.get_content_type() != "multipart/encrypted":
return False
parts_count = 0
for part in message.iter_parts():
if parts_count == 0:
if part.get_content_type() != "application/pgp-encrypted":
return False
elif parts_count == 1:
if part.get_content_type() != "application/octet-stream":
return False
else:
return False
parts_count += 1
return True
class ExampleController(UnixSocketController):
def factory(self):
return LMTP(self.handler, **self.SMTP_kwargs)
class ExampleHandler:
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
envelope.rcpt_tos.append(address)
return "250 OK"
async def handle_DATA(self, server, session, envelope):
logging.info("Processing DATA message from %s", envelope.mail_from)
valid_recipients = []
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
mail_encrypted = check_encrypted(message)
res = []
for recipient in envelope.rcpt_tos:
my_local_domain = envelope.mail_from.split("@")
if len(my_local_domain) != 2:
res += [f"500 Invalid from address <{envelope.mail_from}>"]
continue
if envelope.mail_from == recipient:
# Always allow sending emails to self.
valid_recipients += [recipient]
res += ["250 OK"]
continue
recipient_local_domain = recipient.split("@")
if len(recipient_local_domain) != 2:
res += [f"500 Invalid address <{recipient}>"]
continue
is_outgoing = recipient_local_domain[1] != my_local_domain[1]
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"]
continue
valid_recipients += [recipient]
res += ["250 OK"]
# Reinject the mail back into Postfix.
if valid_recipients:
logging.info("Reinjecting the mail")
client = SMTPClient("localhost", "10026")
client.sendmail(envelope.mail_from, valid_recipients, envelope.content)
return "\r\n".join(res)
async def asyncmain(loop):
controller = ExampleController(
ExampleHandler(), unix_socket="/var/spool/postfix/private/filtermail"
)
controller.start()
def main():
logging.basicConfig(level=logging.INFO)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.create_task(asyncmain(loop=loop))
loop.run_forever()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Email filter for chatmail servers
[Service]
ExecStart=/usr/local/bin/filtermail
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,36 @@
import pytest
from .dictproxy import get_user_data
from .doveauth import verify_user
from .database import Database, DBError
@pytest.fixture()
def db(tmpdir):
db_path = tmpdir / "passdb.sqlite"
print("database path:", db_path)
return Database(db_path)
def test_basic(db):
verify_user(db, "link2xt@c1.testrun.org", "asdf")
data = get_user_data(db, "link2xt@c1.testrun.org")
assert data
def test_verify_or_create(db):
res = verify_user(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
assert res["status"] == "ok"
res = verify_user(db, "newuser1@something.org", "kajdlqweqwe")
assert res["status"] == "fail"
def test_db_version(db):
assert db.get_schema_version() == 1
def test_too_high_db_version(db):
with db.write_transaction() as conn:
conn.execute("PRAGMA user_version=%s;" % (999,))
with pytest.raises(DBError):
db.ensure_tables()

View File

@@ -0,0 +1,290 @@
from .filtermail import check_encrypted
from email.parser import BytesParser
from email import policy
def test_filtermail():
def check_encrypted_bstr(content):
message = BytesParser(policy=policy.default).parsebytes(content)
return check_encrypted(message)
assert not check_encrypted_bstr(b"foo")
assert not check_encrypted_bstr(
"\r\n".join(
[
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
"Chat-Disposition-Notification-To: foobar@c2.testrun.org",
"Chat-User-Avatar: 0",
"From: <foobar@c2.testrun.org>",
"To: <barbaz@c2.testrun.org>",
"Date: Sun, 15 Oct 2023 16:41:44 +0000",
"Message-ID: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"Chat-Version: 1.0",
"Autocrypt: addr=foobar@c2.testrun.org; prefer-encrypt=mutual;",
"\tkeydata=xjMEZSrw3hYJKwYBBAHaRw8BAQdAiEKNQFU28c6qsx4vo/JHdt73RXdjMOmByf/XsGiJ7m",
"\tnNFzxmb29iYXJAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUq8N4CGwMECwkIBwYVCAkKCwID",
"\tFgIBFiEEGil0OvTIa6RngmCLUYNnEa9leJAACgkQUYNnEa9leJCX3gEAhm0MehE5byBBU1avPczr/I",
"\tHjNLht7Qf6++mAhlJmtDcA/0C8VYJhsUpmiDjuZaMDWNv4FO2BJG6LH7gSm6n7ClMJzjgEZSrw3hIK",
"\tKwYBBAGXVQEFAQEHQAxGG/QW0owCfMp1A+vXEMwgzWcBpNFr58kX2eXuPpM6AwEIB8J4BBgWCAAgBQ",
"\tJlKvDeAhsMFiEEGil0OvTIa6RngmCLUYNnEa9leJAACgkQUYNnEa9leJDg1gEAwLf8KDoAAKyYgjyI",
"\tvYvO9VEgBni1C4Xx1VjcaEmlDK8BALoFuUCK+enw76TtDcAUKhlhUiM6SDRExkS4Nskp/BcK",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no",
"",
"Hi!",
"",
"",
]
).encode()
)
assert not check_encrypted_bstr(
"\r\n".join(
[
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
"Chat-Disposition-Notification-To: foobar@c2.testrun.org",
"Chat-User-Avatar: 0",
"From: <foobar@c2.testrun.org>",
"To: <barbaz@c2.testrun.org>",
"Date: Sun, 15 Oct 2023 16:41:44 +0000",
"Message-ID: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"Chat-Version: 1.0",
"Autocrypt: addr=foobar@c2.testrun.org; prefer-encrypt=mutual;",
"\tkeydata=xjMEZSrw3hYJKwYBBAHaRw8BAQdAiEKNQFU28c6qsx4vo/JHdt73RXdjMOmByf/XsGiJ7m",
"\tnNFzxmb29iYXJAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUq8N4CGwMECwkIBwYVCAkKCwID",
"\tFgIBFiEEGil0OvTIa6RngmCLUYNnEa9leJAACgkQUYNnEa9leJCX3gEAhm0MehE5byBBU1avPczr/I",
"\tHjNLht7Qf6++mAhlJmtDcA/0C8VYJhsUpmiDjuZaMDWNv4FO2BJG6LH7gSm6n7ClMJzjgEZSrw3hIK",
"\tKwYBBAGXVQEFAQEHQAxGG/QW0owCfMp1A+vXEMwgzWcBpNFr58kX2eXuPpM6AwEIB8J4BBgWCAAgBQ",
"\tJlKvDeAhsMFiEEGil0OvTIa6RngmCLUYNnEa9leJAACgkQUYNnEa9leJDg1gEAwLf8KDoAAKyYgjyI",
"\tvYvO9VEgBni1C4Xx1VjcaEmlDK8BALoFuUCK+enw76TtDcAUKhlhUiM6SDRExkS4Nskp/BcK",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no",
"",
"Hi!",
"",
"",
]
).encode()
)
# https://xkcd.com/1181/
assert not check_encrypted_bstr(
"\r\n".join(
[
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
"Chat-Disposition-Notification-To: foobar@c2.testrun.org",
"Chat-User-Avatar: 0",
"From: <foobar@c2.testrun.org>",
"To: <barbaz@c2.testrun.org>",
"Date: Sun, 15 Oct 2023 16:41:44 +0000",
"Message-ID: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"References: <Mr.3gckbNy5bch.uK3Hd2Ws6-w@c2.testrun.org>",
"Chat-Version: 1.0",
"Autocrypt: addr=foobar@c2.testrun.org; prefer-encrypt=mutual;",
"\tkeydata=xjMEZSrw3hYJKwYBBAHaRw8BAQdAiEKNQFU28c6qsx4vo/JHdt73RXdjMOmByf/XsGiJ7m",
"\tnNFzxmb29iYXJAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUq8N4CGwMECwkIBwYVCAkKCwID",
"\tFgIBFiEEGil0OvTIa6RngmCLUYNnEa9leJAACgkQUYNnEa9leJCX3gEAhm0MehE5byBBU1avPczr/I",
"\tHjNLht7Qf6++mAhlJmtDcA/0C8VYJhsUpmiDjuZaMDWNv4FO2BJG6LH7gSm6n7ClMJzjgEZSrw3hIK",
"\tKwYBBAGXVQEFAQEHQAxGG/QW0owCfMp1A+vXEMwgzWcBpNFr58kX2eXuPpM6AwEIB8J4BBgWCAAgBQ",
"\tJlKvDeAhsMFiEEGil0OvTIa6RngmCLUYNnEa9leJAACgkQUYNnEa9leJDg1gEAwLf8KDoAAKyYgjyI",
"\tvYvO9VEgBni1C4Xx1VjcaEmlDK8BALoFuUCK+enw76TtDcAUKhlhUiM6SDRExkS4Nskp/BcK",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no",
"",
"-----BEGIN PGP MESSAGE-----",
"Hi!",
"-----END PGP MESSAGE-----",
"",
"",
]
).encode()
)
assert check_encrypted_bstr(
"\r\n".join(
[
"Subject: ...",
"From: <barbaz@c2.testrun.org>",
"To: <foobar@c2.testrun.org>",
"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",
"Autocrypt: addr=barbaz@c2.testrun.org; 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()
)
assert not check_encrypted_bstr(
"\r\n".join(
[
"Subject: Buy Penis Enlargement at www.malicious-domain.com",
"From: <barbaz@c2.testrun.org>",
"To: <foobar@c2.testrun.org>",
"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",
"Autocrypt: addr=barbaz@c2.testrun.org; 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()
)
assert not check_encrypted_bstr(
"\r\n".join(
[
"Subject: Message opened",
"From: <barbaz@c2.testrun.org>",
"To: <foobar@c2.testrun.org>",
"Date: Sun, 15 Oct 2023 16:43:25 +0000",
"Message-ID: <Mr.78MWtlV7RAi.goCFzBhCYfy@c2.testrun.org>",
"Auto-Submitted: auto-replied",
"Chat-Version: 1.0",
"MIME-Version: 1.0",
"Content-Type: multipart/report; report-type=disposition-notification;",
'\tboundary="Gl92xgZjOShJ5PGHntqYkoo2OK2Dvi"',
"",
"",
"--Gl92xgZjOShJ5PGHntqYkoo2OK2Dvi",
"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no",
"",
'The "Hi!" message you sent was displayed on the screen of the recipient.',
"",
"This is no guarantee the content was read.",
"",
"",
"--Gl92xgZjOShJ5PGHntqYkoo2OK2Dvi",
"Content-Type: message/disposition-notification",
"",
"Reporting-UA: Delta Chat 1.124.1",
"Original-Recipient: rfc822;barbaz@c2.testrun.org",
"Final-Recipient: rfc822;barbaz@c2.testrun.org",
"Original-Message-ID: <Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>",
"Disposition: manual-action/MDN-sent-automatically; displayed",
"",
"",
"--Gl92xgZjOShJ5PGHntqYkoo2OK2Dvi--",
"",
"",
]
).encode()
)

View File

@@ -3,7 +3,7 @@ requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "chatmail"
name = "deploy-chatmail"
version = "0.1"
dependencies = [
"pyinfra",

View File

@@ -2,6 +2,7 @@
Chat Mail pyinfra deploy.
"""
import importlib.resources
from pathlib import Path
from pyinfra import host, logger
from pyinfra.operations import apt, files, server, systemd, python
@@ -9,17 +10,67 @@ from pyinfra.facts.files import File
from .acmetool import deploy_acmetool
def _install_chatctl() -> None:
"""Setup chatctl."""
files.put(
src=importlib.resources.files(__package__)
.joinpath("dovecot/doveauth.py")
.open("rb"),
dest="/home/vmail/chatctl",
user="vmail",
group="vmail",
mode="755",
def _install_chatmaild() -> None:
chatmaild_filename = "chatmaild-0.1.tar.gz"
chatmaild_path = importlib.resources.files(__package__).joinpath(
f"../../../dist/{chatmaild_filename}"
)
remote_path = f"/tmp/{chatmaild_filename}"
if Path(str(chatmaild_path)).exists():
files.put(
name="Upload chatmaild source package",
src=chatmaild_path.open("rb"),
dest=remote_path,
)
apt.packages(
name="apt install python3-aiosmtpd",
packages=["python3-aiosmtpd", "python3-pip"],
)
# --no-deps because aiosmtplib is installed with `apt`.
server.shell(
name="install chatmaild with pip",
commands=[f"pip install --break-system-packages {remote_path}"],
)
files.put(
name="upload doveauth-dictproxy.service",
src=importlib.resources.files("chatmaild")
.joinpath("doveauth-dictproxy.service")
.open("rb"),
dest="/etc/systemd/system/doveauth-dictproxy.service",
user="root",
group="root",
mode="644",
)
systemd.service(
name="Setup doveauth-dictproxy service",
service="doveauth-dictproxy.service",
running=True,
enabled=True,
restarted=True,
daemon_reload=True,
)
files.put(
name="upload filtermail.service",
src=importlib.resources.files("chatmaild")
.joinpath("filtermail.service")
.open("rb"),
dest="/etc/systemd/system/filtermail.service",
user="root",
group="root",
mode="644",
)
systemd.service(
name="Setup filtermail service",
service="filtermail.service",
running=True,
enabled=True,
restarted=True,
daemon_reload=True,
)
def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
@@ -100,16 +151,24 @@ def _configure_dovecot(mail_server: str) -> bool:
config={"hostname": mail_server},
)
need_restart |= main_config.changed
# luarocks install http lpeg_patterns fifo
auth_script = files.put(
src=importlib.resources.files(__package__).joinpath("dovecot/doveauth.lua"),
dest="/etc/dovecot/doveauth.lua",
auth_config = files.put(
src=importlib.resources.files(__package__).joinpath("dovecot/auth.conf"),
dest="/etc/dovecot/auth.conf",
user="root",
group="root",
mode="644",
)
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",
)
need_restart |= auth_script.changed
return need_restart
@@ -122,7 +181,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
:param dkim_selector:
"""
apt.update(name="apt update")
apt.update(name="apt update", cache_time=24 * 3600)
server.group(name="Create vmail group", group="vmail", system=True)
server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
@@ -144,11 +203,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
apt.packages(
name="Install Dovecot",
packages=[
"dovecot-imapd",
"dovecot-lmtpd",
"dovecot-auth-lua",
],
packages=["dovecot-imapd", "dovecot-lmtpd"],
)
apt.packages(
@@ -159,7 +214,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
],
)
_install_chatctl()
_install_chatmaild()
dovecot_need_restart = _configure_dovecot(mail_server)
postfix_need_restart = _configure_postfix(mail_domain)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)

View File

@@ -0,0 +1,5 @@
uri = proxy:/run/dovecot/doveauth.socket:auth
iterate_disable = yes
default_pass_scheme = plain
password_key = passdb/%w/%u
user_key = userdb/%u

View File

@@ -8,17 +8,19 @@ auth_verbose = yes
auth_debug = yes
auth_debug_passwords = yes
auth_verbose_passwords = plain
auth_cache_size = 100M
mail_plugins = quota
mail_debug = yes
# Authentication for system users.
passdb {
driver = lua
args = file=/etc/dovecot/doveauth.lua
driver = dict
args = /etc/dovecot/auth.conf
}
userdb {
driver = lua
args = file=/etc/dovecot/doveauth.lua
driver = dict
args = /etc/dovecot/auth.conf
}
##
## Mailbox locations and namespaces
##
@@ -60,13 +62,28 @@ mail_privileged_group = vmail
# Enable IMAP COMPRESS (RFC 4978).
# <https://datatracker.ietf.org/doc/html/rfc4978.html>
protocol imap {
mail_plugins = $mail_plugins imap_zlib
mail_plugins = $mail_plugins imap_zlib imap_quota
}
protocol lmtp {
mail_plugins = $mail_plugins quota
}
plugin {
imap_compress_deflate_level = 6
}
plugin {
# for now we define static quota-rules for all users
quota = maildir:User quota
quota_rule = *:storage=100M
quota_max_mail_size=30M
quota_grace = 0
# quota_over_flag_value = TRUE
}
service lmtp {
user=vmail
@@ -88,7 +105,7 @@ service auth {
service auth-worker {
# Default is root.
# Drop privileges we don't need.
user = $default_internal_user
user = vmail
}
ssl = required

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

@@ -37,6 +37,8 @@ mydestination =
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
# maximum 30MB sized messages
message_size_limit = 31457280
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all

View File

@@ -9,7 +9,7 @@
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
smtp inet n - y - - smtpd
smtp inet n - y - - smtpd -v
#smtp inet n - y - 1 postscreen
#smtpd pass - - y - - smtpd
#dnsblog unix - - y - 0 dnsblog
@@ -28,6 +28,7 @@ submission inet n - y - - smtpd
-o smtpd_recipient_restrictions=
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
-o content_filter=filter:unix:private/filtermail
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
@@ -42,6 +43,7 @@ smtps inet n - y - - smtpd
-o smtpd_recipient_restrictions=
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
-o content_filter=filter:unix:private/filtermail
#628 inet n - y - - qmqpd
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
@@ -70,3 +72,7 @@ lmtp unix - - y - - lmtp
anvil unix - - y - 1 anvil
scache unix - - y - 1 scache
postlog unix-dgram n - n - 1 postlogd
filter unix - n n - - lmtp
# Local SMTP server for reinjecting filered mail.
localhost:10026 inet n - n - 10 smtpd
-o content_filter=

View File

@@ -1,6 +1,6 @@
import os
from pyinfra import host, facts
from chatmail import deploy_chatmail
import pyinfra
from deploy_chatmail import deploy_chatmail
def main():
@@ -15,4 +15,5 @@ def main():
deploy_chatmail(mail_domain, mail_server, dkim_selector)
main()
if pyinfra.is_cli:
main()

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)

201
online-tests/conftest.py Normal file
View File

@@ -0,0 +1,201 @@
import os
import io
import random
import subprocess
import imaplib
import smtplib
import itertools
import pytest
def pytest_addoption(parser):
parser.addoption(
"--slow", action="store_true", default=False, help="also run slow tests"
)
def pytest_runtest_setup(item):
markers = list(item.iter_markers(name="slow"))
if markers:
if not item.config.getoption("--slow"):
pytest.skip("skipping slow test, use --slow to run")
@pytest.fixture
def maildomain():
domain = os.environ.get("CHATMAIL_DOMAIN")
if not domain:
pytest.skip("set CHATMAIL_DOMAIN to a ssh-reachable chatmail instance")
return domain
@pytest.fixture
def sshdomain(maildomain):
return os.environ.get("CHATMAIL_SSH", maildomain)
@pytest.fixture
def maildomain2():
domain = os.environ.get("CHATMAIL_DOMAIN2")
if not domain:
pytest.skip("set CHATMAIL_DOMAIN2 to a ssh-reachable chatmail instance")
return domain
@pytest.fixture
def sshdomain2(maildomain2):
return os.environ.get("CHATMAIL_SSH2", maildomain2)
def pytest_report_header():
domain = os.environ.get("CHATMAIL_DOMAIN")
if domain:
text = f"chatmail test instance: {domain}"
return ["-" * len(text), text, "-" * len(text)]
@pytest.fixture
def imap(maildomain):
return ImapConn(maildomain)
class ImapConn:
AuthError = imaplib.IMAP4.error
logcmd = "journalctl -f -u dovecot"
name = "dovecot"
def __init__(self, host):
self.host = host
def connect(self):
print(f"imap-connect {self.host}")
self.conn = imaplib.IMAP4_SSL(self.host)
def login(self, user, password):
print(f"imap-login {user!r} {password!r}")
self.conn.login(user, password)
@pytest.fixture
def smtp(maildomain):
return SmtpConn(maildomain)
class SmtpConn:
AuthError = smtplib.SMTPAuthenticationError
logcmd = "journalctl -f -t postfix/smtpd -t postfix/smtp -t postfix/lmtp"
name = "postfix"
def __init__(self, host):
self.host = host
def connect(self):
print(f"smtp-connect {self.host}")
self.conn = smtplib.SMTP_SSL(self.host)
def login(self, user, password):
print(f"smtp-login {user!r} {password!r}")
self.conn.login(user, password)
@pytest.fixture(params=["imap", "smtp"])
def imap_or_smtp(request):
return request.getfixturevalue(request.param)
@pytest.fixture
def gencreds(maildomain):
count = itertools.count()
next(count)
def gen(domain=None):
domain = domain if domain else maildomain
while 1:
num = next(count)
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join(random.choices(alphanumeric, k=10))
user = f"ac{num}_{user}"
password = "".join(random.choices(alphanumeric, k=10))
yield f"{user}@{domain}", f"{password}"
return lambda domain=None: next(gen(domain))
#
# Delta Chat testplugin re-use
# use the cmfactory fixture to get chatmail instance accounts
#
class ChatmailTestProcess:
"""Provider for chatmail instance accounts as used by deltachat.testplugin.acfactory"""
def __init__(self, pytestconfig, maildomain, gencreds):
self.pytestconfig = pytestconfig
self.maildomain = maildomain
assert "." in self.maildomain, maildomain
self.gencreds = gencreds
self._addr2files = {}
def get_liveconfig_producer(self):
while 1:
user, password = self.gencreds(self.maildomain)
config = {
"addr": user,
"mail_pw": password,
}
# speed up account configuration
config["mail_server"] = self.maildomain
config["send_server"] = self.maildomain
yield config
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
pass
def cache_maybe_store_configured_db_files(self, acc):
pass
@pytest.fixture
def cmfactory(request, gencreds, tmpdir, data, maildomain):
# cloned from deltachat.testplugin.amfactory
pytest.importorskip("deltachat")
from deltachat.testplugin import ACFactory
testproc = ChatmailTestProcess(request.config, maildomain, gencreds)
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
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
if testproc.pytestconfig.getoption("--extra-info"):
logfile = io.StringIO()
am.dump_imap_summary(logfile=logfile)
print(logfile.getvalue())
# request.node.add_report_section("call", "imap-server-state", s)
@pytest.fixture
def remote(sshdomain):
return Remote(sshdomain)
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,
)
while 1:
line = self.popen.stdout.readline()
yield line.decode().strip().lower()

3
online-tests/pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
addopts = -vrsx --strict-markers
markers = slow: mark test as slow (requires --slow option to run)

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

@@ -0,0 +1,36 @@
import pytest
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
"""Test a) that an initial login creates a user automatically
and b) verify we can also login a second time with the same password
and c) that using a different password fails the login."""
user, password = gencreds()
lp.sec(f"login first time with {user} {password}")
imap_or_smtp.connect()
imap_or_smtp.login(user, password)
lp.indent("success")
lp.sec(f"reconnect and login second time {user} {password}")
imap_or_smtp.connect()
imap_or_smtp.login(user, password)
imap_or_smtp.connect()
lp.sec("success")
lp.sec(f"reconnect and verify wrong password fails {user} ")
imap_or_smtp.connect()
with pytest.raises(imap_or_smtp.AuthError):
imap_or_smtp.login(user, password + "wrong")
def test_login_same_password(imap_or_smtp, gencreds):
"""Test two different users logging in with the same password
to ensure that authentication process does not confuse the users
by using only the password hash as a key.
"""
user1, password1 = gencreds()
user2, _ = gencreds()
imap_or_smtp.connect()
imap_or_smtp.login(user1, password1)
imap_or_smtp.connect()
imap_or_smtp.login(user2, password1)

View File

@@ -0,0 +1,83 @@
import random
import pytest
class TestEndToEndDeltaChat:
"Tests that use Delta Chat accounts on the chat mail instance."
def test_one_on_one(self, cmfactory, lp):
"""Test that a DC account can send a message to a second DC account
on the same chat-mail instance."""
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: prepare and send text message to ac2")
chat.send_text("message0")
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
@pytest.mark.slow
def test_exceed_quota(self, cmfactory, lp, tmpdir, remote):
"""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.
"""
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
quota = 1024 * 1024 * 100
attachsize = 1 * 1024 * 1024
num_to_send = quota // attachsize + 2
lp.sec(f"ac1: send {num_to_send} large files to ac2")
lp.indent(f"per-user quota is assumed to be: {quota/(1024*1024)}MB")
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
msgs = []
for i in range(num_to_send):
attachment = tmpdir / f"attachment{i}"
data = "".join(random.choice(alphanumeric) for i in range(1024))
with open(attachment, "w+") as f:
for j in range(attachsize // len(data)):
f.write(data)
msg = chat.send_file(str(attachment))
msgs.append(msg)
lp.indent(f"Sent out msg {i}, size {attachsize/(1024*1024)}MB")
lp.sec("ac2: check messages are arriving until quota is reached")
addr = ac2.get_config("addr").lower()
saved_ok = 0
for line in remote.iter_output("journalctl -f -u dovecot"):
if addr not in line:
# print(line)
continue
if "quota" in line:
if "quota exceeded" in line:
if saved_ok < num_to_send // 2:
pytest.fail(
f"quota exceeded too early: after {saved_ok} messages already"
)
lp.indent("good, message sending failed because quota was exceeded")
return
if "saved mail to inbox" in line:
saved_ok += 1
print(f"{saved_ok}: {line}")
if saved_ok >= num_to_send:
break
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

@@ -2,32 +2,58 @@
## Dovecot goals/steps
1. create-user-on-login ("doveauth")
- repackage so that "doveauth" does not come from a hard-coded path
- persistence of accounts
- automatic expiry of messages older than M days
- also expunge unread messages
2. per-user quota (adaptive)
3. automatic expiry of messages older than M days
4. automatic expiry of users that haven't logged in for N days
- limit: configure max-connections per account
## Postfix goals/steps
## nami: send out rate limit / rspamd
1. block all outgoing mails with our own LMTP program
- basic outgoing send rate/limits (depending on "account-rating")
use rspamd in a minimal way, check support dkim-signing
(including an online test exceeding rate limit)
2. only allow (outgoing) mails if secure-join or autocrypt-pgp-encrypted format
(probably via an lmtp service)
3. basic outgoing send rate/limits (depending on "account-rating")
## doveauth questions/futures
- bcrypt-password scheme is slow: require long passwords, use faster hashing
- define user-name and password policies, and implement them
(be very restrictive at the beginning, we can relax later)
- password is part of the dictproxy-lookup key, is it safe to use auth-caching?
## How to limit creation of accounts?
attack: a 3-line bash script to fill the chatmail db with millions of unused accouts
- make it computationally expensive (somehow try to except our tests from it)
1st pass instant onboarding: create userid + cheap password -- if it fails then
2nd pass instant onboarding: create userdid + comput. expensive password
- probably also do firewall: limit number of new tcp-connections per IP address per duration
## Open/deferred questions
- automatic expiry of users that haven't logged in for N days
Is it neccessary? If all messages are gone, does the existence of
an e-mail address bother anybody?
## web page for chat-mail servers?
- documentation for users, privacy policy etc.
(probably also with provider-messages ...)
## online tests (first with plain python/pytest)
- write tests for dovecot login (exists)
- write tests for postfix logins
- write A<>B send/receive tests
- write tests for postfix logins (exists)
- write A<>B send/receive tests (exists)
## Delta Chat

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

@@ -1,3 +1,9 @@
#!/usr/bin/env bash
export CHATMAIL_DOMAIN="${1:-c1.testrun.org}"
chatmail-pyinfra/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" deploy.py
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
export CHATMAIL_DOMAIN
venv/bin/python3 -m build -n --sdist chatmaild --outdir dist
deploy-chatmail/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" deploy.py
rm -r dist/

View File

@@ -0,0 +1,14 @@
import os
import time
import imaplib
domain = os.environ.get("CHATMAIL_DOMAIN", "c3.testrun.org")
print("connecting")
conn = imaplib.IMAP4_SSL(domain)
print("logging in")
conn.login(f"imapcapa", "pass")
status, res = conn.capability()
for capa in sorted(res[0].decode().split()):
print(capa)

View File

@@ -1,4 +1,17 @@
#!/bin/sh
python3 -m venv chatmail-pyinfra/venv
chatmail-pyinfra/venv/bin/pip install pyinfra
chatmail-pyinfra/venv/bin/pip install -e chatmail-pyinfra
set -e
python3 -m venv deploy-chatmail/venv
deploy-chatmail/venv/bin/pip install pyinfra pytest
deploy-chatmail/venv/bin/pip install -e deploy-chatmail
deploy-chatmail/venv/bin/pip install -e chatmaild
python3 -m venv chatmaild/venv
chatmaild/venv/bin/pip install pytest
chatmaild/venv/bin/pip install -e chatmaild
python3 -m venv online-tests/venv
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

@@ -0,0 +1,28 @@
#!/usr/bin/env python3
import os
import time
import imaplib
domain = os.environ.get("CHATMAIL_DOMAIN", "c3.testrun.org")
NUM_CONNECTIONS=10
conns = []
start = time.time()
for i in range(NUM_CONNECTIONS):
print(f"opening connection {i} to {domain}")
conn = imaplib.IMAP4_SSL(domain)
conns.append(conn)
tlsdone = time.time()
duration = tlsdone-start
print(f"{duration}: TLS connections opening TLS connections")
for i, conn in enumerate(conns):
print(f"logging into connection {i}")
conn.login(f"measure{i}", "pass")
logindone = time.time()
duration = logindone - tlsdone
print(f"{duration}: LOGINS done")

8
scripts/remote-deploy.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
: ${CHATMAIL_SSH_HOST:=$CHATMAIL_DOMAIN}
rsync -avz . "root@$CHATMAIL_SSH_HOST:/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"

7
scripts/test.sh Executable file
View File

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