Compare commits

..

63 Commits

Author SHA1 Message Date
missytake
376ab4c4a7 www: get actual account restrictions from chatmail.ini 2023-12-12 22:40:06 +01:00
missytake
4b3a214276 tests: test against quota in chatmail.ini 2023-12-12 20:25:23 +01:00
missytake
bbf0e91761 tests: test against ratelimit in chatmail.ini 2023-12-12 20:25:23 +01:00
missytake
f4b8ec6e10 tests: pass --slow to cmdeploy test 2023-12-12 20:25:23 +01:00
missytake
3b45e82c76 www: be honest that only seen messages are removed for now 2023-12-12 20:12:25 +01:00
missytake
4551f36b85 chatmail.ini: switch username length defaults back to 9 2023-12-12 20:12:25 +01:00
missytake
f2d32324e3 tests: use something.testrun.org instead of chat.example.org for 1 test 2023-12-12 20:12:25 +01:00
missytake
5d4b6eec69 tests: ensure len(username) = 9 if CHATMAIL_DOMAIN2==nine.testrun.org 2023-12-12 20:12:25 +01:00
missytake
98fd4b61c9 tests: replace make_config with example_config, add default config params 2023-12-12 20:12:25 +01:00
missytake
74f9e7536b doveauth: fix logging statement 2023-12-12 20:12:25 +01:00
missytake
849b9d430c chatmail.ini: changed docstring must -> can
Co-authored-by: holger krekel  <holger@merlinux.eu>
2023-12-12 20:12:25 +01:00
missytake
0d0cc908c2 chatmaild: move username/password length and passthrough_senders to chatmail.ini 2023-12-12 20:12:25 +01:00
missytake
156434b952 cmdeploy: move max_mailbox_size + delete_mails_after to chatmail.ini 2023-12-12 20:12:25 +01:00
B. Petersen
02d07912dd make 'experimental' more outstanding 2023-12-12 20:02:46 +01:00
B. Petersen
4a7e36618f use a class for the banner, make sure it is always width of page 2023-12-12 20:02:46 +01:00
B. Petersen
e272294e07 add domain right of the menu, standardize menu help-code 2023-12-12 20:02:46 +01:00
B. Petersen
401f215dc9 move menu up 2023-12-12 20:02:46 +01:00
missytake
3cd4265c94 www: add favicon 2023-12-11 18:26:50 +01:00
holger krekel
8e6869d8e3 move tests into cmdeploy 2023-12-11 18:12:23 +01:00
holger krekel
d6df0f0604 rename deploy_chatmail to cmdeploy 2023-12-11 18:12:23 +01:00
missytake
cad1d32682 mta-sts-resolver: fix virtualenv deployment 2023-12-11 17:54:22 +01:00
holger krekel
fd10652f48 fix event waiting in a test 2023-12-11 17:07:07 +01:00
holger krekel
02918de6c1 rename mailname to mail_domain everywhere 2023-12-11 17:07:07 +01:00
holger krekel
e27dd84501 show PATH env 2023-12-11 15:52:12 +01:00
holger krekel
cdbda291c5 fix cmdeploy test command 2023-12-11 15:52:12 +01:00
holger krekel
d70eb78a76 remove tox run from deploy-chatmail and use 'cmdeploy fmt' and 'pytest' directly 2023-12-11 15:52:12 +01:00
holger krekel
a5e4562505 move tests/chatmaild to chatmaild package, streamline tests and fixtures accordingly 2023-12-11 15:52:12 +01:00
holger krekel
3f2eb84323 fix tests and run all tests on "cmdeploy test" 2023-12-11 15:52:12 +01:00
holger krekel
613d3e14ea discover chatmail.ini in tests from CWD and all parent dirs (tox runs change dirs) 2023-12-11 15:52:12 +01:00
holger krekel
453c401ac6 "cmdeploy test" now installs deltachat if it's not there 2023-12-11 15:52:12 +01:00
holger krekel
8b756f2e0c consistently use shell helper 2023-12-11 15:52:12 +01:00
holger krekel
9e9d5b7698 consistently show ssh/shell output 2023-12-11 15:52:12 +01:00
holger krekel
02adb758ff add "build" dependency 2023-12-11 15:52:12 +01:00
holger krekel
ffade66d97 always show which ssh-commands execute 2023-12-11 15:52:12 +01:00
holger krekel
988333d5fd some more shifting around 2023-12-11 15:52:12 +01:00
holger krekel
db41e952e3 shift functions around, discover sub commands automatically 2023-12-11 15:52:12 +01:00
holger krekel
bb1b11df15 make tests depend on chatmail.ini, not env var 2023-12-11 15:52:12 +01:00
holger krekel
b3fdebf8df tweak for making CI happy 2023-12-11 15:52:12 +01:00
holger krekel
4615df2e3b try to fix workflow 2023-12-11 15:52:12 +01:00
holger krekel
73d8e01452 don't print a traceback but do a proper return code for "cmdeploy test" 2023-12-11 15:52:12 +01:00
holger krekel
bf8a99d844 add chatmail.ini to ignore 2023-12-11 15:52:12 +01:00
holger krekel
cb19ac34e3 add manifest so that ini files get included 2023-12-11 15:52:12 +01:00
holger krekel
15e5573ab4 address nami comment 2023-12-11 15:52:12 +01:00
holger krekel
74f5b2847f fix readme 2023-12-11 15:52:12 +01:00
holger krekel
e417e43c54 add test command 2023-12-11 15:52:12 +01:00
holger krekel
158ebe1089 fix README 2023-12-11 15:52:12 +01:00
holger krekel
5fb7833677 add status command and delete last script 2023-12-11 15:52:12 +01:00
holger krekel
587d8142d2 some more housekeeping 2023-12-11 15:52:12 +01:00
holger krekel
a6d24b3af7 introduce "cmdeploy bench" 2023-12-11 15:52:12 +01:00
holger krekel
d730af2e69 cleanup 2023-12-11 15:52:12 +01:00
holger krekel
99b1e18b0a generate dns zone file via cmdeploy 2023-12-11 15:52:12 +01:00
holger krekel
6012039c56 add dns command beginning 2023-12-11 15:52:12 +01:00
holger krekel
f388e86287 make cmdeploy test work 2023-12-11 15:52:12 +01:00
holger krekel
d3ca037ebf snap 2023-12-11 15:52:12 +01:00
holger krekel
00a203c9aa fix various test setups 2023-12-11 15:52:12 +01:00
holger krekel
6bffb5470d add webdev sub command 2023-12-11 15:52:12 +01:00
holger krekel
81c4a6170f making it work 2023-12-11 15:52:12 +01:00
holger krekel
6285283b02 rework UI for chatmail setup 2023-12-11 15:52:12 +01:00
holger krekel
b71e24d6a1 draft init flow 2023-12-11 15:52:12 +01:00
holger krekel
a4e48c9919 document some attributes in chatmail.ini 2023-12-11 15:52:12 +01:00
holger krekel
bc27eb58bf get passthrough_recipients list from config 2023-12-11 15:52:12 +01:00
holger krekel
1b1f9365c9 various fixes 2023-12-11 15:52:12 +01:00
holger krekel
d8c8040f07 introduce basic config file 2023-12-11 15:52:12 +01:00
60 changed files with 434 additions and 161 deletions

View File

