Compare commits

..

3 Commits

Author SHA1 Message Date
missytake
6b6f6f1c50 tried to write a test to test exceeded quota, but not sure delta even gets a proper error on recipient's full mailbox 2023-10-16 01:51:17 +02:00
missytake
f4cf4ab955 sieve is not installed and we don't need it 2023-10-16 01:41:38 +02:00
holger krekel
48d890ee82 make quota work 2023-10-16 01:41:36 +02:00
32 changed files with 377 additions and 892 deletions

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import logging
import os
import sys
import json
@@ -12,8 +11,6 @@ import subprocess
from .database import Database
NOCREATE_FILE = "/etc/chatmail-nocreate"
def encrypt_password(password: str):
password = password.encode("ascii")
@@ -30,11 +27,6 @@ def encrypt_password(password: str):
def create_user(db, user, password):
if os.path.exists(NOCREATE_FILE):
logging.warning(
f"Didn't create account: {NOCREATE_FILE} exists. Delete the file to enable account creation."
)
return
with db.write_transaction() as conn:
conn.create_user(user, password)
return dict(home=f"/home/vmail/{user}", uid="vmail", gid="vmail", password=password)
@@ -61,7 +53,7 @@ def lookup_passdb(db, user, password):
return userdata
def handle_dovecot_request(msg, db, mail_domain):
def handle_dovecot_request(msg, db):
print(f"received msg: {msg!r}", file=sys.stderr)
short_command = msg[0]
if short_command == "L": # LOOKUP
@@ -72,15 +64,13 @@ def handle_dovecot_request(msg, db, mail_domain):
res = ""
if namespace == "shared":
if type == "userdb":
if user.endswith(f"@{mail_domain}"):
res = lookup_userdb(db, user)
res = lookup_userdb(db, user)
if res:
reply_command = "O"
else:
reply_command = "N"
elif type == "passdb":
if user.endswith(f"@{mail_domain}"):
res = lookup_passdb(db, user, password=args[0])
res = lookup_passdb(db, user, password=args[0])
if res:
reply_command = "O"
else:
@@ -99,16 +89,14 @@ def main():
socket = sys.argv[1]
passwd_entry = pwd.getpwnam(sys.argv[2])
db = Database(sys.argv[3])
with open("/etc/mailname", "r") as fp:
mail_domain = fp.read().strip()
class Handler(StreamRequestHandler):
def handle(self):
while True:
msg = self.rfile.readline().strip().decode()
if not msg:
break
res = handle_dovecot_request(msg, db, mail_domain)
continue
res = handle_dovecot_request(msg, db)
if res:
print(f"sending result: {res!r}", file=sys.stderr)
self.wfile.write(res.encode("ascii"))

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

