mirror of
https://github.com/chatmail/relay.git
synced 2026-05-12 17:14:36 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77727e259e | ||
|
|
732fdb3dab | ||
|
|
fe648f4784 | ||
|
|
d43e046c5d | ||
|
|
3716f2e429 |
21
.github/workflows/ci.yaml
vendored
21
.github/workflows/ci.yaml
vendored
@@ -5,14 +5,27 @@ on:
|
|||||||
push:
|
push:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
tox:
|
||||||
name: Lint
|
name: chatmail tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Lint chatmaild
|
- name: run chatmaild tests
|
||||||
working-directory: chatmaild
|
working-directory: chatmaild
|
||||||
run: pipx run tox
|
run: pipx run tox
|
||||||
- name: Lint deploy-chatmail
|
- name: run deploy-chatmail offline tests
|
||||||
working-directory: deploy-chatmail
|
working-directory: deploy-chatmail
|
||||||
run: pipx run tox
|
run: pipx run tox
|
||||||
|
- name: run deploy-chatmail offline tests
|
||||||
|
working-directory: deploy-chatmail
|
||||||
|
run: pipx run tox
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
name: chatmail script invocations
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: run init.sh
|
||||||
|
run: ./scripts/init.sh
|
||||||
|
- name: run test.sh
|
||||||
|
run: ./scripts/test.sh
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ addopts = "-v -ra --strict-markers"
|
|||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
[tox]
|
[tox]
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
envlist = lint
|
envlist = lint,py
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
skipdist = True
|
skipdist = True
|
||||||
@@ -31,4 +31,10 @@ deps =
|
|||||||
commands =
|
commands =
|
||||||
black --quiet --check --diff src/
|
black --quiet --check --diff src/
|
||||||
ruff src/
|
ruff src/
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
passenv = CHATMAIL_DOMAIN
|
||||||
|
deps = pytest
|
||||||
|
pdbpp
|
||||||
|
commands = pytest -v -rsXx {posargs: ../tests/chatmaild}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,68 +21,66 @@ def encrypt_password(password: str):
|
|||||||
return "{SHA512-CRYPT}" + passhash
|
return "{SHA512-CRYPT}" + passhash
|
||||||
|
|
||||||
|
|
||||||
class DictProxy:
|
def create_user(db, user, password):
|
||||||
def __init__(self, db, mail_domain):
|
if os.path.exists(NOCREATE_FILE):
|
||||||
self.db = db
|
logging.warning(
|
||||||
self.mail_domain = mail_domain
|
f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation."
|
||||||
|
)
|
||||||
def create_user(self, user, password):
|
return
|
||||||
if os.path.exists(NOCREATE_FILE):
|
with db.write_transaction() as conn:
|
||||||
logging.warning(f"Didn't create account: {NOCREATE_FILE} exists.")
|
conn.create_user(user, password)
|
||||||
return
|
return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
|
||||||
with self.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(self, user):
|
|
||||||
with self.db.read_connection() as conn:
|
|
||||||
result = conn.get_user(user)
|
|
||||||
if result:
|
|
||||||
result["uid"] = "vmail"
|
|
||||||
result["gid"] = "vmail"
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def lookup_userdb(self, user):
|
def get_user_data(db, user):
|
||||||
return self.get_user_data(user)
|
with db.read_connection() as conn:
|
||||||
|
result = conn.get_user(user)
|
||||||
|
if result:
|
||||||
|
result["uid"] = "vmail"
|
||||||
|
result["gid"] = "vmail"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def lookup_passdb(self, user, password):
|
def lookup_userdb(db, user):
|
||||||
userdata = self.get_user_data(user)
|
return get_user_data(db, user)
|
||||||
if not userdata:
|
|
||||||
return self.create_user(user, encrypt_password(password))
|
|
||||||
userdata["password"] = userdata["password"].strip()
|
|
||||||
return userdata
|
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_request(self, msg):
|
def lookup_passdb(db, user, password):
|
||||||
print(f"received msg: {msg!r}", file=sys.stderr)
|
userdata = get_user_data(db, user)
|
||||||
short_command = msg[0]
|
if not userdata:
|
||||||
if short_command == "L": # LOOKUP
|
return create_user(db, user, encrypt_password(password))
|
||||||
parts = msg[1:].split("\t")
|
userdata["password"] = userdata["password"].strip()
|
||||||
keyname, user = parts[:2]
|
return userdata
|
||||||
namespace, type, *args = keyname.split("/")
|
|
||||||
reply_command = "F"
|
|
||||||
res = ""
|
def handle_dovecot_request(msg, db, mail_domain):
|
||||||
if namespace == "shared":
|
print(f"received msg: {msg!r}", file=sys.stderr)
|
||||||
if type == "userdb":
|
short_command = msg[0]
|
||||||
if user.endswith(f"@{self.mail_domain}"):
|
if short_command == "L": # LOOKUP
|
||||||
res = lookup_userdb(db, user)
|
parts = msg[1:].split("\t")
|
||||||
if res:
|
keyname, user = parts[:2]
|
||||||
reply_command = "O"
|
namespace, type, *args = keyname.split("/")
|
||||||
else:
|
reply_command = "F"
|
||||||
reply_command = "N"
|
res = ""
|
||||||
elif type == "passdb":
|
if namespace == "shared":
|
||||||
if user.endswith(f"@{self.mail_domain}"):
|
if type == "userdb":
|
||||||
res = lookup_passdb(db, user, password=args[0])
|
if user.endswith(f"@{mail_domain}"):
|
||||||
if res:
|
res = lookup_userdb(db, user)
|
||||||
reply_command = "O"
|
if res:
|
||||||
else:
|
reply_command = "O"
|
||||||
reply_command = "N"
|
else:
|
||||||
print(f"res: {res!r}", file=sys.stderr)
|
reply_command = "N"
|
||||||
json_res = json.dumps(res) if res else ""
|
elif type == "passdb":
|
||||||
return f"{reply_command}{json_res}\n"
|
if user.endswith(f"@{mail_domain}"):
|
||||||
return None
|
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):
|
class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
|
||||||
@@ -92,18 +90,17 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
|
|||||||
def main():
|
def main():
|
||||||
socket = sys.argv[1]
|
socket = sys.argv[1]
|
||||||
passwd_entry = pwd.getpwnam(sys.argv[2])
|
passwd_entry = pwd.getpwnam(sys.argv[2])
|
||||||
|
db = Database(sys.argv[3])
|
||||||
with open("/etc/mailname", "r") as fp:
|
with open("/etc/mailname", "r") as fp:
|
||||||
mail_domain = fp.read().strip()
|
mail_domain = fp.read().strip()
|
||||||
|
|
||||||
db = Database(sys.argv[3])
|
|
||||||
dictproxy = DictProxy(db, mail_domain)
|
|
||||||
class Handler(StreamRequestHandler):
|
class Handler(StreamRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
while True:
|
while True:
|
||||||
msg = self.rfile.readline().strip().decode()
|
msg = self.rfile.readline().strip().decode()
|
||||||
if not msg:
|
if not msg:
|
||||||
break
|
break
|
||||||
res = dictproxy.handle_dovecot_request(msg)
|
res = handle_dovecot_request(msg, db, mail_domain)
|
||||||
if res:
|
if res:
|
||||||
print(f"sending result: {res!r}", file=sys.stderr)
|
print(f"sending result: {res!r}", file=sys.stderr)
|
||||||
self.wfile.write(res.encode("ascii"))
|
self.wfile.write(res.encode("ascii"))
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ conn.login(f"imapcapa", "pass")
|
|||||||
status, res = conn.capability()
|
status, res = conn.capability()
|
||||||
for capa in sorted(res[0].decode().split()):
|
for capa in sorted(res[0].decode().split()):
|
||||||
print(capa)
|
print(capa)
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
python3 -m venv deploy-chatmail/venv
|
python3 -m venv venv
|
||||||
deploy-chatmail/venv/bin/pip install pyinfra pytest
|
pip=venv/bin/pip
|
||||||
deploy-chatmail/venv/bin/pip install -e deploy-chatmail
|
|
||||||
deploy-chatmail/venv/bin/pip install -e chatmaild
|
|
||||||
|
|
||||||
python3 -m venv chatmaild/venv
|
$pip install pyinfra pytest build 'setuptools>=68' tox deltachat
|
||||||
chatmaild/venv/bin/pip install --upgrade pytest build 'setuptools>=68'
|
$pip install -e deploy-chatmail
|
||||||
chatmaild/venv/bin/pip install -e chatmaild
|
$pip install -e chatmaild
|
||||||
|
|
||||||
python3 -m venv online-tests/venv
|
|
||||||
online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import imaplib
|
|||||||
|
|
||||||
domain = os.environ.get("CHATMAIL_DOMAIN", "c3.testrun.org")
|
domain = os.environ.get("CHATMAIL_DOMAIN", "c3.testrun.org")
|
||||||
|
|
||||||
NUM_CONNECTIONS = 10
|
NUM_CONNECTIONS=10
|
||||||
|
|
||||||
conns = []
|
conns = []
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ for i in range(NUM_CONNECTIONS):
|
|||||||
conns.append(conn)
|
conns.append(conn)
|
||||||
|
|
||||||
tlsdone = time.time()
|
tlsdone = time.time()
|
||||||
duration = tlsdone - start
|
duration = tlsdone-start
|
||||||
print(f"{duration}: TLS connections opening TLS connections")
|
print(f"{duration}: TLS connections opening TLS connections")
|
||||||
|
|
||||||
for i, conn in enumerate(conns):
|
for i, conn in enumerate(conns):
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
chatmaild/venv/bin/pytest chatmaild/ $@
|
tox -c chatmaild
|
||||||
online-tests/venv/bin/pytest online-tests/ -vrx --durations=5 $@
|
tox -c deploy-chatmail
|
||||||
|
venv/bin/pytest tests/online -vrx --durations=5 $@
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from chatmaild.database import Database
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def db(tmpdir):
|
|
||||||
db_path = tmpdir / "passdb.sqlite"
|
|
||||||
print("database path:", db_path)
|
|
||||||
return Database(db_path)
|
|
||||||
@@ -3,38 +3,43 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import chatmaild.dictproxy
|
import chatmaild.dictproxy
|
||||||
from chatmaild.dictproxy import DictProxy
|
from chatmaild.dictproxy import get_user_data, lookup_passdb
|
||||||
from chatmaild.database import DBError
|
from chatmaild.database import Database, DBError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture()
|
||||||
def dictproxy(db, maildomain):
|
def db(tmpdir):
|
||||||
return DictProxy(db, maildomain)
|
db_path = tmpdir / "passdb.sqlite"
|
||||||
|
print("database path:", db_path)
|
||||||
|
return Database(db_path)
|
||||||
|
|
||||||
|
|
||||||
def test_basic(dictproxy, tmpdir, monkeypatch):
|
def test_basic(db):
|
||||||
monkeypatch.setattr(
|
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
|
||||||
chatmaild.dictproxy, "NOCREATE_FILE", tmpdir.join("nocreate").strpath
|
if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE):
|
||||||
)
|
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
|
||||||
dictproxy.lookup_passdb("link2xt@c1.testrun.org", "asdf")
|
lookup_passdb(db, "link2xt@c1.testrun.org", "asdf")
|
||||||
assert dictproxy.get_user_data("link2xt@c1.testrun.org")
|
data = get_user_data(db, "link2xt@c1.testrun.org")
|
||||||
|
assert data
|
||||||
|
|
||||||
|
|
||||||
def test_dont_overwrite_password_on_wrong_login(dictproxy):
|
def test_dont_overwrite_password_on_wrong_login(db):
|
||||||
"""Test that logging in with a different password doesn't create a new user"""
|
"""Test that logging in with a different password doesn't create a new user"""
|
||||||
res = dictproxy.lookup_passdb("newuser1@something.org", "kajdlkajsldk12l3kj1983")
|
res = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
|
||||||
assert res["password"]
|
assert res["password"]
|
||||||
res2 = dictproxy.lookup_passdb("newuser1@something.org", "kajdlqweqwe")
|
res2 = lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
|
||||||
# this function always returns a password hash, which is actually compared by dovecot.
|
# this function always returns a password hash, which is actually compared by dovecot.
|
||||||
assert res["password"] == res2["password"]
|
assert res["password"] == res2["password"]
|
||||||
|
|
||||||
|
|
||||||
def test_nocreate_file(dictproxy, tmpdir, monkeypatch):
|
def test_nocreate_file(db):
|
||||||
nocreate = tmpdir.join("nocreate")
|
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
|
||||||
monkeypatch.setattr(chatmaild.dictproxy, "NOCREATE_FILE", str(nocreate))
|
with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f:
|
||||||
nocreate.write("")
|
f.write("")
|
||||||
dictproxy.lookup_passdb("newuser1@something.org", "kajdlqweqwe")
|
assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE)
|
||||||
assert not dictproxy.get_user_data("newuser1@something.org")
|
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):
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ from chatmaild.filtermail import check_encrypted, check_DATA, SendRateLimiter
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def maildomain():
|
||||||
|
# let's not depend on a real chatmail instance for the offline tests below
|
||||||
|
return "chatmail.example.org"
|
||||||
|
|
||||||
|
|
||||||
def test_reject_forged_from(maildata, gencreds):
|
def test_reject_forged_from(maildata, gencreds):
|
||||||
class env:
|
class env:
|
||||||
mail_from = gencreds()[0]
|
mail_from = gencreds()[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user