@@ -31,7 +31,7 @@ jobs:
run: cmdeploy fmt -v run: cmdeploy fmt -v
- name: run deploy-chatmail offline tests - name: run deploy-chatmail offline tests
run: pytest tests run: pytest --pyargs cmdeploy
- name: initialize with chatmail domain - name: initialize with chatmail domain
run: cmdeploy init chat.example.org run: cmdeploy init chat.example.org

View File

@@ -9,11 +9,17 @@ def read_config(inipath):
class Config: class Config:
def __init__(self, inipath, params): def __init__(self, inipath, params):
self._inipath = inipath self._inipath = inipath
self.mailname = self.mail_domain = params["mailname"] self.mail_domain = params["mail_domain"]
self.max_user_send_per_minute = int(params["max_user_send_per_minute"]) self.max_user_send_per_minute = int(params["max_user_send_per_minute"])
self.max_mailbox_size = params["max_mailbox_size"]
self.delete_mails_after = params["delete_mails_after"]
self.username_min_length = int(params["username_min_length"])
self.username_max_length = int(params["username_max_length"])
self.password_min_length = int(params["password_min_length"])
self.passthrough_senders = params["passthrough_senders"].split()
self.passthrough_recipients = params["passthrough_recipients"].split()
self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.postfix_reinject_port = int(params["postfix_reinject_port"]) self.postfix_reinject_port = int(params["postfix_reinject_port"])
self.passthrough_recipients = params["passthrough_recipients"].split()
self.privacy_postal = params.get("privacy_postal") self.privacy_postal = params.get("privacy_postal")
self.privacy_mail = params.get("privacy_mail") self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo") self.privacy_pdo = params.get("privacy_pdo")
@@ -23,12 +29,14 @@ class Config:
return open(self._inipath, "rb") return open(self._inipath, "rb")
def write_initial_config(inipath, mailname): def write_initial_config(inipath, mail_domain):
from importlib.resources import files from importlib.resources import files
inidir = files(__package__).joinpath("ini") inidir = files(__package__).joinpath("ini")
content = inidir.joinpath("chatmail.ini.f").read_text().format(mailname=mailname) content = (
if mailname.endswith(".testrun.org"): inidir.joinpath("chatmail.ini.f").read_text().format(mail_domain=mail_domain)
)
if mail_domain.endswith(".testrun.org"):
override_inipath = inidir.joinpath("override-testrun.ini") override_inipath = inidir.joinpath("override-testrun.ini")
privacy = iniconfig.IniConfig(override_inipath)["privacy"] privacy = iniconfig.IniConfig(override_inipath)["privacy"]
lines = [] lines = []

View File

@@ -12,6 +12,7 @@ from socketserver import (
import pwd import pwd
from .database import Database from .database import Database
from .config import read_config, Config
NOCREATE_FILE = "/etc/chatmail-nocreate" NOCREATE_FILE = "/etc/chatmail-nocreate"
@@ -22,14 +23,17 @@ def encrypt_password(password: str):
return "{SHA512-CRYPT}" + passhash return "{SHA512-CRYPT}" + passhash
def is_allowed_to_create(user, cleartext_password) -> bool: def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
"""Return True if user and password are admissable.""" """Return True if user and password are admissable."""
if os.path.exists(NOCREATE_FILE): if os.path.exists(NOCREATE_FILE):
logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.") logging.warning(f"blocked account creation because {NOCREATE_FILE!r} exists.")
return False return False
if len(cleartext_password) < 9: if len(cleartext_password) < config.password_min_length:
logging.warning("Password needs to be at least 9 characters long") logging.warning(
"Password needs to be at least %s characters long",
config.password_min_length,
)
return False return False
parts = user.split("@") parts = user.split("@")
@@ -38,11 +42,17 @@ def is_allowed_to_create(user, cleartext_password) -> bool:
return False return False
localpart, domain = parts localpart, domain = parts
if domain == "nine.testrun.org": if (
# nine.testrun.org policy, username has to be exactly nine chars len(localpart) > config.username_max_length
if len(localpart) != 9: or len(localpart) < config.username_min_length
logging.warning(f"localpart {localpart!r} has not exactly nine chars") ):
return False logging.warning(
"localpart %s has to be between %s and %s chars long",
localpart,
config.username_min_length,
config.username_max_length,
)
return False
return True return True
@@ -60,7 +70,7 @@ def lookup_userdb(db, user):
return get_user_data(db, user) return get_user_data(db, user)
def lookup_passdb(db, user, cleartext_password): def lookup_passdb(db, config: Config, user, cleartext_password):
with db.write_transaction() as conn: with db.write_transaction() as conn:
userdata = conn.get_user(user) userdata = conn.get_user(user)
if userdata: if userdata:
@@ -72,7 +82,7 @@ def lookup_passdb(db, user, cleartext_password):
userdata["uid"] = "vmail" userdata["uid"] = "vmail"
userdata["gid"] = "vmail" userdata["gid"] = "vmail"
return userdata return userdata
if not is_allowed_to_create(user, cleartext_password): if not is_allowed_to_create(config, user, cleartext_password):
return return
encrypted_password = encrypt_password(cleartext_password) encrypted_password = encrypt_password(cleartext_password)
@@ -87,7 +97,7 @@ def lookup_passdb(db, user, cleartext_password):
) )
def handle_dovecot_request(msg, db, mail_domain): def handle_dovecot_request(msg, db, config: Config):
short_command = msg[0] short_command = msg[0]
if short_command == "L": # LOOKUP if short_command == "L": # LOOKUP
parts = msg[1:].split("\t") parts = msg[1:].split("\t")
@@ -97,15 +107,15 @@ def handle_dovecot_request(msg, db, mail_domain):
res = "" res = ""
if namespace == "shared": if namespace == "shared":
if type == "userdb": if type == "userdb":
if user.endswith(f"@{mail_domain}"): if user.endswith(f"@{config.mail_domain}"):
res = lookup_userdb(db, user) res = lookup_userdb(db, user)
if res: if res:
reply_command = "O" reply_command = "O"
else: else:
reply_command = "N" reply_command = "N"
elif type == "passdb": elif type == "passdb":
if user.endswith(f"@{mail_domain}"): if user.endswith(f"@{config.mail_domain}"):
res = lookup_passdb(db, user, cleartext_password=args[0]) res = lookup_passdb(db, config, user, cleartext_password=args[0])
if res: if res:
reply_command = "O" reply_command = "O"
else: else:
@@ -123,8 +133,7 @@ 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]) db = Database(sys.argv[3])
with open("/etc/mailname", "r") as fp: config = read_config(sys.argv[4])
mail_domain = fp.read().strip()
class Handler(StreamRequestHandler): class Handler(StreamRequestHandler):
def handle(self): def handle(self):
@@ -133,7 +142,7 @@ def main():
msg = self.rfile.readline().strip().decode() msg = self.rfile.readline().strip().decode()
if not msg: if not msg:
break break
res = handle_dovecot_request(msg, db, mail_domain) res = handle_dovecot_request(msg, db, config)
if res: if res:
self.wfile.write(res.encode("ascii")) self.wfile.write(res.encode("ascii"))
self.wfile.flush() self.wfile.flush()

View File

@@ -2,7 +2,7 @@
Description=Chatmail dict authentication proxy for dovecot Description=Chatmail dict authentication proxy for dovecot
[Service] [Service]
ExecStart={execpath} /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite ExecStart={execpath} /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite {config_path}
Restart=always Restart=always
RestartSec=30 RestartSec=30