@@ -1,19 +1,17 @@
#!/usr/bin/env python3
import asyncio
import logging
import time
import sys
from email.parser import BytesParser
from email import policy
from email.utils import parseaddr
from aiosmtpd.smtp import SMTP
from aiosmtpd.controller import Controller
from aiosmtpd.lmtp import LMTP
from aiosmtpd.controller import UnixSocketController
from smtplib import SMTP as SMTPClient
def check_encrypted(message):
def check_encrypted(content):
"""Check that the message is an OpenPGP-encrypted message."""
message = BytesParser(policy=policy.default).parsebytes(content)
if not message.is_multipart():
return False
if message.get("subject") != "...":
@@ -34,93 +32,72 @@ def check_encrypted(message):
return True
class SMTPController(Controller):
class ExampleController(UnixSocketController):
def factory(self):
return SMTP(self.handler, **self.SMTP_kwargs)
return LMTP(self.handler, **self.SMTP_kwargs)
class BeforeQueueHandler:
def __init__(self):
self.send_rate_limiter = SendRateLimiter()
async def handle_MAIL(self, server, session, envelope, address, mail_options):
logging.info(f"handle_MAIL from {address}")
envelope.mail_from = address
if not self.send_rate_limiter.is_sending_allowed(address):
return f"450 4.7.1: Too much mail from {address}"
parts = envelope.mail_from.split("@")
if len(parts) != 2:
return f"500 Invalid from address <{envelope.mail_from!r}>"
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("handle_DATA before-queue")
error = check_DATA(envelope)
if error:
return error
logging.info("re-injecting the mail that passed checks")
client = SMTPClient("localhost", "10025")
client.sendmail(envelope.mail_from, envelope.rcpt_tos, envelope.content)
return "250 OK"
logging.info("Processing DATA message from %s", envelope.mail_from)
valid_recipients = []
mail_encrypted = check_encrypted(envelope.content)
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:
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_beforequeue(port):
Controller(BeforeQueueHandler(), hostname="127.0.0.1", port=port).start()
def check_DATA(envelope):
"""the central filtering function for e-mails."""
logging.info(f"Processing DATA message from {envelope.mail_from}")
message = BytesParser(policy=policy.default).parsebytes(envelope.content)
mail_encrypted = check_encrypted(message)
_, from_addr = parseaddr(message.get("from").strip())
logging.info(f"mime-from: {from_addr} envelope-from: {envelope.mail_from!r}")
if envelope.mail_from.lower() != from_addr.lower():
return f"500 Invalid FROM <{from_addr!r}> for <{envelope.mail_from!r}>"
envelope_from_domain = from_addr.split("@").pop()
for recipient in envelope.rcpt_tos:
if envelope.mail_from == recipient:
# Always allow sending emails to self.
continue
res = recipient.split("@")
if len(res) != 2:
return f"500 Invalid address <{recipient}>"
_recipient_addr, recipient_domain = res
is_outgoing = recipient_domain != envelope_from_domain
if is_outgoing and not mail_encrypted:
is_securejoin = message.get("secure-join") in ["vc-request", "vg-request"]
if not is_securejoin:
return f"500 Invalid unencrypted mail to <{recipient}>"
class SendRateLimiter:
MAX_USER_SEND_PER_MINUTE = 80
def __init__(self):
self.addr2timestamps = {}
def is_sending_allowed(self, mail_from):
last = self.addr2timestamps.setdefault(mail_from, [])
now = time.time()
last[:] = [ts for ts in last if ts >= (now - 60)]
if len(last) <= self.MAX_USER_SEND_PER_MINUTE:
last.append(now)
return True
return False
async def asyncmain(loop):
controller = ExampleController(
ExampleHandler(), unix_socket="/var/spool/postfix/private/filtermail"
)
controller.start()
def main():
args = sys.argv[1:]
assert len(args) == 1
logging.basicConfig(level=logging.INFO)
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
task = asyncmain_beforequeue(port=int(args[0]))
loop.create_task(task)
loop.create_task(asyncmain(loop=loop))
loop.run_forever()
if __name__ == "__main__":
main()

View File

@@ -1,8 +1,8 @@
[Unit]
Description=Chatmail Postfix BeforeQeue filter
Description=Email filter for chatmail servers
[Service]
ExecStart=/usr/local/bin/filtermail 10080
ExecStart=/usr/local/bin/filtermail
Restart=always
RestartSec=30

View File

@@ -1,9 +1,7 @@
import os
import pytest
import chatmaild.dictproxy
from .dictproxy import get_user_data, lookup_passdb
from .dictproxy import get_user_data
from .doveauth import verify_user
from .database import Database, DBError
@@ -15,31 +13,16 @@ def db(tmpdir):
def test_basic(db):
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
if os.path.exists(chatmaild.dictproxy.NOCREATE_FILE):
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
lookup_passdb(db, "link2xt@c1.testrun.org", "asdf")
verify_user(db, "link2xt@c1.testrun.org", "asdf")
data = get_user_data(db, "link2xt@c1.testrun.org")
assert data
def test_dont_overwrite_password_on_wrong_login(db):
"""Test that logging in with a different password doesn't create a new user"""
res = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
assert res["password"]
res2 = lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
# this function always returns a password hash, which is actually compared by dovecot.
assert res["password"] == res2["password"]
def test_nocreate_file(db):
chatmaild.dictproxy.NOCREATE_FILE = "/tmp/nocreate"
with open(chatmaild.dictproxy.NOCREATE_FILE, "w+") as f:
f.write("")
assert os.path.exists(chatmaild.dictproxy.NOCREATE_FILE)
lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
assert not get_user_data(db, "newuser1@something.org")
os.remove(chatmaild.dictproxy.NOCREATE_FILE)
def test_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):

