Compare commits

...

50 Commits

Author SHA1 Message Date
holger krekel
c0f20f0c0b adapt init 2023-10-14 14:04:56 +02:00
holger krekel
d07aab03b9 add basic delta chat tests 2023-10-14 13:56:18 +02:00
holger krekel
fb7d34b06a fix/merge test files 2023-10-14 13:15:58 +02:00
holger krekel
d6eeb0b1d4 merge accidental test files 2023-10-14 13:15:07 +02:00
holger krekel
fc1779926a let mail connection setting come from CHATMAIL_DOMAIN env 2023-10-14 12:25:13 +02:00
holger krekel
0f2cb90e09 add tests 2023-10-14 12:07:46 +02:00
holger krekel
207d7c7060 add smtp tests and fix scripts 2023-10-14 12:01:39 +02:00
holger krekel
c7dfd7ca41 refactor dovecot tests, move online tests one level up 2023-10-14 11:42:58 +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
holger krekel
6d3ffd8f4e add plan as discussed with alex and nami 2023-10-13 15:44:06 +00:00
holger krekel
a24f1e8393 create venv in chatmail-pyinfra 2023-10-13 15:44:06 +00:00
holger krekel
f84692a07a fix/rename 2023-10-13 15:44:06 +00:00
holger krekel
4badc7c8d6 (nami, hpk) draft repackaging goal 2023-10-13 15:44:06 +00:00
link2xt
4b82fd6f77 Add init.sh and deploy.sh scripts (#2) 2023-10-13 16:14:02 +02:00
link2xt
9da375cf5c README: add --ssh-user root 2023-10-13 14:06:30 +00:00
missytake
8086e2ee2f removed inventory, doesn't work anymore anyway 2023-10-13 14:28:08 +02:00
holger krekel
1b88e0d9d0 add a functional online test for login ok/failure 2023-10-13 14:23:14 +02:00
holger krekel
db6df34703 python tests work 2023-10-13 14:23:11 +02:00
holger krekel
a907da9907 wip 2023-10-13 14:22:08 +02:00
holger krekel
14649c37fd this seems to work 2023-10-13 14:22:08 +02:00
holger krekel
28fe373489 (nami, hpk) some random WIP-changes to make dovecot auth work and get us an understanding 2023-10-13 14:22:08 +02:00
missytake
1942ad3cef ensure environment variables are set 2023-10-13 11:17:55 +02:00
holger krekel
47091db28c renames 2023-10-13 11:02:57 +02:00
holger krekel
f013ff434e rename chatctl to doveauth 2023-10-13 10:43:07 +02:00
holger krekel
4ba5b49d19 rename hpk to c1 inv 2023-10-13 09:36:59 +02:00
holger krekel
15d650dc83 move tox.ini into pyproject.toml 2023-10-13 09:36:27 +02:00
holger krekel
5f5cc99567 first test for python chatctl part and applying black 2023-10-12 19:32:49 +02:00
holger krekel
a1f0854f33 refactor python part a little (thanks missytake) 2023-10-12 19:18:16 +02:00
holger krekel
f2a26bc5fe added more tests 2023-10-12 19:01:47 +02:00
holger krekel
940b39bce7 more tests, better interface between lua and chatctl, also passing dovecot extras 2023-10-12 18:50:40 +02:00
holger krekel
067252703f another test 2023-10-12 17:55:46 +02:00
holger krekel
77d800b13f write a bit of tests for a lua script that calls into chatctl.py (#1)
* add a lua test script running directly against  chatctl.py
2023-10-12 17:41:57 +02:00
34 changed files with 762 additions and 112 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
doveauth/dist/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/

View File

@@ -4,7 +4,61 @@ This package deploys Postfix and Dovecot servers, including OpenDKIM for DKIM si
Postfix uses Dovecot for authentication as described in <https://www.postfix.org/SASL_README.html#server_dovecot> Postfix uses Dovecot for authentication as described in <https://www.postfix.org/SASL_README.html#server_dovecot>
## Ports ## Getting started
prepare:
pip install -e chatmail-infra
then run with pyinfra command line tool:
CHATMAIL_DOMAIN=c1.testrun.org pyinfra --ssh-user root c1.testrun.org deploy.py
## Structure (wip)
```
# package doveauth tool and deploy chatmail server to a envvar-specified ssh-reachable host
deploy.py
# chatmail pyinfra deploy package
chatmail-pyinfra
pyproject.toml
chatmail/__init__ ...
# 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
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
### Ports
Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions). Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
Dovecot listens on ports 143(imap) and 993 (imaps). Dovecot listens on ports 143(imap) and 993 (imaps).
@@ -12,9 +66,3 @@ 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 in /etc/opendkim/selector.txt (where selector is the opendkim_selector configured in the chatmail inventory).
## Run with pyinfra
```
CHATMAIL_DOMAIN=c1.testrun.org pyinfra c1.testrun.org deploy.py
```

View File

@@ -0,0 +1,30 @@
[build-system]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "chatmail"
version = "0.1"
dependencies = [
"pyinfra",
]
[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

@@ -2,7 +2,7 @@
Chat Mail pyinfra deploy. Chat Mail pyinfra deploy.
""" """
import importlib.resources import importlib.resources
from io import StringIO from pathlib import Path
from pyinfra import host, logger from pyinfra import host, logger
from pyinfra.operations import apt, files, server, systemd, python from pyinfra.operations import apt, files, server, systemd, python
@@ -10,17 +10,29 @@ from pyinfra.facts.files import File
from .acmetool import deploy_acmetool from .acmetool import deploy_acmetool
def _install_chatctl() -> None: def _install_doveauth() -> None:
"""Setup chatctl.""" """Setup chatctl."""
files.put( doveauth_filename = "doveauth-0.1.tar.gz"
src=importlib.resources.files(__package__) doveauth_path = importlib.resources.files(__package__).joinpath(
.joinpath("chatctl/chatctl.py") f"../../../doveauth/dist/{doveauth_filename}"
.open("rb"),
dest="/home/vmail/chatctl",
user="vmail",
group="vmail",
mode="755",
) )
remote_path = f"/tmp/{doveauth_filename}"
if Path(str(doveauth_path)).exists():
files.put(
name="upload local doveauth build",
src=doveauth_path.open("rb"),
dest=remote_path,
)
apt.packages(
name="apt install python3-pip",
packages="python3-pip",
)
# Maybe if we introduce dependencies to the doveauth package at some point, we should not install doveauth
# system-wide anymore. For now it's fine though.
server.shell(
name="install local doveauth build with pip",
commands=[f"pip install --break-system-packages {remote_path}"],
)
def _configure_opendkim(domain: str, dkim_selector: str) -> bool: def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
@@ -104,8 +116,8 @@ def _configure_dovecot(mail_server: str) -> bool:
# luarocks install http lpeg_patterns fifo # luarocks install http lpeg_patterns fifo
auth_script = files.put( auth_script = files.put(
src=importlib.resources.files(__package__).joinpath("dovecot/auth.lua"), src=importlib.resources.files("doveauth").joinpath("doveauth.lua"),
dest="/etc/dovecot/auth.lua", dest="/etc/dovecot/doveauth.lua",
user="root", user="root",
group="root", group="root",
mode="644", mode="644",
@@ -118,7 +130,7 @@ def _configure_dovecot(mail_server: str) -> bool:
def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None: def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None:
"""Deploy a chat-mail instance. """Deploy a chat-mail instance.
:param mail_domain: the domain part of your future email addresses, so "example.org" in user@example.org :param mail_domain: domain part of your future email addresses
:param mail_server: the DNS name under which your mail server is reachable :param mail_server: the DNS name under which your mail server is reachable
:param dkim_selector: :param dkim_selector:
""" """
@@ -160,7 +172,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
], ],
) )
_install_chatctl() _install_doveauth()
dovecot_need_restart = _configure_dovecot(mail_server) dovecot_need_restart = _configure_dovecot(mail_server)
postfix_need_restart = _configure_postfix(mail_domain) postfix_need_restart = _configure_postfix(mail_domain)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector) opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)

View File

@@ -4,14 +4,19 @@ protocols = imap lmtp
auth_mechanisms = plain auth_mechanisms = plain
auth_verbose = yes
auth_debug = yes
auth_debug_passwords = yes
auth_verbose_passwords = plain
# Authentication for system users. # Authentication for system users.
passdb { passdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth.lua args = file=/etc/dovecot/doveauth.lua
} }
userdb { userdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth.lua args = file=/etc/dovecot/doveauth.lua
} }
## ##
@@ -83,7 +88,7 @@ service auth {
service auth-worker { service auth-worker {
# Default is root. # Default is root.
# Drop privileges we don't need. # Drop privileges we don't need.
user = $default_internal_user user = vmail
} }
ssl = required ssl = required

View File

@@ -1,5 +1,5 @@
import os import os
from pyinfra import host, facts import pyinfra
from chatmail import deploy_chatmail from chatmail import deploy_chatmail
@@ -8,7 +8,12 @@ def main():
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain) mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023") dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "2023")
assert mail_domain
assert mail_server
assert dkim_selector
deploy_chatmail(mail_domain, mail_server, dkim_selector) deploy_chatmail(mail_domain, mail_server, dkim_selector)
main() if pyinfra.is_cli:
main()

7
doveauth/README.md Normal file
View File

@@ -0,0 +1,7 @@
# doveauth
doveauth is a python tool
to create dovecot users on login.
It is called by the
[dovecot lua authentication module](https://doc.dovecot.org/configuration_manual/authentication/lua_based_authentication/)

30
doveauth/pyproject.toml Normal file
View File

@@ -0,0 +1,30 @@
[build-system]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "doveauth"
version = "0.1"
[project.scripts]
doveauth = "doveauth.doveauth: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,153 @@
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
def set_config(self, name: str, value: str) -> str:
ok = [
"dbversion",
]
assert name in ok, name
q = "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)"
self.cursor().execute(q, (name, value)).fetchone()
return value
def get_config(self, key: str) -> str:
q = "SELECT key, value from config WHERE name = ?"
c = self._sqlconn.cursor()
try:
return c.execute(q, key).fetchone()
except sqlite3.OperationalError:
return None
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)
CURRENT_DBVERSION = 1
def ensure_tables(self):
with self.write_transaction() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS users (
addr TEXT PRIMARY KEY,
password TEXT,
last_login INTEGER
)
""",
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT
)
""",
)
conn.set_config("dbversion", self.CURRENT_DBVERSION)

View File

@@ -0,0 +1,59 @@
-- 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 = "doveauth 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)
local cmd = "doveauth hexlookup "..escape(user)
assert(user)
print("executing: "..cmd)
local handle = io.popen(cmd)
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

@@ -0,0 +1,65 @@
#!/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

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

@@ -0,0 +1,25 @@
import subprocess
import pytest
from doveauth.doveauth import get_user_data, verify_user, Database
def test_basic(tmpdir):
db = Database(tmpdir / "passdb.sqlite")
verify_user(db, "link2xt@c1.testrun.org", "asdf")
data = get_user_data(db, "link2xt@c1.testrun.org")
assert data
def test_verify_or_create(tmpdir):
db = Database(tmpdir / "passdb.sqlite")
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_lua_integration(request):
p = request.fspath.dirpath("test_doveauth.lua")
proc = subprocess.run(["lua", str(p)])
assert proc.returncode == 0

View File

@@ -1,11 +0,0 @@
chatmail = [
(
"c1.testrun.org",
{
"ssh_user": "root",
"domain": "c1.testrun.org",
"dkim_selector": "2023",
},
),
]

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

@@ -0,0 +1,101 @@
import os
import io
import imaplib
import smtplib
import itertools
import pytest
@pytest.fixture
def maildomain():
return os.environ.get("CHATMAIL_DOMAIN", "c1.testrun.org")
@pytest.fixture
def imap(maildomain):
return ImapConn(maildomain)
class ImapConn:
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:
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
def gencreds(maildomain):
count = itertools.count()
def gen():
while 1:
num = next(count)
yield f"user{num}@{maildomain}", f"password{num}"
return lambda: next(gen())
#
# 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
self.gencreds = gencreds
self._addr2files = {}
def get_liveconfig_producer(self):
while 1:
user, password = self.gencreds()
config = {"addr": user, "mail_pw": password}
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, maildomain, gencreds, tmpdir, data):
# 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)
yield am
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
if testprocess.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)

View File

@@ -0,0 +1,13 @@
class TestMailSending:
def test_one_on_one(self, cmfactory, lp):
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"

View File

@@ -0,0 +1,42 @@
import pytest
import imaplib
import smtplib
class TestDovecot:
def test_login_ok(self, imap, gencreds):
user, password = gencreds()
imap.connect()
imap.login(user, password)
# verify it works on another connection
imap.connect()
imap.login(user, password)
def test_login_fail(self, imap, gencreds):
user, password = gencreds()
imap.connect()
imap.login(user, password)
imap.connect()
with pytest.raises(imaplib.IMAP4.error) as excinfo:
imap.login(user, password + "wrong")
assert "AUTHENTICATIONFAILED" in str(excinfo)
class TestPostfix:
def test_login_ok(self, smtp, gencreds):
user, password = gencreds()
smtp.connect()
smtp.login(user, password)
# verify it works on another connection
smtp.connect()
smtp.login(user, password)
def test_login_fail(self, smtp, gencreds):
user, password = gencreds()
smtp.connect()
smtp.login(user, password)
smtp.connect()
with pytest.raises(smtplib.SMTPAuthenticationError) as excinfo:
smtp.login(user, password + "wrong")
assert excinfo.value.smtp_code == 535
assert "authentication failed" in str(excinfo)

37
plan.txt Normal file
View File

@@ -0,0 +1,37 @@
# Chat-mail server development (up until Oct 18th)
## Dovecot goals/steps
1. create-user-on-login ("doveauth")
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
## Postfix goals/steps
1. block all outgoing mails with our own LMTP program
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")
## online tests (first with plain python/pytest)
- write tests for dovecot login (exists)
- write tests for postfix logins
- write A<>B send/receive tests
## Delta Chat
1. qr code that defines access to a chatmail instance (like mailadm but without http etc.)
2. support for creating username/password and verifying login works

View File

@@ -1,10 +0,0 @@
[build-system]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "chatmail"
version = "0.1"
dependencies = [
"pyinfra",
]

7
scripts/deploy.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
export CHATMAIL_DOMAIN
cd doveauth
venv/bin/python3 -m build
../chatmail-pyinfra/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" ../deploy.py
rm -r dist/

12
scripts/init.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
python3 -m venv chatmail-pyinfra/venv
chatmail-pyinfra/venv/bin/pip install pyinfra pytest
chatmail-pyinfra/venv/bin/pip install -e chatmail-pyinfra
chatmail-pyinfra/venv/bin/pip install -e doveauth
python3 -m venv doveauth/venv
doveauth/venv/bin/pip install pytest build
doveauth/venv/bin/pip install -e doveauth
python3 -m venv online-tests/venv
online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat

6
scripts/test.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
pushd doveauth/src/doveauth
../../venv/bin/pytest
popd
online-tests/venv/bin/pytest online-tests/ -vrx --durations=5

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env python3
import base64
import sys
if sys.argv[1] == "hexauth":
login = base64.b16decode(sys.argv[2])
password = base64.b16decode(sys.argv[3])
if login == b"link2xt@instant2.testrun.org" and password == b"Ahyei6ie":
sys.exit(0)
else:
sys.exit(1)
elif sys.argv[1] == "hexlookup":
login = base64.b16decode(sys.argv[2])
if login == b"link2xt@instant2.testrun.org":
sys.exit(0)
else:
sys.exit(1)

View File

@@ -1,35 +0,0 @@
-- Lua based authentication script for Dovecot.
--
-- It calls external chatctl command to answer requests.
-- Hexadecimal aka base16 encoding.
function hex(data)
return (data:gsub(".", function(char) return string.format("%2X", char:byte()) end))
end
-- Escape shell argument by hex encoding it and wrapping in quotes.
function escape(data)
return ("'"..hex(data).."'")
end
function auth_password_verify(request, password)
if os.execute("/home/vmail/chatctl hexauth "..escape(request.user).." "..escape(password)) then
return dovecot.auth.PASSDB_RESULT_OK, {}
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, ""
end
function auth_passdb_lookup(request)
if os.execute("/home/vmail/chatctl hexlookup "..escape(request.user)) then
return dovecot.auth.PASSDB_RESULT_OK, {}
end
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "no such user"
end
function auth_userdb_lookup(request)
if os.execute("/home/vmail/chatctl hexlookup "..escape(request.user)) then
return dovecot.auth.USERDB_RESULT_OK, "uid=vmail gid=vmail"
end
return dovecot.auth.USERDB_RESULT_USER_UNKNOWN, "no such user"
end

13
tox.ini
View File

@@ -1,13 +0,0 @@
[tox]
isolated_build = true
envlist = lint
[testenv:lint]
skipdist = True
skip_install = True
deps =
ruff
black
commands =
black --quiet --check --diff src/
ruff src/