View File

@@ -111,6 +111,9 @@ class BeforeQueueHandler:
if not mail_encrypted and check_mdn(message, envelope): if not mail_encrypted and check_mdn(message, envelope):
return return
if envelope.mail_from in self.config.passthrough_senders:
return
passthrough_recipients = self.config.passthrough_recipients passthrough_recipients = self.config.passthrough_recipients
envelope_from_domain = from_addr.split("@").pop() envelope_from_domain = from_addr.split("@").pop()
for recipient in envelope.rcpt_tos: for recipient in envelope.rcpt_tos:

View File

@@ -1,24 +1,54 @@
[params] [params]
# mail domain (MUST be set to fully qualified chat mail domain) # mail domain (MUST be set to fully qualified chat mail domain)
mailname = {mailname} mail_domain = {mail_domain}
# #
# If you only do private test deploys, you don't need to modify any settings below # If you only do private test deploys, you don't need to modify any settings below
# #
#
# Account Restrictions
#
# how many mails a user can send out per minute # how many mails a user can send out per minute
max_user_send_per_minute = 60 max_user_send_per_minute = 60
# maximum mailbox size of a chatmail account
max_mailbox_size = 100M
# time after which seen mails are deleted
delete_mails_after = 40d
# minimum length a username must have
username_min_length = 9
# maximum length a username can have
username_max_length = 9
# minimum length a password must have
password_min_length = 9
# list of chatmail accounts which can send outbound un-encrypted mail
passthrough_senders =
# list of e-mail recipients for which to accept outbound un-encrypted mails # list of e-mail recipients for which to accept outbound un-encrypted mails
passthrough_recipients = passthrough_recipients =
#
# Deployment Details
#
# where the filtermail SMTP service listens # where the filtermail SMTP service listens
filtermail_smtp_port = 10080 filtermail_smtp_port = 10080
# postfix accepts on the localhost reinject SMTP port # postfix accepts on the localhost reinject SMTP port
postfix_reinject_port = 10025 postfix_reinject_port = 10025
#
# Privacy Policy
#
# postal address of privacy contact # postal address of privacy contact
privacy_postal = privacy_postal =

View File

@@ -1,23 +1,25 @@
#!/usr/bin/python3 #!/usr/local/lib/chatmaild/venv/bin/python3
""" CGI script for creating new accounts. """ """ CGI script for creating new accounts. """
import json import json
import random import random
mailname_path = "/etc/mailname" from chatmaild.config import read_config, Config
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
def create_newemail_dict(domain): def create_newemail_dict(config: Config):
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join(random.choices(alphanumeric, k=9)) user = "".join(random.choices(alphanumeric, k=config.username_min_length))
password = "".join(random.choices(alphanumeric, k=12)) password = "".join(random.choices(alphanumeric, k=config.password_min_length + 3))
return dict(email=f"{user}@{domain}", password=f"{password}") return dict(email=f"{user}@{config.mail_domain}", password=f"{password}")
def print_new_account(): def print_new_account():
domain = open(mailname_path).read().strip() config = read_config(CONFIG_PATH)
creds = create_newemail_dict(domain=domain) creds = create_newemail_dict(config)
print("Content-Type: application/json") print("Content-Type: application/json")
print("") print("")

View File

@@ -13,8 +13,8 @@ from chatmaild.config import read_config, write_initial_config
def make_config(tmp_path): def make_config(tmp_path):
inipath = tmp_path.joinpath("chatmail.ini") inipath = tmp_path.joinpath("chatmail.ini")
def make_conf(mailname): def make_conf(mail_domain):
write_initial_config(inipath, mailname=mailname) write_initial_config(inipath, mail_domain=mail_domain)
return read_config(inipath) return read_config(inipath)
return make_conf return make_conf
@@ -27,7 +27,7 @@ def example_config(make_config):
@pytest.fixture @pytest.fixture
def maildomain(example_config): def maildomain(example_config):
return example_config.mailname return example_config.mail_domain
@pytest.fixture @pytest.fixture

View File

@@ -1,22 +1,21 @@
from chatmaild.config import read_config from chatmaild.config import read_config
def test_read_config_basic(make_config): def test_read_config_basic(example_config):
config = make_config("chat.example.org") assert example_config.mail_domain == "chat.example.org"
assert config.mailname == "chat.example.org" assert not example_config.privacy_supervisor and not example_config.privacy_mail
assert not config.privacy_supervisor and not config.privacy_mail assert not example_config.privacy_pdo and not example_config.privacy_postal
assert not config.privacy_pdo and not config.privacy_postal
inipath = config._inipath inipath = example_config._inipath
inipath.write_text(inipath.read_text().replace("60", "37")) inipath.write_text(inipath.read_text().replace("60", "37"))
config = read_config(inipath) example_config = read_config(inipath)
assert config.max_user_send_per_minute == 37 assert example_config.max_user_send_per_minute == 37
assert config.mailname == "chat.example.org" assert example_config.mail_domain == "chat.example.org"
def test_read_config_testrun(make_config): def test_read_config_testrun(make_config):
config = make_config("something.testrun.org") config = make_config("something.testrun.org")
assert config.mailname == "something.testrun.org" assert config.mail_domain == "something.testrun.org"
assert len(config.privacy_postal.split("\n")) > 1 assert len(config.privacy_postal.split("\n")) > 1
assert len(config.privacy_supervisor.split("\n")) > 1 assert len(config.privacy_supervisor.split("\n")) > 1
assert len(config.privacy_pdo.split("\n")) > 1 assert len(config.privacy_pdo.split("\n")) > 1
@@ -24,4 +23,10 @@ def test_read_config_testrun(make_config):
assert config.filtermail_smtp_port == 10080 assert config.filtermail_smtp_port == 10080
assert config.postfix_reinject_port == 10025 assert config.postfix_reinject_port == 10025
assert config.max_user_send_per_minute == 60 assert config.max_user_send_per_minute == 60
assert config.passthrough_recipients assert config.max_mailbox_size == "100M"
assert config.delete_mails_after == "40d"
assert config.username_min_length == 9
assert config.username_max_length == 9
assert config.password_min_length == 9
assert config.passthrough_recipients == ["privacy@testrun.org"]
assert config.passthrough_senders == []

View File