View File

@@ -1,52 +1,12 @@
from .filtermail import check_encrypted, check_DATA, SendRateLimiter
from email.parser import BytesParser
from email import policy
import pytest
def test_reject_forged_from():
def makemail(from_addr):
return BytesParser(policy=policy.default).parsebytes(
"\r\n".join(
[
f"From: <{from_addr}",
"To: <barbaz@c3.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",
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no",
"",
"Hi!",
"",
"",
]
).encode()
)
class envelope:
mail_from = "bob@c3.testrun.org"
rcpt_tos = ["somebody@c3.testrun.org"]
# test that the filter lets good mail through
envelope.content = makemail(envelope.mail_from).as_bytes()
assert not check_DATA(envelope=envelope)
# test that the filter rejects forged mail
envelope.content = makemail("forged@c3.testrun.org").as_bytes()
error = check_DATA(envelope=envelope)
assert "500" in error
from .filtermail import check_encrypted
def test_filtermail():
def check_encrypted_bstr(content):
message = BytesParser(policy=policy.default).parsebytes(content)
return check_encrypted(message)
assert not check_encrypted(b"foo")
assert not check_encrypted_bstr(b"foo")
assert not check_encrypted_bstr(
assert not check_encrypted(
"\r\n".join(
[
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
@@ -76,7 +36,7 @@ def test_filtermail():
).encode()
)
assert not check_encrypted_bstr(
assert not check_encrypted(
"\r\n".join(
[
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
@@ -107,7 +67,7 @@ def test_filtermail():
)
# https://xkcd.com/1181/
assert not check_encrypted_bstr(
assert not check_encrypted(
"\r\n".join(
[
"Subject: =?utf-8?q?Message_from_foobar=40c2=2Etestrun=2Eorg?=",
@@ -139,7 +99,7 @@ def test_filtermail():
).encode()
)
assert check_encrypted_bstr(
assert check_encrypted(
"\r\n".join(
[
"Subject: ...",
@@ -212,7 +172,7 @@ def test_filtermail():
).encode()
)
assert not check_encrypted_bstr(
assert not check_encrypted(
"\r\n".join(
[
"Subject: Buy Penis Enlargement at www.malicious-domain.com",
@@ -285,7 +245,7 @@ def test_filtermail():
).encode()
)
assert not check_encrypted_bstr(
assert not check_encrypted(
"\r\n".join(
[
"Subject: Message opened",
@@ -324,15 +284,3 @@ def test_filtermail():
]
).encode()
)
def test_send_rate_limiter():
limiter = SendRateLimiter()
for i in range(100):
if limiter.is_sending_allowed("some@example.org"):
if i <= SendRateLimiter.MAX_USER_SEND_PER_MINUTE:
continue
pytest.fail("limiter didn't work")
else:
assert i == SendRateLimiter.MAX_USER_SEND_PER_MINUTE + 1
break

View File

@@ -34,28 +34,43 @@ def _install_chatmaild() -> None:
commands=[f"pip install --break-system-packages {remote_path}"],
)
for fn in (
"doveauth-dictproxy",
"filtermail",
):
files.put(
name=f"Upload {fn}.service",
src=importlib.resources.files("chatmaild")
.joinpath(f"{fn}.service")
.open("rb"),
dest=f"/etc/systemd/system/{fn}.service",
user="root",
group="root",
mode="644",
)
systemd.service(
name=f"Setup {fn} service",
service=f"{fn}.service",
running=True,
enabled=True,
restarted=True,
daemon_reload=True,
)
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:
@@ -95,7 +110,7 @@ def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
return need_restart
def _configure_postfix(domain: str, debug: bool = False) -> bool:
def _configure_postfix(domain: str) -> bool:
"""Configures Postfix SMTP server."""
need_restart = False
@@ -109,20 +124,21 @@ def _configure_postfix(domain: str, debug: bool = False) -> bool:
)
need_restart |= main_config.changed
master_config = files.template(
src=importlib.resources.files(__package__).joinpath("postfix/master.cf.j2"),
master_config = files.put(
src=importlib.resources.files(__package__)
.joinpath("postfix/master.cf")
.open("rb"),
dest="/etc/postfix/master.cf",
user="root",
group="root",
mode="644",
debug=debug,
)
need_restart |= master_config.changed
return need_restart
def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
def _configure_dovecot(mail_server: str) -> bool:
"""Configures Dovecot IMAP server."""
need_restart = False
@@ -133,7 +149,6 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
group="root",
mode="644",
config={"hostname": mail_server},
debug=debug,
)
need_restart |= main_config.changed
auth_config = files.put(
@@ -145,43 +160,6 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
)
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",
)
return need_restart
def _configure_nginx(domain: str, debug: bool = False) -> bool:
"""Configures nginx HTTP server."""
need_restart = False
main_config = files.template(
src=importlib.resources.files(__package__).joinpath("nginx.conf.j2"),
dest="/etc/nginx/nginx.conf",
user="root",
group="root",
mode="644",
config={"domain_name": domain},
)
need_restart |= main_config.changed
autoconfig = files.template(
src=importlib.resources.files(__package__).joinpath("autoconfig.xml.j2"),
dest="/var/www/html/.well-known/autoconfig/mail/config-v1.1.xml",
user="root",
group="root",
mode="644",
config={"domain_name": domain},
)
need_restart |= autoconfig.changed
return need_restart
@@ -206,7 +184,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
)
# Deploy acmetool to have TLS certificates.
deploy_acmetool(nginx_hook=True, domains=[mail_server])
deploy_acmetool(domains=[mail_server])
apt.packages(
name="Install Postfix",
@@ -226,17 +204,10 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
],
)
apt.packages(
name="Install nginx",
packages=["nginx"],
)
_install_chatmaild()
debug = False
dovecot_need_restart = _configure_dovecot(mail_server, debug=debug)
postfix_need_restart = _configure_postfix(mail_domain, debug=debug)
dovecot_need_restart = _configure_dovecot(mail_server)
postfix_need_restart = _configure_postfix(mail_domain)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
nginx_need_restart = _configure_nginx(mail_domain)
systemd.service(
name="Start and enable OpenDKIM",
@@ -262,21 +233,6 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
restarted=dovecot_need_restart,
)
systemd.service(
name="Start and enable nginx",
service="nginx.service",
running=True,
enabled=True,
restarted=nginx_need_restart,
)
# This file is used by auth proxy.
# https://wiki.debian.org/EtcMailName
server.shell(
name="Setup /etc/mailname",
commands=[f"echo {mail_domain} >/etc/mailname; chmod 644 /etc/mailname"],
)
def callback():
result = server.shell(
commands=[

View File

@@ -38,13 +38,22 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
email=email,
)
files.template(
src=importlib.resources.files(__package__).joinpath("target.yaml.j2"),
dest="/var/lib/acme/conf/target",
service_file = files.put(
src=importlib.resources.files(__package__)
.joinpath("acmetool-redirector.service")
.open("rb"),
dest="/etc/systemd/system/acmetool-redirector.service",
user="root",
group="root",
mode="644",
)
systemd.service(
name="Setup acmetool-redirector service",
service="acmetool-redirector.service",
running=True,
enabled=True,
restarted=service_file.changed,
)
for domain in domains:
server.shell(

View File

@@ -0,0 +1,11 @@
[Unit]
Description=acmetool HTTP redirector
[Service]
Type=notify
ExecStart=/usr/bin/acmetool redirector --service.uid=daemon
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -1,7 +0,0 @@
request:
provider: https://acme-v02.api.letsencrypt.org/directory
key:
type: rsa
challenge:
webroot-paths:
- /var/www/html/.well-known/acme-challenge

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="{{ config.domain_name }}">
<domain>{{ config.domain_name }}</domain>
<displayName>{{ config.domain_name }} chatmail</displayName>
<displayShortName>{{ config.domain_name }}</displayShortName>
<incomingServer type="imap">
<hostname>{{ config.domain_name }}</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type="imap">
<hostname>{{ config.domain_name }}</hostname>
<port>143</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>{{ config.domain_name }}</hostname>
<port>465</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<outgoingServer type="smtp">
<hostname>{{ config.domain_name }}</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>

View File

@@ -4,23 +4,14 @@ protocols = imap lmtp
auth_mechanisms = plain
{% if debug == true %}
auth_verbose = yes
auth_debug = yes
auth_debug_passwords = yes
auth_verbose_passwords = plain
auth_cache_size = 100M
mail_debug = yes
{% endif %}
mail_plugins = quota
mail_debug = yes
# 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
# uncomment this if you want to debug authentication and user creation
#auth_verbose = yes
#auth_debug = yes
#auth_debug_passwords = yes
#auth_verbose_passwords = plain
# Authentication for system users.
passdb {
@@ -89,7 +80,7 @@ plugin {
quota_rule = *:storage=100M
quota_max_mail_size=30M
quota_grace = 0
# quota_over_flag_value = TRUE
quota_over_flag_value = TRUE
}

View File

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

@@ -1,47 +0,0 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
events {
worker_connections 768;
# multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
# Do not emit nginx version on error pages.
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_certificate /var/lib/acme/live/{{ config.domain_name }}/fullchain;
ssl_certificate_key /var/lib/acme/live/{{ config.domain_name }}/privkey;
gzip on;
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
root /var/www/html;
index index.html index.htm;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
}
}

View File

@@ -37,8 +37,6 @@ 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,11 +9,7 @@
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
{% if debug == true %}
smtp inet n - y - - smtpd -v
{% else %}
smtp inet n - y - - smtpd
{% endif %}
smtp inet n - y - - smtpd
#smtp inet n - y - 1 postscreen
#smtpd pass - - y - - smtpd
#dnsblog unix - - y - 0 dnsblog
@@ -32,7 +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 smtpd_proxy_filter=127.0.0.1:10080
-o content_filter=filter:unix:private/filtermail
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
@@ -47,7 +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 smtpd_proxy_filter=127.0.0.1:10080
-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,11 +66,13 @@ showq unix n - y - - showq
error unix - - y - - error
retry unix - - y - - error
discard unix - - y - - discard
local unix - n n - - local
virtual unix - n n - - virtual
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:10025 inet n - n - 10 smtpd
localhost:10026 inet n - n - 10 smtpd
-o content_filter=

View File

@@ -1,34 +0,0 @@
def test_tls_serialized_connect(benchmark, imap_or_smtp):
def connect():
imap_or_smtp.connect()
benchmark(connect)
def test_login(benchmark, imap_or_smtp, gencreds):
cls = imap_or_smtp.__class__
conns = []
for i in range(20):
conn = cls(imap_or_smtp.host)
conn.connect()
conns.append(conn)
def login():
conn = conns.pop()
conn.login(*gencreds())
benchmark(login)
def test_send_and_receive_10(benchmark, cmfactory, lp):
"""send many messages between two accounts"""
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
def send_10_receive_all():
for i in range(10):
chat.send_text(f"hello {i}")
for i in range(10):
ac2.wait_next_incoming_message()
benchmark(send_10_receive_all)

View File

@@ -1,57 +1,15 @@
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")
import time
@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)]
return os.environ.get("CHATMAIL_DOMAIN", "c1.testrun.org")
@pytest.fixture
@@ -60,10 +18,6 @@ def imap(maildomain):
class ImapConn:
AuthError = imaplib.IMAP4.error
logcmd = "journalctl -f -u dovecot"
name = "dovecot"
def __init__(self, host):
self.host = host
@@ -75,24 +29,6 @@ class ImapConn:
print(f"imap-login {user!r} {password!r}")
self.conn.login(user, password)
def fetch_all(self):
print("imap-fetch all")
status, res = self.conn.select()
if int(res[0]) == 0:
raise ValueError("no messages in imap folder")
status, results = self.conn.fetch("1:*", "(RFC822)")
assert status == "OK"
return results
def fetch_all_messages(self):
print("imap-fetch all messages")
results = self.fetch_all()
messages = []
for item in results:
if len(item) == 2:
messages.append(item[1].decode())
return messages
@pytest.fixture
def smtp(maildomain):
@@ -100,10 +36,6 @@ def smtp(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
@@ -115,33 +47,18 @@ class SmtpConn:
print(f"smtp-login {user!r} {password!r}")
self.conn.login(user, password)
def sendmail(self, from_addr, to_addrs, msg):
print(f"smtp-sendmail from={from_addr!r} to_addrs={to_addrs!r}")
print(f"smtp-sendmail message size: {len(msg)}")
return self.conn.sendmail(from_addr=from_addr, to_addrs=to_addrs, msg=msg)
@pytest.fixture(params=["imap", "smtp"])
def imap_or_smtp(request):
return request.getfixturevalue(request.param)
@pytest.fixture
def gencreds(maildomain):
prefix = str(time.time())
count = itertools.count()
next(count)
def gen(domain=None):
domain = domain if domain else maildomain
def gen():
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}"
yield f"user{prefix}_{num}@{maildomain}", f"password{prefix}_{num}"
return lambda domain=None: next(gen(domain))
return lambda: next(gen())
#
@@ -156,20 +73,13 @@ class ChatmailTestProcess:
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
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):
@@ -180,107 +90,17 @@ class ChatmailTestProcess:
@pytest.fixture
def cmfactory(request, gencreds, tmpdir, data, maildomain):
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)
# 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"):
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)
@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()
res = line.decode().strip().lower()
if res:
yield res
else:
break
@pytest.fixture
def mailgen(request):
class Mailgen:
def get_encrypted(self, from_addr, to_addr):
data = request.fspath.dirpath("mailgen/encrypted.eml").read()
return data.format(from_addr=from_addr, to_addr=to_addr)
return Mailgen()
@pytest.fixture
def cmsetup(maildomain, gencreds):
return CMSetup(maildomain, gencreds)
class CMSetup:
def __init__(self, maildomain, gencreds):
self.maildomain = maildomain
self.gencreds = gencreds
def gen_users(self, num):
print(f"Creating {num} online users")
users = []
for i in range(num):
addr, password = self.gencreds()
user = CMUser(self.maildomain, addr, password)
assert user.smtp
users.append(user)
return users
class CMUser:
def __init__(self, maildomain, addr, password):
self.maildomain = maildomain
self.addr = addr
self.password = password
self._smtp = None
self._imap = None
@property
def smtp(self):
if not self._smtp:
handle = SmtpConn(self.maildomain)
handle.connect()
handle.login(self.addr, self.password)
self._smtp = handle
return self._smtp
@property
def imap(self):
if not self._imap:
imap = ImapConn(self.maildomain)
imap.connect()
imap.login(self.addr, self.password)
self._imap = imap
return self._imap

View File

@@ -1,66 +0,0 @@
From: {from_addr}
To: {to_addr}
Subject: ...
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>
<Mr.MvmCz-GQbi_.6FGRkhDf05c@c2.testrun.org>
Chat-Version: 1.0
Autocrypt: addr={from_addr}; prefer-encrypt=mutual;
keydata=xjMEZSwWjhYJKwYBBAHaRw8BAQdAQBEhqeJh0GueHB6kF/DUQqYCxARNBVokg/AzT+7LqH
rNFzxiYXJiYXpAYzIudGVzdHJ1bi5vcmc+wosEEBYIADMCGQEFAmUsFo4CGwMECwkIBwYVCAkKCwID
FgIBFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX9A4AEAnHWHp49eBCMHK5t66gYPiW
XQuB1mwUjzGfYWB+0RXUoA/0xcQ3FbUNlGKW7Blp6eMFfViv6Mv2d3kNSXACB6nmcMzjgEZSwWjhIK
KwYBBAGXVQEFAQEHQBpY5L2M1XHo0uxf8SX1wNLBp/OVvidoWHQF2Jz+kJsUAwEIB8J4BBgWCAAgBQ
JlLBaOAhsMFiEEFTfUNvVnY3b9F7yHnmme1PfUhX8ACgkQnmme1PfUhX/INgEA37AJaNvruYsJVanP
IXnYw4CKd55UAwl8Zcy+M2diAbkA/0fHHcGV4r78hpbbL1Os52DPOdqYQRauIeJUeG+G6bQO
MIME-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="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--

View File

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

View File

@@ -1,60 +0,0 @@
import smtplib
import pytest
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
@pytest.mark.parametrize("forgeaddr", ["internal", "someone@example.org"])
def test_reject_forged_from(cmsetup, mailgen, lp, forgeaddr):
user1, user3 = cmsetup.gen_users(2)
lp.sec("send encrypted message with forged from")
print("envelope_from", user1.addr)
if forgeaddr == "internal":
addr_to_forge = cmsetup.gen_users(1)[0].addr
else:
addr_to_forge = "someone@example.org"
print("message to inject:")
msg = mailgen.get_encrypted(from_addr=addr_to_forge, to_addr=user3.addr)
for line in msg.split("\n")[:4]:
print(f" {line}")
lp.sec("Send forged mail and check remote postfix lmtp processing result")
with pytest.raises(smtplib.SMTPException) as e:
user1.smtp.sendmail(from_addr=user1.addr, to_addrs=[user3.addr], msg=msg)
assert "500" in str(e.value)
@pytest.mark.slow
def test_exceed_rate_limit(cmsetup, gencreds, mailgen):
"""Test that the per-account send-mail limit is exceeded."""
user1, user2 = cmsetup.gen_users(2)
mail = mailgen.get_encrypted(user1.addr, user2.addr)
for i in range(100):
print("Sending mail", str(i))
try:
user1.smtp.sendmail(user1.addr, [user2.addr], mail)
except smtplib.SMTPException as e:
if i < 80:
pytest.fail(f"rate limit was exceeded too early with msg {i}")
outcome = e.recipients[user2.addr]
assert outcome[0] == 450
assert b'4.7.1: Too much mail from' in outcome[1]
return
pytest.fail("Rate limit was not exceeded")

View File

@@ -1,37 +1,55 @@
import pytest
import imaplib
import smtplib
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")
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)
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")
def test_login_same_password(self, imap, gencreds):
"""Test two different users logging in with the same password.
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")
This ensures that authentication process does not confuse the users
by using only the password hash as a key.
"""
user1, password1 = gencreds()
user2, _password2 = gencreds()
imap.connect()
imap.login(user1, password1)
imap.connect()
imap.login(user2, password1)
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)
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)
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)

View File

@@ -1,83 +1,45 @@
import os.path
import random
import pytest
import time
class TestEndToEndDeltaChat:
"Tests that use Delta Chat accounts on the chat mail instance."
class TestMailSending:
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")
msg1 = 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.
"""
def test_exceed_quota(self, cmfactory, lp, tmpdir):
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")
ac2.set_config("download_limit", 1024 * 2) # set download_limit to 2 KB avoid downloading all those 5MB files
lp.sec("ac1: send 25 5 MB files to ac2")
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
msgs = []
for i in range(num_to_send):
for i in range(25):
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)
for j in range(1024 * 1024 * 5):
f.write(random.choice(alphanumeric))
msg = chat.send_file(str(attachment))
msgs.append(msg)
lp.indent(f"Sent out msg {i}, size {attachsize/(1024*1024)}MB")
print("Sent out msg", str(i))
chat.send_file(str(attachment))
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)
ac2.wait_next_incoming_message()
lp.sec("ac2: check that at least one message failed")
failed = False
for i in range(25):
if chat.get_messages()[i].is_out_failed():
failed = True
print(chat.get_messages()[i].get_message_info())
try:
assert failed
except:
import pdb; pdb.set_trace()

View File

@@ -3,11 +3,17 @@
## Dovecot goals/steps
- automatic expiry of messages older than M days
- also expunge unread messages
- delete unconditionally messages older than 40 days
- limit: configure max-connections per account
## Filtermail
- (alex, Only allow (outgoing) mails if secure-join or autocrypt-pgp-encrypted format.
TODO: mime-parse mails and check/add tests
## nami: send out rate limit / rspamd
- basic outgoing send rate/limits (depending on "account-rating")

View File

@@ -1,4 +0,0 @@
#!/bin/bash
set -e
online-tests/venv/bin/pytest online-tests/benchmark.py -vrx

View File

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

View File

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

View File

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

View File

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