mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 08:24:37 +00:00
Compare commits
1 Commits
hpk/less-r
...
link2xt/cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1ddf4d9e0 |
@@ -81,11 +81,10 @@ comprised of minimal setups of
|
||||
|
||||
as well as two custom services that are integrated with these two:
|
||||
|
||||
- `chatmaild/src/chatmaild/doveauth.py` implements
|
||||
- `chatmaild/src/chatmaild/dictproxy.py` implements
|
||||
create-on-login account creation semantics and is used
|
||||
by Dovecot during login authentication and by Postfix
|
||||
which in turn uses [Dovecot SASL](https://doc.dovecot.org/configuration_manual/authentication/dict/#complete-example-for-authenticating-via-a-unix-socket)
|
||||
to authenticate users
|
||||
which in turn uses Dovecot SASL to authenticate users
|
||||
to send mails for them.
|
||||
|
||||
- `chatmaild/src/chatmaild/filtermail.py` prevents
|
||||
|
||||
@@ -10,7 +10,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
doveauth = "chatmaild.doveauth:main"
|
||||
doveauth-dictproxy = "chatmaild.dictproxy:main"
|
||||
filtermail = "chatmaild.filtermail:main"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=Dict authentication proxy for dovecot
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/doveauth /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite
|
||||
ExecStart=/usr/local/bin/doveauth-dictproxy /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
@@ -7,7 +7,6 @@ from pathlib import Path
|
||||
from pyinfra import host
|
||||
from pyinfra.operations import apt, files, server, systemd
|
||||
from pyinfra.facts.files import File
|
||||
from pyinfra.facts.systemd import SystemdEnabled
|
||||
from .acmetool import deploy_acmetool
|
||||
|
||||
|
||||
@@ -35,17 +34,8 @@ def _install_chatmaild() -> None:
|
||||
commands=[f"pip install --break-system-packages {remote_path}"],
|
||||
)
|
||||
|
||||
# disable legacy doveauth-dictproxy.service
|
||||
if host.get_fact(SystemdEnabled).get("doveauth-dictproxy.service"):
|
||||
systemd.service(
|
||||
name="Disable legacy doveauth-dictproxy.service",
|
||||
service="doveauth-dictproxy.service",
|
||||
running=False,
|
||||
enabled=False,
|
||||
)
|
||||
|
||||
for fn in (
|
||||
"doveauth",
|
||||
"doveauth-dictproxy",
|
||||
"filtermail",
|
||||
):
|
||||
files.put(
|
||||
|
||||
@@ -19,7 +19,7 @@ mail_plugins = quota
|
||||
# these are the capabilities Delta Chat cares about actually
|
||||
# so let's keep the network overhead per login small
|
||||
# https://github.com/deltachat/deltachat-core-rust/blob/master/src/imap/capabilities.rs
|
||||
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY
|
||||
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE
|
||||
|
||||
|
||||
# Authentication for system users.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
venv/bin/pytest tests/online/benchmark.py -vrx
|
||||
venv/bin/pytest online-tests/benchmark.py -vrx
|
||||
|
||||
@@ -15,6 +15,6 @@ _submission._tcp.$CHATMAIL_DOMAIN. SRV 0 1 587 $CHATMAIL_DOMAIN.
|
||||
_submissions._tcp.$CHATMAIL_DOMAIN. SRV 0 1 465 $CHATMAIL_DOMAIN.
|
||||
_imap._tcp.$CHATMAIL_DOMAIN. SRV 0 1 143 $CHATMAIL_DOMAIN.
|
||||
_imaps._tcp.$CHATMAIL_DOMAIN. SRV 0 1 993 $CHATMAIL_DOMAIN.
|
||||
$CHATMAIL_DOMAIN. IN CAA 128 issue "letsencrypt.org; accounturi=$ACME_ACCOUNT_URL"
|
||||
$CHATMAIL_DOMAIN. IN CAA 0 issue "letsencrypt.org; accounturi=$ACME_ACCOUNT_URL"
|
||||
EOF
|
||||
$SSH opendkim-genzone -F | sed 's/^;.*$//;/^$/d'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
venv/bin/tox -c chatmaild
|
||||
venv/bin/tox -c deploy-chatmail
|
||||
venv/bin/pytest tests/online -rs -vrx --durations=5 $@
|
||||
venv/bin/pytest tests/online -vrx --durations=5 $@
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import threading
|
||||
import queue
|
||||
import traceback
|
||||
|
||||
import chatmaild.doveauth
|
||||
from chatmaild.doveauth import get_user_data, lookup_passdb, handle_dovecot_request
|
||||
import chatmaild.dictproxy
|
||||
from chatmaild.dictproxy import get_user_data, lookup_passdb, handle_dovecot_request
|
||||
from chatmaild.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):
|
||||
lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
|
||||
data = get_user_data(db, "link2xt@c1.testrun.org")
|
||||
@@ -30,7 +36,7 @@ def test_dont_overwrite_password_on_wrong_login(db):
|
||||
def test_nocreate_file(db, monkeypatch, tmpdir):
|
||||
p = tmpdir.join("nocreate")
|
||||
p.write("")
|
||||
monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p))
|
||||
monkeypatch.setattr(chatmaild.dictproxy, "NOCREATE_FILE", str(p))
|
||||
lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik")
|
||||
assert not get_user_data(db, "newuser1@something.org")
|
||||
|
||||
@@ -47,10 +53,8 @@ def test_too_high_db_version(db):
|
||||
|
||||
|
||||
def test_handle_dovecot_request(db):
|
||||
msg = (
|
||||
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
|
||||
"some42@c3.testrun.org\tsome42@c3.testrun.org"
|
||||
)
|
||||
msg = ('Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/'
|
||||
'some42@c3.testrun.org\tsome42@c3.testrun.org')
|
||||
res = handle_dovecot_request(msg, db, "c3.testrun.org")
|
||||
assert res
|
||||
assert res[0] == "O" and res.endswith("\n")
|
||||
@@ -58,29 +62,3 @@ def test_handle_dovecot_request(db):
|
||||
assert userdata["home"] == "/home/vmail/some42@c3.testrun.org"
|
||||
assert userdata["uid"] == userdata["gid"] == "vmail"
|
||||
assert userdata["password"].startswith("{SHA512-CRYPT}")
|
||||
|
||||
|
||||
def test_100_concurrent_lookups(db):
|
||||
num = 100
|
||||
dbs = [Database(db.path) for i in range(num)]
|
||||
print(f"created {num} databases")
|
||||
results = queue.Queue()
|
||||
|
||||
def lookup(db):
|
||||
try:
|
||||
lookup_passdb(db, "something@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
|
||||
except Exception:
|
||||
results.put(traceback.format_exc())
|
||||
else:
|
||||
results.put(None)
|
||||
|
||||
threads = [threading.Thread(target=lookup, args=(db,), daemon=True) for db in dbs]
|
||||
|
||||
print(f"created {num} threads, starting them and waiting for results")
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
|
||||
for _ in dbs:
|
||||
res = results.get()
|
||||
if res is not None:
|
||||
pytest.fail(f"concurrent lookup failed\n{res}")
|
||||
@@ -9,10 +9,9 @@ import itertools
|
||||
from email.parser import BytesParser
|
||||
from email import policy
|
||||
from pathlib import Path
|
||||
from math import ceil
|
||||
import pytest
|
||||
|
||||
from chatmaild.database import Database
|
||||
|
||||
|
||||
conftestdir = Path(__file__).parent
|
||||
|
||||
@@ -72,7 +71,7 @@ def pytest_report_header():
|
||||
|
||||
@pytest.fixture
|
||||
def benchmark(request):
|
||||
def bench(func, num, name=None, reportfunc=None):
|
||||
def bench(func, num, name=None):
|
||||
if name is None:
|
||||
name = func.__name__
|
||||
durations = []
|
||||
@@ -81,7 +80,7 @@ def benchmark(request):
|
||||
func()
|
||||
durations.append(time.time() - now)
|
||||
durations.sort()
|
||||
request.config._benchresults[name] = (reportfunc, durations)
|
||||
request.config._benchresults[name] = durations
|
||||
|
||||
return bench
|
||||
|
||||
@@ -102,9 +101,7 @@ def pytest_terminal_summary(terminalreporter):
|
||||
headers = f"{'benchmark name': <30} " + fcol(float_names)
|
||||
tr.write_line(headers)
|
||||
tr.write_line("-" * len(headers))
|
||||
summary_lines = []
|
||||
|
||||
for name, (reportfunc, durations) in results.items():
|
||||
for name, durations in results.items():
|
||||
measures = [
|
||||
sorted(durations)[len(durations) // 2],
|
||||
min(durations),
|
||||
@@ -113,16 +110,6 @@ def pytest_terminal_summary(terminalreporter):
|
||||
line = f"{name: <30} "
|
||||
line += fcol(f"{float: 2.2f}" for float in measures)
|
||||
tr.write_line(line)
|
||||
vmedian, vmin, vmax = measures
|
||||
if reportfunc:
|
||||
for line in reportfunc(vmin=vmin, vmedian=vmedian, vmax=vmax):
|
||||
summary_lines.append(line)
|
||||
|
||||
if summary_lines:
|
||||
tr.write_line("")
|
||||
tr.section("benchmark summary measures")
|
||||
for line in summary_lines:
|
||||
tr.write_line(line)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -130,16 +117,6 @@ def imap(maildomain):
|
||||
return ImapConn(maildomain)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_imap_connection(maildomain):
|
||||
def make_imap_connection():
|
||||
conn = ImapConn(maildomain)
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
return make_imap_connection
|
||||
|
||||
|
||||
class ImapConn:
|
||||
AuthError = imaplib.IMAP4.error
|
||||
logcmd = "journalctl -f -u dovecot"
|
||||
@@ -180,16 +157,6 @@ def smtp(maildomain):
|
||||
return SmtpConn(maildomain)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_smtp_connection(maildomain):
|
||||
def make_smtp_connection():
|
||||
conn = SmtpConn(maildomain)
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
return make_smtp_connection
|
||||
|
||||
|
||||
class SmtpConn:
|
||||
AuthError = smtplib.SMTPAuthenticationError
|
||||
logcmd = "journalctl -f -t postfix/smtpd -t postfix/smtp -t postfix/lmtp"
|
||||
@@ -235,13 +202,6 @@ def gencreds(maildomain):
|
||||
return lambda domain=None: next(gen(domain))
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def db(tmpdir):
|
||||
db_path = tmpdir / "passdb.sqlite"
|
||||
print("database path:", db_path)
|
||||
return Database(db_path)
|
||||
|
||||
|
||||
#
|
||||
# Delta Chat testplugin re-use
|
||||
# use the cmfactory fixture to get chatmail instance accounts
|
||||
@@ -312,7 +272,7 @@ class Remote:
|
||||
self.sshdomain = sshdomain
|
||||
|
||||
def iter_output(self, logcmd=""):
|
||||
getjournal = "journalctl -f" if not logcmd else logcmd
|
||||
getjournal = f"journalctl -f" if not logcmd else logcmd
|
||||
self.popen = subprocess.Popen(
|
||||
["ssh", f"root@{self.sshdomain}", getjournal],
|
||||
stdout=subprocess.PIPE,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import pytest
|
||||
import threading
|
||||
import queue
|
||||
import smtplib
|
||||
|
||||
|
||||
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
||||
@@ -24,7 +23,7 @@ def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
||||
with pytest.raises(imap_or_smtp.AuthError):
|
||||
imap_or_smtp.login(user, password + "wrong")
|
||||
|
||||
lp.sec("creating users with a short password is not allowed")
|
||||
lp.sec(f"creating users with a short password is not allowed")
|
||||
user, _password = gencreds()
|
||||
with pytest.raises(imap_or_smtp.AuthError):
|
||||
imap_or_smtp.login(user, "admin")
|
||||
@@ -41,30 +40,3 @@ def test_login_same_password(imap_or_smtp, gencreds):
|
||||
imap_or_smtp.login(user1, password1)
|
||||
imap_or_smtp.connect()
|
||||
imap_or_smtp.login(user2, password1)
|
||||
|
||||
|
||||
def test_concurrent_logins_same_account(
|
||||
make_imap_connection, make_smtp_connection, gencreds
|
||||
):
|
||||
"""Test concurrent smtp and imap logins
|
||||
and check remote server succeeds on each connection.
|
||||
"""
|
||||
user1, password1 = gencreds()
|
||||
login_results = queue.Queue()
|
||||
|
||||
def login_smtp_imap(smtp, imap):
|
||||
try:
|
||||
imap.login(user1, password1)
|
||||
except Exception:
|
||||
login_results.put(False)
|
||||
else:
|
||||
login_results.put(True)
|
||||
|
||||
conns = [(make_smtp_connection(), make_imap_connection()) for i in range(10)]
|
||||
|
||||
for args in conns:
|
||||
thread = threading.Thread(target=login_smtp_imap, args=args, daemon=True)
|
||||
thread.start()
|
||||
|
||||
for _ in conns:
|
||||
assert login_results.get()
|
||||
|
||||
@@ -91,7 +91,7 @@ class TestEndToEndDeltaChat:
|
||||
|
||||
lp.sec("setup encrypted comms between ac1 and ac2 on different instances")
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
ac2.qr_setup_contact(qr)
|
||||
ch = ac2.qr_setup_contact(qr)
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
assert "verified" in msg.text
|
||||
|
||||
|
||||
@@ -20,14 +20,15 @@
|
||||
box-sizing: border-box;
|
||||
padding: 9px;
|
||||
font-size: 18px;
|
||||
font-family: "Swansea", "Helvetica", sans-serif;
|
||||
color: black;
|
||||
}
|
||||
a {
|
||||
color: black;
|
||||
font-family: "Courier New", monospace;
|
||||
color: white;
|
||||
background-position: left top;
|
||||
background-image: url(collage-bg.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
h1, h2, h3 {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -36,39 +37,24 @@
|
||||
<div class="wrapper">
|
||||
<img class="section" src="collage-top.png" />
|
||||
<div class="section text">
|
||||
<h1>Dear Delta Chat users and newcomers,</h1>
|
||||
<h1>welcome to nine.testrun.org</h1>
|
||||
<p>
|
||||
welcome to the first public "chat-mail instance",
|
||||
a small and lean e-mail provider for smooth chatting.
|
||||
Install Delta Chat or add an account:
|
||||
<ul>
|
||||
<li>Tap "LOG INTO YOUR E-MAIL ACCOUNT".</li>
|
||||
<li>Address: invent a word with <i>exactly</i> nine characters
|
||||
and append @nine.testrun.org to it.</li>
|
||||
<li>Password: invent at least 10 characters. The first login sets your password.</li>
|
||||
</ul>
|
||||
If the e-mail address is not yet taken, you'll get that account.
|
||||
to get an account,
|
||||
invent a word with <i>exactly</i> nine characters
|
||||
and append @nine.testrun.org to it.
|
||||
eg. <b>hellofits@nine.testrun.org</b>
|
||||
</p>
|
||||
<p>
|
||||
<img class="section" src="collage-down.png" />
|
||||
|
||||
<h2>What's behind it, how does it operate?</h2>
|
||||
<p>nine.testrun.org is run
|
||||
by a small group of devs and sysadmins, reachable via root@.
|
||||
They want to keep this instance running at least until end 2024.
|
||||
Current limits:
|
||||
<ul>
|
||||
<li>Un-encrypted mails can not leave the chat-mail instance.</li>
|
||||
<li>Use <a href="https://delta.chat/en/help#howtoe2ee">
|
||||
guaranteed end-to-end encryption via QR code scans</a>
|
||||
to setup contact with users outside of the chat-mail instance.
|
||||
</li>
|
||||
<li>You may send up to 60 messages per minute.</li>
|
||||
<li>Messages are unconditionally removed 40 days after arrival.</li>
|
||||
<li>Max storage per user is 100MB.</li>
|
||||
</ul>
|
||||
<h2>Why are other email providers 1000 times more complicated?</h2>
|
||||
<p>¯\_(ツ)_/¯</p>
|
||||
if the email address is not yet taken, you'll get that account.
|
||||
the first login sets your password.
|
||||
that's it.
|
||||
</p>
|
||||
</div>
|
||||
<img class="section" src="collage-down.png" />
|
||||
<div class="section text">
|
||||
<h1>faq</h1>
|
||||
<p><i>why are other email providers 1000 times more complicated?</i></p>
|
||||
<p>because they want to for $reasons</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user