@@ -9,29 +9,35 @@ from chatmaild.doveauth import get_user_data, lookup_passdb, handle_dovecot_requ
from chatmaild.database import DBError from chatmaild.database import DBError
def test_basic(db): def test_basic(db, example_config):
lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u") lookup_passdb(db, example_config, "asdf12345@chat.example.org", "q9mr3faue")
data = get_user_data(db, "link2xt@c1.testrun.org") data = get_user_data(db, "asdf12345@chat.example.org")
assert data assert data
data2 = lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u") data2 = lookup_passdb(
db, example_config, "asdf12345@chat.example.org", "q9mr3jewvadsfaue"
)
assert data == data2 assert data == data2
def test_dont_overwrite_password_on_wrong_login(db): def test_dont_overwrite_password_on_wrong_login(db, example_config):
"""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 = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983") res = lookup_passdb(
db, example_config, "newuser12@chat.example.org", "kajdlkajsldk12l3kj1983"
)
assert res["password"] assert res["password"]
res2 = lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe") res2 = lookup_passdb(db, example_config, "newuser12@chat.example.org", "kajdslqwe")
# 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(db, monkeypatch, tmpdir): def test_nocreate_file(db, monkeypatch, tmpdir, example_config):
p = tmpdir.join("nocreate") p = tmpdir.join("nocreate")
p.write("") p.write("")
monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p)) monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p))
lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik") lookup_passdb(
assert not get_user_data(db, "newuser1@something.org") db, example_config, "newuser12@chat.example.org", "zequ0Aimuchoodaechik"
)
assert not get_user_data(db, "newuser12@chat.example.org")
def test_db_version(db): def test_db_version(db):
@@ -45,21 +51,21 @@ def test_too_high_db_version(db):
db.ensure_tables() db.ensure_tables()
def test_handle_dovecot_request(db): def test_handle_dovecot_request(db, example_config):
msg = ( msg = (
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/" "Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
"some42@c3.testrun.org\tsome42@c3.testrun.org" "some42123@chat.example.org\tsome42123@chat.example.org"
) )
res = handle_dovecot_request(msg, db, "c3.testrun.org") res = handle_dovecot_request(msg, db, example_config)
assert res assert res
assert res[0] == "O" and res.endswith("\n") assert res[0] == "O" and res.endswith("\n")
userdata = json.loads(res[1:].strip()) userdata = json.loads(res[1:].strip())
assert userdata["home"] == "/home/vmail/some42@c3.testrun.org" assert userdata["home"] == "/home/vmail/some42123@chat.example.org"
assert userdata["uid"] == userdata["gid"] == "vmail" assert userdata["uid"] == userdata["gid"] == "vmail"
assert userdata["password"].startswith("{SHA512-CRYPT}") assert userdata["password"].startswith("{SHA512-CRYPT}")
def test_50_concurrent_lookups_different_accounts(db, gencreds): def test_50_concurrent_lookups_different_accounts(db, gencreds, example_config):
num_threads = 50 num_threads = 50
req_per_thread = 5 req_per_thread = 5
results = queue.Queue() results = queue.Queue()
@@ -68,7 +74,7 @@ def test_50_concurrent_lookups_different_accounts(db, gencreds):
for i in range(req_per_thread): for i in range(req_per_thread):
addr, password = gencreds() addr, password = gencreds()
try: try:
lookup_passdb(db, addr, password) lookup_passdb(db, example_config, addr, password)
except Exception: except Exception:
results.put(traceback.format_exc()) results.put(traceback.format_exc())
else: else:

View File

@@ -127,3 +127,19 @@ def test_excempt_privacy(maildata, gencreds, handler):
content = msg.as_bytes() content = msg.as_bytes()
assert "500" in handler.check_DATA(envelope=env2) assert "500" in handler.check_DATA(envelope=env2)
def test_passthrough_senders(gencreds, handler, maildata):
acc1 = gencreds()[0]
to_addr = "recipient@something.org"
handler.config.passthrough_senders = [acc1]
msg = maildata("plain.eml", acc1, to_addr)
class env:
mail_from = acc1
rcpt_tos = to_addr
content = msg.as_bytes()
# assert that None/no error is returned
assert not handler.check_DATA(envelope=env)

View File

@@ -4,26 +4,24 @@ import chatmaild
from chatmaild.newemail import create_newemail_dict, print_new_account from chatmaild.newemail import create_newemail_dict, print_new_account
def test_create_newemail_dict(): def test_create_newemail_dict(example_config):
ac1 = create_newemail_dict(domain="example.org") ac1 = create_newemail_dict(example_config)
assert "@" in ac1["email"] assert "@" in ac1["email"]
assert len(ac1["password"]) >= 10 assert len(ac1["password"]) >= 10
ac2 = create_newemail_dict(domain="example.org") ac2 = create_newemail_dict(example_config)
assert ac1["email"] != ac2["email"] assert ac1["email"] != ac2["email"]
assert ac1["password"] != ac2["password"] assert ac1["password"] != ac2["password"]
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir): def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_config):
p = tmpdir.join("mailname") monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath))
p.write(maildomain)
monkeypatch.setattr(chatmaild.newemail, "mailname_path", str(p))
print_new_account() print_new_account()
out, err = capsys.readouterr() out, err = capsys.readouterr()
lines = out.split("\n") lines = out.split("\n")
assert lines[0] == "Content-Type: application/json" assert lines[0] == "Content-Type: application/json"
assert not lines[1] assert not lines[1]
dic = json.loads(lines[2]) dic = json.loads(lines[2])
assert dic["email"].endswith(f"@{maildomain}") assert dic["email"].endswith(f"@{example_config.mail_domain}")
assert len(dic["password"]) >= 10 assert len(dic["password"]) >= 10

View File

@@ -3,8 +3,8 @@ requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "deploy-chatmail" name = "cmdeploy"
version = "0.1" version = "0.2"
dependencies = [ dependencies = [
"pyinfra", "pyinfra",
"pillow", "pillow",
@@ -22,10 +22,11 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
cmdeploy = "deploy_chatmail.cmdeploy:main" cmdeploy = "cmdeploy.cmdeploy:main"
[project.entry-points.pytest11] [project.entry-points.pytest11]
"chatmaild.testplugin" = "chatmaild.tests.plugin" "chatmaild.testplugin" = "chatmaild.tests.plugin"
"cmdeploy.testplugin" = "cmdeploy.tests.plugin"
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-v -ra --strict-markers" addopts = "-v -ra --strict-markers"

View File

@@ -195,7 +195,7 @@ def _install_mta_sts_daemon() -> bool:
server.shell( server.shell(
name="install postfix-mta-sts-resolver with pip", name="install postfix-mta-sts-resolver with pip",
commands=[ commands=[
"python3 -m venv /usr/local/lib/postfix-mta-sts-resolver", "python3 -m virtualenv /usr/local/lib/postfix-mta-sts-resolver",
"/usr/local/lib/postfix-mta-sts-resolver/bin/pip install postfix-mta-sts-resolver", "/usr/local/lib/postfix-mta-sts-resolver/bin/pip install postfix-mta-sts-resolver",
], ],
) )
@@ -243,7 +243,7 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
return need_restart return need_restart
def _configure_dovecot(mail_server: str, debug: bool = False) -> bool: def _configure_dovecot(config: Config, debug: bool = False) -> bool:
"""Configures Dovecot IMAP server.""" """Configures Dovecot IMAP server."""
need_restart = False need_restart = False
@@ -253,7 +253,7 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
user="root", user="root",
group="root", group="root",
mode="644", mode="644",
config={"hostname": mail_server}, config=config,
debug=debug, debug=debug,
) )
need_restart |= main_config.changed need_restart |= main_config.changed
@@ -266,14 +266,13 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
) )
need_restart |= auth_config.changed need_restart |= auth_config.changed
files.put( files.template(
src=importlib.resources.files(__package__) src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"),
.joinpath("dovecot/expunge.cron")
.open("rb"),
dest="/etc/cron.d/expunge", dest="/etc/cron.d/expunge",
user="root", user="root",
group="root", group="root",
mode="644", mode="644",
config=config,
) )
# as per https://doc.dovecot.org/configuration_manual/os/ # as per https://doc.dovecot.org/configuration_manual/os/
@@ -347,8 +346,8 @@ def _configure_nginx(domain: str, debug: bool = False) -> bool:
def check_config(config): def check_config(config):
mailname = config.mailname mail_domain = config.mail_domain
if mailname != "testrun.org" and not mailname.endswith(".testrun.org"): if mail_domain != "testrun.org" and not mail_domain.endswith(".testrun.org"):
blocked_words = "merlinux schmieder testrun.org".split() blocked_words = "merlinux schmieder testrun.org".split()
for value in config.__dict__.values(): for value in config.__dict__.values():
if any(x in value for x in blocked_words): if any(x in value for x in blocked_words):
@@ -423,7 +422,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
_install_remote_venv_with_chatmaild(config) _install_remote_venv_with_chatmaild(config)
debug = False debug = False
dovecot_need_restart = _configure_dovecot(mail_server, debug=debug) dovecot_need_restart = _configure_dovecot(config, debug=debug)
postfix_need_restart = _configure_postfix(config, debug=debug) postfix_need_restart = _configure_postfix(config, debug=debug)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector) opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
mta_sts_need_restart = _install_mta_sts_daemon() mta_sts_need_restart = _install_mta_sts_daemon()

View File

@@ -52,17 +52,17 @@ def run_cmd(args, out):
"""Deploy chatmail services on the remote server.""" """Deploy chatmail services on the remote server."""
env = os.environ.copy() env = os.environ.copy()
env["CHATMAIL_DOMAIN"] = args.config.mailname env["CHATMAIL_DOMAIN"] = args.config.mail_domain
deploypy = "deploy-chatmail/src/deploy_chatmail/deploy.py" deploy_path = "cmdeploy/src/cmdeploy/deploy.py"
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
cmd = f"{pyinf} --ssh-user root {args.config.mailname} {deploypy}" cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}"
out.check_call(cmd, env=env) out.check_call(cmd, env=env)
def dns_cmd(args, out): def dns_cmd(args, out):
"""Generate dns zone file.""" """Generate dns zone file."""
template = importlib.resources.files(__package__).joinpath("chatmail.zone.f") template = importlib.resources.files(__package__).joinpath("chatmail.zone.f")
ssh = f"ssh root@{args.config.mailname}" ssh = f"ssh root@{args.config.mail_domain}"
def read_dkim_entries(entry): def read_dkim_entries(entry):
lines = [] lines = []
@@ -77,16 +77,16 @@ def dns_cmd(args, out):
dkim_entry = read_dkim_entries(out.shell_output(f"{ssh} -- opendkim-genzone -F")) dkim_entry = read_dkim_entries(out.shell_output(f"{ssh} -- opendkim-genzone -F"))
out( out(
f"[writing {args.config.mailname} zone data (using space as separator) to stdout output]", f"[writing {args.config.mail_domain} zone data (using space as separator) to stdout output]",
green=True, green=True,
) )
print( print(
template.read_text() template.read_text()
.format( .format(
acme_account_url=acme_account_url, acme_account_url=acme_account_url,
email=f"root@{args.config.mailname}", email=f"root@{args.config.mail_domain}",
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"), sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
chatmail_domain=args.config.mailname, chatmail_domain=args.config.mail_domain,
dkim_entry=dkim_entry, dkim_entry=dkim_entry,
) )
.strip() .strip()
@@ -96,9 +96,9 @@ def dns_cmd(args, out):
def status_cmd(args, out): def status_cmd(args, out):
"""Display status for online chatmail instance.""" """Display status for online chatmail instance."""
ssh = f"ssh root@{args.config.mailname}" ssh = f"ssh root@{args.config.mail_domain}"
out.green(f"chatmail domain: {args.config.mailname}") out.green(f"chatmail domain: {args.config.mail_domain}")
if args.config.privacy_mail: if args.config.privacy_mail:
out.green("privacy settings: present") out.green("privacy settings: present")
else: else:
@@ -110,6 +110,15 @@ def status_cmd(args, out):
print(line) print(line)
def test_cmd_options(parser):
parser.add_argument(
"--slow",
dest="slow",
action="store_true",
help="also run slow tests",
)
def test_cmd(args, out): def test_cmd(args, out):
"""Run local and online tests for chatmail deployment. """Run local and online tests for chatmail deployment.
@@ -121,9 +130,18 @@ def test_cmd(args, out):
out.check_call(f"{sys.executable} -m pip install deltachat") out.check_call(f"{sys.executable} -m pip install deltachat")
pytest_path = shutil.which("pytest") pytest_path = shutil.which("pytest")
ret = out.run_ret( pytest_args = [
[pytest_path, "tests/", "-n4", "-rs", "-x", "-vrx", "--durations=5"] pytest_path,
) "cmdeploy/src/",
"-n4",
"-rs",
"-x",
"-vrx",
"--durations=5",
]
if args.slow:
pytest_args.append("--slow")
ret = out.run_ret(pytest_args)
return ret return ret
@@ -147,11 +165,7 @@ def fmt_cmd_options(parser):
def fmt_cmd(args, out): def fmt_cmd(args, out):
"""Run formattting fixes (fuff and black) on all chatmail source code.""" """Run formattting fixes (fuff and black) on all chatmail source code."""
chatmaild = importlib.resources.files("chatmaild") sources = [str(importlib.resources.files(x)) for x in ("chatmaild", "cmdeploy")]
deploy_chatmail = importlib.resources.files("deploy_chatmail")
tests = deploy_chatmail.joinpath("../../../tests")
sources = list(str(x) for x in [chatmaild, deploy_chatmail, tests])
black_args = [shutil.which("black")] black_args = [shutil.which("black")]
ruff_args = [shutil.which("ruff")] ruff_args = [shutil.which("ruff")]
@@ -174,9 +188,10 @@ def fmt_cmd(args, out):
def bench_cmd(args, out): def bench_cmd(args, out):
"""Run benchmarks against an online chatmail instance.""" """Run benchmarks against an online chatmail instance."""
pytest_path = shutil.which("pytest") args = ["pytest", "--pyargs", "cmdeploy.tests.online.benchmark", "-vrx"]
benchmark = "tests/online/benchmark.py" cmdstring = " ".join(args)
subprocess.check_call([pytest_path, benchmark, "-vrx"]) out.green(f"[$ {cmdstring}]")
subprocess.check_call(args)
def webdev_cmd(args, out): def webdev_cmd(args, out):

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,6 +1,6 @@
import os import os
import pyinfra import pyinfra
from deploy_chatmail import deploy_chatmail from cmdeploy import deploy_chatmail
def main(): def main():

View File

@@ -86,7 +86,7 @@ plugin {
plugin { plugin {
# for now we define static quota-rules for all users # for now we define static quota-rules for all users
quota = maildir:User quota quota = maildir:User quota
quota_rule = *:storage=100M quota_rule = *:storage={{ config.max_mailbox_size }}
quota_max_mail_size=30M quota_max_mail_size=30M
quota_grace = 0 quota_grace = 0
# quota_over_flag_value = TRUE # quota_over_flag_value = TRUE
@@ -137,8 +137,8 @@ service imap-login {
} }
ssl = required ssl = required
ssl_cert = </var/lib/acme/live/{{ config.hostname }}/fullchain ssl_cert = </var/lib/acme/live/{{ config.mail_domain }}/fullchain
ssl_key = </var/lib/acme/live/{{ config.hostname }}/privkey ssl_key = </var/lib/acme/live/{{ config.mail_domain }}/privkey
ssl_dh = </usr/share/dovecot/dh.pem ssl_dh = </usr/share/dovecot/dh.pem
ssl_min_protocol = TLSv1.2 ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes ssl_prefer_server_ciphers = yes

View File

@@ -0,0 +1,4 @@
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} INBOX
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} Deltachat
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} Trash
2 30 * * * dovecot doveadm purge -A

View File

@@ -1,4 +1,4 @@
myorigin = {{ config.mailname }} myorigin = {{ config.mail_domain }}
smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU) smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no biff = no
@@ -16,8 +16,8 @@ readme_directory = no
compatibility_level = 2 compatibility_level = 2
# TLS parameters # TLS parameters
smtpd_tls_cert_file=/var/lib/acme/live/{{ config.mailname }}/fullchain smtpd_tls_cert_file=/var/lib/acme/live/{{ config.mail_domain }}/fullchain
smtpd_tls_key_file=/var/lib/acme/live/{{ config.mailname }}/privkey smtpd_tls_key_file=/var/lib/acme/live/{{ config.mail_domain }}/privkey
smtpd_tls_security_level=may smtpd_tls_security_level=may
smtp_tls_CApath=/etc/ssl/certs smtp_tls_CApath=/etc/ssl/certs
@@ -26,7 +26,7 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix smtp_tls_policy_maps = socketmap:inet:127.0.0.1:8461:postfix
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = {{ config.mailname }} myhostname = {{ config.mail_domain }}
alias_maps = hash:/etc/aliases alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases alias_database = hash:/etc/aliases
@@ -45,7 +45,7 @@ inet_interfaces = all
inet_protocols = all inet_protocols = all
virtual_transport = lmtp:unix:private/dovecot-lmtp virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = {{ config.mailname }} virtual_mailbox_domains = {{ config.mail_domain }}
smtpd_milters = unix:opendkim/opendkim.sock smtpd_milters = unix:opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters non_smtpd_milters = $smtpd_milters

View File

View File

@@ -0,0 +1,16 @@
import requests
from cmdeploy.genqr import gen_qr_png_data
def test_gen_qr_png_data(maildomain):
data = gen_qr_png_data(maildomain)
assert data
def test_fastcgi_working(maildomain, chatmail_config):
url = f"https://{maildomain}/cgi-bin/newemail.py"
print(url)
res = requests.post(url)
assert maildomain in res.json().get("email")
assert len(res.json().get("password")) > chatmail_config.password_min_length

View File

@@ -43,18 +43,18 @@ def test_reject_forged_from(cmsetup, maildata, gencreds, lp, forgeaddr):
@pytest.mark.slow @pytest.mark.slow
def test_exceed_rate_limit(cmsetup, gencreds, maildata): def test_exceed_rate_limit(cmsetup, gencreds, maildata, chatmail_config):
"""Test that the per-account send-mail limit is exceeded.""" """Test that the per-account send-mail limit is exceeded."""
user1, user2 = cmsetup.gen_users(2) user1, user2 = cmsetup.gen_users(2)
mail = maildata( mail = maildata(
"encrypted.eml", from_addr=user1.addr, to_addr=user2.addr "encrypted.eml", from_addr=user1.addr, to_addr=user2.addr
).as_string() ).as_string()
for i in range(100): for i in range(chatmail_config.max_user_send_per_minute + 5):
print("Sending mail", str(i)) print("Sending mail", str(i))
try: try:
user1.smtp.sendmail(user1.addr, [user2.addr], mail) user1.smtp.sendmail(user1.addr, [user2.addr], mail)
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
if i < 60: if i < chatmail_config.max_user_send_per_minute:
pytest.fail(f"rate limit was exceeded too early with msg {i}") pytest.fail(f"rate limit was exceeded too early with msg {i}")
outcome = e.recipients[user2.addr] outcome = e.recipients[user2.addr]
assert outcome[0] == 450 assert outcome[0] == 450

View File

@@ -1,4 +1,5 @@
import time import time
import re
import random import random
import pytest import pytest
@@ -20,14 +21,24 @@ class TestEndToEndDeltaChat:
assert msg2.text == "message0" assert msg2.text == "message0"
@pytest.mark.slow @pytest.mark.slow
def test_exceed_quota(self, cmfactory, lp, tmpdir, remote): def test_exceed_quota(self, cmfactory, lp, tmpdir, remote, chatmail_config):
"""This is a very slow test as it needs to upload >100MB of mail data """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. before quota is exceeded, and thus depends on the speed of the upload.
""" """
ac1, ac2 = cmfactory.get_online_accounts(2) ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2) chat = cmfactory.get_accepted_chat(ac1, ac2)
quota = 1024 * 1024 * 100 def parse_size_limit(limit: str) -> int:
"""Parse a size limit and return the number of bytes as integer.
Example input: 100M, 2.4T, 500 K
"""
units = {"B": 1, "K": 2**10, "M": 2**20, "G": 2**30, "T": 2**40}
size = re.sub(r"([KMGT])", r" \1", limit.upper())
number, unit = [string.strip() for string in size.split()]
return int(float(number) * units[unit])
quota = parse_size_limit(chatmail_config.max_mailbox_size)
attachsize = 1 * 1024 * 1024 attachsize = 1 * 1024 * 1024
num_to_send = quota // attachsize + 2 num_to_send = quota // attachsize + 2
lp.sec(f"ac1: send {num_to_send} large files to ac2") lp.sec(f"ac1: send {num_to_send} large files to ac2")
@@ -91,9 +102,9 @@ class TestEndToEndDeltaChat:
lp.sec("setup encrypted comms between ac1 and ac2 on different instances") lp.sec("setup encrypted comms between ac1 and ac2 on different instances")
qr = ac1.get_setup_contact_qr() qr = ac1.get_setup_contact_qr()
ac2.qr_setup_contact(qr) ch = ac2.qr_setup_contact(qr)
msg = ac2.wait_next_incoming_message() assert ch.id >= 10
assert "verified" in msg.text ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac1 sends a message and ac2 marks it as seen") lp.sec("ac1 sends a message and ac2 marks it as seen")
chat = ac1.create_chat(ac2) chat = ac1.create_chat(ac2)

View File

@@ -38,7 +38,7 @@ def pytest_runtest_setup(item):
@pytest.fixture @pytest.fixture
def chatmail_config(pytestconfig): def chatmail_config(pytestconfig):
current = basedir = Path() current = basedir = Path().resolve()
while 1: while 1:
path = current.joinpath("chatmail.ini").resolve() path = current.joinpath("chatmail.ini").resolve()
if path.exists(): if path.exists():
@@ -52,7 +52,7 @@ def chatmail_config(pytestconfig):
@pytest.fixture @pytest.fixture
def maildomain(chatmail_config): def maildomain(chatmail_config):
return chatmail_config.mailname return chatmail_config.mail_domain
@pytest.fixture @pytest.fixture
@@ -228,18 +228,25 @@ def imap_or_smtp(request):
@pytest.fixture @pytest.fixture
def gencreds(maildomain): def gencreds(chatmail_config):
count = itertools.count() count = itertools.count()
next(count) next(count)
def gen(domain=None): def gen(domain=None):
domain = domain if domain else maildomain domain = domain if domain else chatmail_config.mail_domain
while 1: while 1:
num = next(count) num = next(count)
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join(random.choices(alphanumeric, k=10)) user = "".join(
user = f"ac{num}_{user}"[:9] random.choices(alphanumeric, k=chatmail_config.username_max_length)
password = "".join(random.choices(alphanumeric, k=12)) )
if domain == "nine.testrun.org":
user = f"ac{num}_{user}"[:9]
else:
user = f"ac{num}_{user}"[: chatmail_config.username_max_length]
password = "".join(
random.choices(alphanumeric, k=chatmail_config.password_min_length)
)
yield f"{user}@{domain}", f"{password}" yield f"{user}@{domain}", f"{password}"
return lambda domain=None: next(gen(domain)) return lambda domain=None: next(gen(domain))

View File

@@ -1,7 +1,7 @@
import os import os
import pytest import pytest
from deploy_chatmail.cmdeploy import get_parser, main from cmdeploy.cmdeploy import get_parser, main
from chatmaild.config import read_config from chatmaild.config import read_config
@@ -25,7 +25,7 @@ class TestCmdline:
main(["init", "chat.example.org"]) main(["init", "chat.example.org"])
inipath = tmp_path.joinpath("chatmail.ini") inipath = tmp_path.joinpath("chatmail.ini")
config = read_config(inipath) config = read_config(inipath)
assert config.mailname == "chat.example.org" assert config.mail_domain == "chat.example.org"
def test_init_not_overwrite(self): def test_init_not_overwrite(self):
main(["init", "chat.example.org"]) main(["init", "chat.example.org"])

View File

@@ -1,10 +1,10 @@
import importlib.resources import importlib.resources
from deploy_chatmail.www import build_webpages from cmdeploy.www import build_webpages
def test_build_webpages(tmp_path, make_config): def test_build_webpages(tmp_path, make_config):
pkgroot = importlib.resources.files("deploy_chatmail") pkgroot = importlib.resources.files("cmdeploy")
src_dir = pkgroot.joinpath("../../../www/src").resolve() src_dir = pkgroot.joinpath("../../../www/src").resolve()
assert src_dir.exists(), src_dir assert src_dir.exists(), src_dir
config = make_config("chat.example.org") config = make_config("chat.example.org")

View File

@@ -36,8 +36,55 @@ def build_webpages(src_dir, build_dir, config):
print(traceback.format_exc()) print(traceback.format_exc())
def timespan_to_english(timespan):
val = int(timespan[:-1])
c = timespan[-1].lower()
match c:
case "y":
return f"{val} years"
case "m":
return f"{val} months"
case "w":
return f"{val} weeks"
case "d":
return f"{val} days"
case "h":
return f"{val} hours"
case "c":
return f"{val} seconds"
case _:
raise ValueError(
c
+ " is not a valid time unit. Try [y]ears, [w]eeks, [d]ays, or [h]ours"
)
def int_to_english(number):
if number >= 0 and number <= 12:
a = [
"zero",
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
]
return a[number]
elif number <= 50:
return str(number)
if number > 50:
return "more"
def _build_webpages(src_dir, build_dir, config): def _build_webpages(src_dir, build_dir, config):
mail_domain = config.mailname mail_domain = config.mail_domain
assert src_dir.exists(), src_dir assert src_dir.exists(), src_dir
if not build_dir.exists(): if not build_dir.exists():
build_dir.mkdir() build_dir.mkdir()
@@ -48,6 +95,18 @@ def _build_webpages(src_dir, build_dir, config):
for path in src_dir.iterdir(): for path in src_dir.iterdir():
if path.suffix == ".md": if path.suffix == ".md":
render_vars, content = prepare_template(path) render_vars, content = prepare_template(path)
render_vars["username_min_length"] = int_to_english(
config.username_min_length
)
render_vars["username_max_length"] = int_to_english(
config.username_max_length
)
render_vars["password_min_length"] = int_to_english(
config.password_min_length
)
render_vars["delete_mails_after"] = timespan_to_english(
config.delete_mails_after
)
target = build_dir.joinpath(path.stem + ".html") target = build_dir.joinpath(path.stem + ".html")
# recursive jinja2 rendering # recursive jinja2 rendering
@@ -71,7 +130,7 @@ def main():
inipath = reporoot.joinpath("chatmail.ini") inipath = reporoot.joinpath("chatmail.ini")
config = read_config(inipath) config = read_config(inipath)
config.webdev = True config.webdev = True
assert config.mailname assert config.mail_domain
www_path = reporoot.joinpath("www") www_path = reporoot.joinpath("www")
src_path = www_path.joinpath("src") src_path = www_path.joinpath("src")
stats = None stats = None

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

@@ -2,8 +2,8 @@
set -e set -e
python3 -m venv venv python3 -m venv venv
venv/bin/pip install -e deploy-chatmail
venv/bin/pip install -e chatmaild venv/bin/pip install -e chatmaild
venv/bin/pip install -e cmdeploy
source venv/bin/activate source venv/bin/activate
echo activated 'venv' python virtualenv environment containing "cmdeploy" tool echo activated 'venv' python virtualenv environment containing "cmdeploy" tool

View File

@@ -1,6 +0,0 @@
from deploy_chatmail.genqr import gen_qr_png_data
def test_gen_qr_png_data(maildomain):
data = gen_qr_png_data(maildomain)
assert data

View File

@@ -1,5 +1,5 @@
<img width="800px" src="collage-top.png"/> <img class="banner" src="collage-top.png"/>
## Dear [Delta Chat](https://get.delta.chat) users and newcomers, ## Dear [Delta Chat](https://get.delta.chat) users and newcomers,
@@ -14,6 +14,7 @@ Welcome to instant, interoperable and [privacy-preserving](privacy.html) messagi
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee) 💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
<div class="experimental">Note: this is an experimental service</div>
## ⚡ Note: this is an experimental service ⚡

View File

@@ -1,5 +1,5 @@
<img width="800px" src="collage-info.png"/> <img class="banner" src="collage-info.png"/>
## More information ## More information
@@ -9,10 +9,21 @@ In the Delta Chat account setup
you may tap `LOG INTO YOUR E-MAIL ACCOUNT` you may tap `LOG INTO YOUR E-MAIL ACCOUNT`
and fill the two fields like this: and fill the two fields like this:
- `Address`: invent a word with *exactly* nine characters - `Address`: invent a word with
and append `@{{config.mail_domain}}` to it. {% if username_min_length == username_max_length %}
*exactly* {{ username_min_length }}
{% else %}
{{ username_min_length}}
{% if username_max_length == "more" %}
or more
{% else %}
to {{ username_max_length }}
{% endif %}
{% endif %}
characters
and append `@{{config.mail_domain}}` to it.
- `Password`: invent at least 9 characters. - `Password`: invent at least {{ password_min_length }} characters.
If the e-mail address is not yet taken, you'll get that account. If the e-mail address is not yet taken, you'll get that account.
The first login sets your password. The first login sets your password.
@@ -24,11 +35,11 @@ The first login sets your password.
{{config.mail_domain}} but setting up contact via [QR invite codes](https://delta.chat/en/help#howtoe2ee) {{config.mail_domain}} but setting up contact via [QR invite codes](https://delta.chat/en/help#howtoe2ee)
allows your messages to pass freely to any outside recipients. allows your messages to pass freely to any outside recipients.
- You may send up to 60 messages per minute - You may send up to {{ config.max_user_send_per_minute }} messages per minute.
- Messages are unconditionally removed 40 days after arriving on the server - Seen messages are removed {{ delete_mails_after }} after arriving on the server.
- You can store up to [100MB messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server) - You can store up to [{{ config.max_mailbox_size }} messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server).
### Who are the operators? Which software is running? ### Who are the operators? Which software is running?

44
www/src/logo.svg Normal file
View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="145"
height="145"
version="1.1"
id="svg4"
sodipodi:docname="At_sign.svg"
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="3.0241379"
inkscape:cx="67.622577"
inkscape:cy="72.913341"
inkscape:window-width="1390"
inkscape:window-height="1027"
inkscape:window-x="55"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<g
aria-label="@"
id="text2"
style="font-size:144px;font-family:Arial">
<path
d="m 79.927878,94.422406 c -2.704286,3.120332 -5.741407,5.637394 -9.111364,7.551194 -3.328352,1.87221 -6.677506,2.80831 -10.047463,2.80831 -3.702792,0 -7.301573,-1.08172 -10.796342,-3.24515 -3.49477,-2.163426 -6.344671,-5.491779 -8.549704,-9.985058 -2.163429,-4.493275 -3.245144,-9.423397 -3.245144,-14.790365 0,-6.615099 1.684978,-13.230199 5.054935,-19.845299 3.411561,-6.656705 7.634407,-11.649233 12.66854,-14.977585 5.034133,-3.328352 9.92265,-4.992528 14.665552,-4.992528 3.619583,0 7.072748,0.956901 10.359496,2.870704 3.286748,1.872198 6.115847,4.742902 8.487297,8.612111 l 2.121825,-9.673023 h 11.170784 l -8.986557,41.87483 c -1.248129,5.824616 -1.872194,9.048957 -1.872194,9.673023 0,1.123319 0.416044,2.101022 1.248132,2.93311 0.873692,0.790484 1.913802,1.185726 3.120332,1.185726 2.20503,0 5.096537,-1.268934 8.674517,-3.806803 4.7429,-3.328352 8.4873,-7.780023 11.23319,-13.355013 2.78749,-5.616594 4.18124,-11.399606 4.18124,-17.349035 0,-6.947935 -1.78899,-13.438222 -5.36697,-19.47086 -3.53637,-6.032638 -8.84094,-10.858749 -15.913687,-14.478332 -7.03114,-3.619583 -14.811161,-5.429374 -23.340064,-5.429374 -9.73543,0 -18.638772,2.288242 -26.710026,6.864726 -8.029649,4.534879 -14.27031,11.06677 -18.721981,19.595673 -4.410066,8.487298 -6.615099,17.598662 -6.615099,27.334092 0,10.193078 2.205033,18.971607 6.615099,26.33559 2.290454,3.78888 -7.136335,18.96983 -3.810585,21.73443 3.138096,2.60861 18.971963,-7.14297 23.031819,-5.44631 8.404089,3.53637 17.702673,5.30456 27.895752,5.30456 10.90035,0 20.032515,-1.83059 27.396492,-5.49178 7.36399,-3.66119 12.87657,-8.11286 16.53776,-13.35501 l 9.29559,4 c -2.12183,4.36846 -3.76221,4.82013 -8.92116,9.35501 -5.15895,4.53488 -11.2956,8.11286 -18.40995,10.73393 -7.114346,2.66268 -15.684851,3.99402 -25.711512,3.99402 -9.236177,0 -17.76508,-1.18572 -25.586707,-3.55717 -7.780023,-2.37145 -29.296198,9.26152 -34.78798,4.47701 -5.49178,-4.7429 5.248856,-25.42482 2.461361,-31.62388 -3.49477,-7.863231 -5.242155,-16.350531 -5.242155,-25.461894 0,-10.151474 2.08022,-19.824498 6.240661,-29.019071 5.075736,-11.274793 12.273297,-19.907706 21.592683,-25.898739 9.360991,-5.991034 20.69819,-8.986551 34.011599,-8.986551 10.317891,0 19.574873,2.121824 27.77093,6.365473 8.23767,4.202045 14.72796,10.484309 19.47086,18.846794 4.03563,7.197561 6.05344,15.019189 6.05344,23.464883 0,12.065277 -4.24365,22.77841 -12.73094,32.1394 -7.572,8.404095 -15.85128,12.606135 -24.837827,12.606135 -2.870704,0 -5.200551,-0.43684 -6.98954,-1.31053 -1.747385,-0.8737 -3.037121,-2.12183 -3.869209,-3.744402 -0.540857,-1.040114 -0.936099,-2.829105 -1.185726,-5.366972 z M 49.723082,77.510217 c 0,5.699803 1.352143,10.130671 4.05643,13.292606 2.704286,3.161935 5.803814,4.742902 9.298583,4.742902 2.329847,0 4.784506,-0.686473 7.363979,-2.059418 2.579473,-1.41455 5.034133,-3.49477 7.363979,-6.240661 2.371451,-2.74589 4.306056,-6.219857 5.803815,-10.421902 1.497759,-4.243649 2.246638,-8.487298 2.246638,-12.730947 0,-5.658198 -1.41455,-10.047462 -4.243649,-13.167793 -2.787495,-3.12033 -6.199056,-4.680495 -10.234683,-4.680495 -2.662682,0 -5.179749,0.686473 -7.5512,2.059418 -2.329846,1.331341 -4.597286,3.494769 -6.802319,6.490286 -2.205033,2.995517 -3.97322,6.635903 -5.304561,10.921156 -1.331341,4.285253 -1.997012,8.216869 -1.997012,11.794848 z"
id="path347"
style="stroke-width:0.887561"
sodipodi:nodetypes="ccsscscsscccccscsccsccsccscscssccscscccsccsccscscccssscccscscsss" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

31
www/src/main.css Normal file
View File

@@ -0,0 +1,31 @@
#menu {
display: flex;
flex-wrap: wrap;
padding: 0;
}
#menu li {
display: inline-block;
padding-right: 0.5em;
}
#domain {
margin-left: auto;
}
#domain a {
color: #888;
}
.banner {
width: 100%;
}
.experimental {
margin: 3em 0;
padding: 1em;
border: 4px dashed red;
color: red;
font-weight: bold;
}

View File

@@ -7,14 +7,21 @@
{% endif %} {% endif %}
<title>{{ config.mail_domain }} {{ pagename }}</title> <title>{{ config.mail_domain }} {{ pagename }}</title>
<link rel="stylesheet" href="./water.css"> <link rel="stylesheet" href="./water.css">
<link rel="stylesheet" href="./main.css">
<link rel="icon" href="/logo.svg">
<link rel=”mask-icon” href=”/logo.svg” color=”#000000">
</head> </head>
<body> <body>
<ul id="menu">
<li><a href="index.html">home</a></li>
<li><a href="info.html">info</a></li>
<li><a href="privacy.html">privacy</a></li>
<li><a href="https://github.com/deltachat/chatmail">public code ↗</a></li>
<li id="domain"><a href="index.html">{{ config.mail_domain }}</a></li>
</ul>
{{ markdown_html }} {{ markdown_html }}
<footer>
<a href="index.html">home</a> |
<a href="info.html">more info</a> |
<a href="privacy.html">privacy</a> |
<a href="https://github.com/deltachat/chatmail">-> public development </a>
</footer>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,4 @@
<img width="800px" src="collage-privacy.png"/> <img class="banner" src="collage-privacy.png"/>
# Privacy Policy for {{ config.mail_domain }} # Privacy Policy for {{ config.mail_domain }}