Compare commits

..

40 Commits

Author SHA1 Message Date
holger krekel
805d743e9e show PATH env 2023-12-11 15:29:21 +01:00
holger krekel
b0d5ee084e fix cmdeploy test command 2023-12-11 15:22:12 +01:00
holger krekel
56c7853e5b remove tox run from deploy-chatmail and use 'cmdeploy fmt' and 'pytest' directly 2023-12-11 13:51:45 +01:00
holger krekel
071d708a89 move tests/chatmaild to chatmaild package, streamline tests and fixtures accordingly 2023-12-11 13:04:11 +01:00
holger krekel
c7c7ed8ff2 fix tests and run all tests on "cmdeploy test" 2023-12-11 12:18:10 +01:00
holger krekel
0aa0ef8a74 discover chatmail.ini in tests from CWD and all parent dirs (tox runs change dirs) 2023-12-11 02:27:05 +01:00
holger krekel
0662e3a8b1 "cmdeploy test" now installs deltachat if it's not there 2023-12-11 02:13:46 +01:00
holger krekel
6c453f93a1 consistently use shell helper 2023-12-11 01:58:31 +01:00
holger krekel
54f29f6bae consistently show ssh/shell output 2023-12-11 01:50:56 +01:00
holger krekel
ad8fee76cd add "build" dependency 2023-12-11 01:45:32 +01:00
holger krekel
528cd3da25 always show which ssh-commands execute 2023-12-11 01:43:09 +01:00
holger krekel
5734e00625 some more shifting around 2023-12-11 01:36:11 +01:00
holger krekel
947e1d6f89 shift functions around, discover sub commands automatically 2023-12-11 01:18:57 +01:00
holger krekel
33423459fe make tests depend on chatmail.ini, not env var 2023-12-11 00:49:32 +01:00
holger krekel
c70b72a21a tweak for making CI happy 2023-12-11 00:05:25 +01:00
holger krekel
33352f4694 try to fix workflow 2023-12-10 18:28:40 +01:00
holger krekel
59083ad16a don't print a traceback but do a proper return code for "cmdeploy test" 2023-12-10 18:24:08 +01:00
holger krekel
11518c2ef4 add chatmail.ini to ignore 2023-12-10 18:05:11 +01:00
holger krekel
a0cdfe6126 add manifest so that ini files get included 2023-12-10 18:02:00 +01:00
holger krekel
c25eefccc4 address nami comment 2023-12-10 17:56:26 +01:00
holger krekel
8b878e38cf fix readme 2023-12-10 14:41:20 +01:00
holger krekel
9e1e6d3c69 add test command 2023-12-10 14:38:57 +01:00
holger krekel
a1c817d758 fix README 2023-12-10 12:52:35 +01:00
holger krekel
ccc552f852 add status command and delete last script 2023-12-10 12:45:23 +01:00
holger krekel
73768256f6 some more housekeeping 2023-12-10 12:17:05 +01:00
holger krekel
34f7b3c0d3 introduce "cmdeploy bench" 2023-12-10 12:10:36 +01:00
holger krekel
e2828f4103 cleanup 2023-12-10 12:00:03 +01:00
holger krekel
df515bea41 generate dns zone file via cmdeploy 2023-12-10 11:56:41 +01:00
holger krekel
5614f03611 add dns command beginning 2023-12-09 18:07:44 +01:00
holger krekel
70443545d7 make cmdeploy test work 2023-12-09 17:55:08 +01:00
holger krekel
10ef842061 snap 2023-12-09 17:45:26 +01:00
holger krekel
98f92cd9b6 fix various test setups 2023-12-09 16:54:05 +01:00
holger krekel
fe99b97386 add webdev sub command 2023-12-09 16:42:03 +01:00
holger krekel
542decf798 making it work 2023-12-09 15:15:57 +01:00
holger krekel
2e7f8483b3 rework UI for chatmail setup 2023-12-09 13:43:56 +01:00
holger krekel
5c58e625f0 draft init flow 2023-12-09 02:07:09 +01:00
holger krekel
009f549619 document some attributes in chatmail.ini 2023-12-09 01:20:17 +01:00
holger krekel
99d36235fe get passthrough_recipients list from config 2023-12-09 01:07:37 +01:00
holger krekel
b52a8c969f various fixes 2023-12-09 00:22:58 +01:00
holger krekel
8520a9d8f2 introduce basic config file 2023-12-08 21:56:15 +01:00
61 changed files with 162 additions and 369 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 --pyargs cmdeploy run: pytest tests
- name: initialize with chatmail domain - name: initialize with chatmail domain
run: cmdeploy init chat.example.org run: cmdeploy init chat.example.org

View File

@@ -25,11 +25,10 @@ DNS domain name (FQDN), for example `chat.example.org`.
ssh root@CHATMAIL_DOMAIN ssh root@CHATMAIL_DOMAIN
``` ```
2. Install the `cmdeploy` command in a virtualenv and activate it 2. Install the `cmdeploy` command in a virtualenv
``` ```
scripts/initenv.sh source scripts/initenv.sh
. venv/bin/activate
``` ```
3. Create chatmail configuration file `chatmail.ini`: 3. Create chatmail configuration file `chatmail.ini`:

View File

@@ -9,17 +9,11 @@ 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.mail_domain = params["mail_domain"] self.mailname = self.mail_domain = params["mailname"]
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")
@@ -29,14 +23,12 @@ class Config:
return open(self._inipath, "rb") return open(self._inipath, "rb")
def write_initial_config(inipath, mail_domain): def write_initial_config(inipath, mailname):
from importlib.resources import files from importlib.resources import files
inidir = files(__package__).joinpath("ini") inidir = files(__package__).joinpath("ini")
content = ( content = inidir.joinpath("chatmail.ini.f").read_text().format(mailname=mailname)
inidir.joinpath("chatmail.ini.f").read_text().format(mail_domain=mail_domain) if mailname.endswith(".testrun.org"):
)
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,7 +12,6 @@ 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"
@@ -23,17 +22,14 @@ def encrypt_password(password: str):
return "{SHA512-CRYPT}" + passhash return "{SHA512-CRYPT}" + passhash
def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: def is_allowed_to_create(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) < config.password_min_length: if len(cleartext_password) < 9:
logging.warning( logging.warning("Password needs to be at least 9 characters long")
"Password needs to be at least %s characters long",
config.password_min_length,
)
return False return False
parts = user.split("@") parts = user.split("@")
@@ -42,17 +38,11 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
return False return False
localpart, domain = parts localpart, domain = parts
if ( if domain == "nine.testrun.org":
len(localpart) > config.username_max_length # nine.testrun.org policy, username has to be exactly nine chars
or len(localpart) < config.username_min_length if len(localpart) != 9:
): logging.warning(f"localpart {localpart!r} has not exactly nine chars")
logging.warning( return False
"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
@@ -70,7 +60,7 @@ def lookup_userdb(db, user):
return get_user_data(db, user) return get_user_data(db, user)
def lookup_passdb(db, config: Config, user, cleartext_password): def lookup_passdb(db, 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:
@@ -82,7 +72,7 @@ def lookup_passdb(db, config: Config, 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(config, user, cleartext_password): if not is_allowed_to_create(user, cleartext_password):
return return
encrypted_password = encrypt_password(cleartext_password) encrypted_password = encrypt_password(cleartext_password)
@@ -97,7 +87,7 @@ def lookup_passdb(db, config: Config, user, cleartext_password):
) )
def handle_dovecot_request(msg, db, config: Config): def handle_dovecot_request(msg, db, mail_domain):
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")
@@ -107,15 +97,15 @@ def handle_dovecot_request(msg, db, config: Config):
res = "" res = ""
if namespace == "shared": if namespace == "shared":
if type == "userdb": if type == "userdb":
if user.endswith(f"@{config.mail_domain}"): if user.endswith(f"@{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"@{config.mail_domain}"): if user.endswith(f"@{mail_domain}"):
res = lookup_passdb(db, config, user, cleartext_password=args[0]) res = lookup_passdb(db, user, cleartext_password=args[0])
if res: if res:
reply_command = "O" reply_command = "O"
else: else:
@@ -133,7 +123,8 @@ 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])
config = read_config(sys.argv[4]) with open("/etc/mailname", "r") as fp:
mail_domain = fp.read().strip()
class Handler(StreamRequestHandler): class Handler(StreamRequestHandler):
def handle(self): def handle(self):
@@ -142,7 +133,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, config) res = handle_dovecot_request(msg, db, mail_domain)
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 {config_path} ExecStart={execpath} /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite
Restart=always Restart=always
RestartSec=30 RestartSec=30

View File

@@ -111,9 +111,6 @@ 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,54 +1,24 @@
[params] [params]
# mail domain (MUST be set to fully qualified chat mail domain) # mail domain (MUST be set to fully qualified chat mail domain)
mail_domain = {mail_domain} mailname = {mailname}
# #
# 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,25 +1,23 @@
#!/usr/local/lib/chatmaild/venv/bin/python3 #!/usr/bin/python3
""" CGI script for creating new accounts. """ """ CGI script for creating new accounts. """
import json import json
import random import random
from chatmaild.config import read_config, Config mailname_path = "/etc/mailname"
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
def create_newemail_dict(config: Config): def create_newemail_dict(domain):
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join(random.choices(alphanumeric, k=config.username_min_length)) user = "".join(random.choices(alphanumeric, k=9))
password = "".join(random.choices(alphanumeric, k=config.password_min_length + 3)) password = "".join(random.choices(alphanumeric, k=12))
return dict(email=f"{user}@{config.mail_domain}", password=f"{password}") return dict(email=f"{user}@{domain}", password=f"{password}")
def print_new_account(): def print_new_account():
config = read_config(CONFIG_PATH) domain = open(mailname_path).read().strip()
creds = create_newemail_dict(config) creds = create_newemail_dict(domain=domain)
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(mail_domain): def make_conf(mailname):
write_initial_config(inipath, mail_domain=mail_domain) write_initial_config(inipath, mailname=mailname)
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.mail_domain return example_config.mailname
@pytest.fixture @pytest.fixture

View File

@@ -1,21 +1,22 @@
from chatmaild.config import read_config from chatmaild.config import read_config
def test_read_config_basic(example_config): def test_read_config_basic(make_config):
assert example_config.mail_domain == "chat.example.org" config = make_config("chat.example.org")
assert not example_config.privacy_supervisor and not example_config.privacy_mail assert config.mailname == "chat.example.org"
assert not example_config.privacy_pdo and not example_config.privacy_postal assert not config.privacy_supervisor and not config.privacy_mail
assert not config.privacy_pdo and not config.privacy_postal
inipath = example_config._inipath inipath = config._inipath
inipath.write_text(inipath.read_text().replace("60", "37")) inipath.write_text(inipath.read_text().replace("60", "37"))
example_config = read_config(inipath) config = read_config(inipath)
assert example_config.max_user_send_per_minute == 37 assert config.max_user_send_per_minute == 37
assert example_config.mail_domain == "chat.example.org" assert config.mailname == "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.mail_domain == "something.testrun.org" assert config.mailname == "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
@@ -23,10 +24,4 @@ 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.max_mailbox_size == "100M" assert config.passthrough_recipients
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,35 +9,29 @@ 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, example_config): def test_basic(db):
lookup_passdb(db, example_config, "asdf12345@chat.example.org", "q9mr3faue") lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
data = get_user_data(db, "asdf12345@chat.example.org") data = get_user_data(db, "link2xt@c1.testrun.org")
assert data assert data
data2 = lookup_passdb( data2 = lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
db, example_config, "asdf12345@chat.example.org", "q9mr3jewvadsfaue"
)
assert data == data2 assert data == data2
def test_dont_overwrite_password_on_wrong_login(db, example_config): def test_dont_overwrite_password_on_wrong_login(db):
"""Test that logging in with a different password doesn't create a new user""" """Test that logging in with a different password doesn't create a new user"""
res = lookup_passdb( res = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
db, example_config, "newuser12@chat.example.org", "kajdlkajsldk12l3kj1983"
)
assert res["password"] assert res["password"]
res2 = lookup_passdb(db, example_config, "newuser12@chat.example.org", "kajdslqwe") res2 = lookup_passdb(db, "newuser1@something.org", "kajdlqweqwe")
# this function always returns a password hash, which is actually compared by dovecot. # this function always returns a password hash, which is actually compared by dovecot.
assert res["password"] == res2["password"] assert res["password"] == res2["password"]
def test_nocreate_file(db, monkeypatch, tmpdir, example_config): def test_nocreate_file(db, monkeypatch, tmpdir):
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( lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik")
db, example_config, "newuser12@chat.example.org", "zequ0Aimuchoodaechik" assert not get_user_data(db, "newuser1@something.org")
)
assert not get_user_data(db, "newuser12@chat.example.org")
def test_db_version(db): def test_db_version(db):
@@ -51,21 +45,21 @@ def test_too_high_db_version(db):
db.ensure_tables() db.ensure_tables()
def test_handle_dovecot_request(db, example_config): def test_handle_dovecot_request(db):
msg = ( msg = (
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/" "Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
"some42123@chat.example.org\tsome42123@chat.example.org" "some42@c3.testrun.org\tsome42@c3.testrun.org"
) )
res = handle_dovecot_request(msg, db, example_config) res = handle_dovecot_request(msg, db, "c3.testrun.org")
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/some42123@chat.example.org" assert userdata["home"] == "/home/vmail/some42@c3.testrun.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, example_config): def test_50_concurrent_lookups_different_accounts(db, gencreds):
num_threads = 50 num_threads = 50
req_per_thread = 5 req_per_thread = 5
results = queue.Queue() results = queue.Queue()
@@ -74,7 +68,7 @@ def test_50_concurrent_lookups_different_accounts(db, gencreds, example_config):
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, example_config, addr, password) lookup_passdb(db, addr, password)
except Exception: except Exception:
results.put(traceback.format_exc()) results.put(traceback.format_exc())
else: else:

View File

@@ -127,19 +127,3 @@ 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,24 +4,26 @@ 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(example_config): def test_create_newemail_dict():
ac1 = create_newemail_dict(example_config) ac1 = create_newemail_dict(domain="example.org")
assert "@" in ac1["email"] assert "@" in ac1["email"]
assert len(ac1["password"]) >= 10 assert len(ac1["password"]) >= 10
ac2 = create_newemail_dict(example_config) ac2 = create_newemail_dict(domain="example.org")
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, example_config): def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir):
monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath)) p = tmpdir.join("mailname")
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"@{example_config.mail_domain}") assert dic["email"].endswith(f"@{maildomain}")
assert len(dic["password"]) >= 10 assert len(dic["password"]) >= 10

View File

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

@@ -3,8 +3,8 @@ requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "cmdeploy" name = "deploy-chatmail"
version = "0.2" version = "0.1"
dependencies = [ dependencies = [
"pyinfra", "pyinfra",
"pillow", "pillow",
@@ -22,11 +22,10 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
cmdeploy = "cmdeploy.cmdeploy:main" cmdeploy = "deploy_chatmail.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 virtualenv /usr/local/lib/postfix-mta-sts-resolver", "python3 -m venv /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(config: Config, debug: bool = False) -> bool: def _configure_dovecot(mail_server: str, 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(config: Config, debug: bool = False) -> bool:
user="root", user="root",
group="root", group="root",
mode="644", mode="644",
config=config, config={"hostname": mail_server},
debug=debug, debug=debug,
) )
need_restart |= main_config.changed need_restart |= main_config.changed
@@ -266,13 +266,14 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
) )
need_restart |= auth_config.changed need_restart |= auth_config.changed
files.template( files.put(
src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"), src=importlib.resources.files(__package__)
.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/
@@ -346,8 +347,8 @@ def _configure_nginx(domain: str, debug: bool = False) -> bool:
def check_config(config): def check_config(config):
mail_domain = config.mail_domain mailname = config.mailname
if mail_domain != "testrun.org" and not mail_domain.endswith(".testrun.org"): if mailname != "testrun.org" and not mailname.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):
@@ -378,20 +379,6 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
system=True, system=True,
) )
# Run local DNS resolver `unbound`.
# `resolvconf` takes care of setting up /etc/resolv.conf
# to use 127.0.0.1 as the resolver.
apt.packages(
name="Install unbound",
packages="unbound",
)
systemd.service(
name="Start and enable unbound",
service="unbound.service",
running=True,
enabled=True,
)
# Deploy acmetool to have TLS certificates. # Deploy acmetool to have TLS certificates.
deploy_acmetool(nginx_hook=True, domains=[mail_server, f"mta-sts.{mail_server}"]) deploy_acmetool(nginx_hook=True, domains=[mail_server, f"mta-sts.{mail_server}"])
@@ -436,7 +423,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(config, debug=debug) dovecot_need_restart = _configure_dovecot(mail_server, 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.mail_domain env["CHATMAIL_DOMAIN"] = args.config.mailname
deploy_path = "cmdeploy/src/cmdeploy/deploy.py" deploypy = "deploy-chatmail/src/deploy_chatmail/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.mail_domain} {deploy_path}" cmd = f"{pyinf} --ssh-user root {args.config.mailname} {deploypy}"
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.mail_domain}" ssh = f"ssh root@{args.config.mailname}"
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.mail_domain} zone data (using space as separator) to stdout output]", f"[writing {args.config.mailname} 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.mail_domain}", email=f"root@{args.config.mailname}",
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.mail_domain, chatmail_domain=args.config.mailname,
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.mail_domain}" ssh = f"ssh root@{args.config.mailname}"
out.green(f"chatmail domain: {args.config.mail_domain}") out.green(f"chatmail domain: {args.config.mailname}")
if args.config.privacy_mail: if args.config.privacy_mail:
out.green("privacy settings: present") out.green("privacy settings: present")
else: else:
@@ -110,15 +110,6 @@ 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.
@@ -130,10 +121,9 @@ 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")
pytest_args = [pytest_path, "cmdeploy/src/", "-n4", "-rs", "-x", "-vrx", "--durations=5"] ret = out.run_ret(
if args.slow: [pytest_path, "tests/", "-n4", "-rs", "-x", "-vrx", "--durations=5"]
pytest_args.append("--slow") )
ret = out.run_ret(pytest_args)
return ret return ret
@@ -155,9 +145,13 @@ def fmt_cmd_options(parser):
def fmt_cmd(args, out): def fmt_cmd(args, out):
"""Run formattting fixes (ruff and black) on all chatmail source code.""" """Run formattting fixes (fuff and black) on all chatmail source code."""
chatmaild = importlib.resources.files("chatmaild")
deploy_chatmail = importlib.resources.files("deploy_chatmail")
tests = deploy_chatmail.joinpath("../../../tests")
sources = list(str(x) for x in [chatmaild, deploy_chatmail, tests])
sources = [str(importlib.resources.files(x)) for x in ("chatmaild", "cmdeploy")]
black_args = [shutil.which("black")] black_args = [shutil.which("black")]
ruff_args = [shutil.which("ruff")] ruff_args = [shutil.which("ruff")]
@@ -180,10 +174,9 @@ 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."""
args = ["pytest", "--pyargs", "cmdeploy.tests.online.benchmark", "-vrx"] pytest_path = shutil.which("pytest")
cmdstring = " ".join(args) benchmark = "tests/online/benchmark.py"
out.green(f"[$ {cmdstring}]") subprocess.check_call([pytest_path, benchmark, "-vrx"])
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 cmdeploy import deploy_chatmail from deploy_chatmail 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={{ config.max_mailbox_size }} quota_rule = *:storage=100M
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.mail_domain }}/fullchain ssl_cert = </var/lib/acme/live/{{ config.hostname }}/fullchain
ssl_key = </var/lib/acme/live/{{ config.mail_domain }}/privkey ssl_key = </var/lib/acme/live/{{ config.hostname }}/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 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,4 +1,4 @@
myorigin = {{ config.mail_domain }} myorigin = {{ config.mailname }}
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.mail_domain }}/fullchain smtpd_tls_cert_file=/var/lib/acme/live/{{ config.mailname }}/fullchain
smtpd_tls_key_file=/var/lib/acme/live/{{ config.mail_domain }}/privkey smtpd_tls_key_file=/var/lib/acme/live/{{ config.mailname }}/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.mail_domain }} myhostname = {{ config.mailname }}
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.mail_domain }} virtual_mailbox_domains = {{ config.mailname }}
smtpd_milters = unix:opendkim/opendkim.sock smtpd_milters = unix:opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters non_smtpd_milters = $smtpd_milters

View File

@@ -37,7 +37,7 @@ def build_webpages(src_dir, build_dir, config):
def _build_webpages(src_dir, build_dir, config): def _build_webpages(src_dir, build_dir, config):
mail_domain = config.mail_domain mail_domain = config.mailname
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()
@@ -71,7 +71,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.mail_domain assert config.mailname
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

@@ -2,5 +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
echo activated 'venv' python virtualenv environment containing "cmdeploy" tool

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().resolve() current = basedir = Path()
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.mail_domain return chatmail_config.mailname
@pytest.fixture @pytest.fixture
@@ -228,25 +228,18 @@ def imap_or_smtp(request):
@pytest.fixture @pytest.fixture
def gencreds(chatmail_config): def gencreds(maildomain):
count = itertools.count() count = itertools.count()
next(count) next(count)
def gen(domain=None): def gen(domain=None):
domain = domain if domain else chatmail_config.mail_domain domain = domain if domain else maildomain
while 1: while 1:
num = next(count) num = next(count)
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join( user = "".join(random.choices(alphanumeric, k=10))
random.choices(alphanumeric, k=chatmail_config.username_max_length) user = f"ac{num}_{user}"[:9]
) 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

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

@@ -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, chatmail_config): def test_exceed_rate_limit(cmsetup, gencreds, maildata):
"""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(chatmail_config.max_user_send_per_minute + 5): for i in range(100):
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 < chatmail_config.max_user_send_per_minute: if i < 60:
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,5 +1,4 @@
import time import time
import re
import random import random
import pytest import pytest
@@ -21,24 +20,14 @@ 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, chatmail_config): def test_exceed_quota(self, cmfactory, lp, tmpdir, remote):
"""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)
def parse_size_limit(limit: str) -> int: quota = 1024 * 1024 * 100
"""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")
@@ -102,9 +91,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()
ch = ac2.qr_setup_contact(qr) ac2.qr_setup_contact(qr)
assert ch.id >= 10 msg = ac2.wait_next_incoming_message()
ac1._evtracker.wait_securejoin_inviter_progress(1000) assert "verified" in msg.text
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

@@ -1,7 +1,7 @@
import os import os
import pytest import pytest
from cmdeploy.cmdeploy import get_parser, main from deploy_chatmail.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.mail_domain == "chat.example.org" assert config.mailname == "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 cmdeploy.www import build_webpages from deploy_chatmail.www import build_webpages
def test_build_webpages(tmp_path, make_config): def test_build_webpages(tmp_path, make_config):
pkgroot = importlib.resources.files("cmdeploy") pkgroot = importlib.resources.files("deploy_chatmail")
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

@@ -1,5 +1,5 @@
<img class="banner" src="collage-top.png"/> <img width="800px" 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,7 +14,6 @@ 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 class="banner" src="collage-info.png"/> <img width="800px" src="collage-info.png"/>
## More information ## More information
@@ -26,7 +26,7 @@ The first login sets your password.
- You may send up to 60 messages per minute - You may send up to 60 messages per minute
- Seen messages are removed 40 days after arriving on the server - Messages are unconditionally removed 40 days 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 [100MB messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server)

View File

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

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,31 +0,0 @@
#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,21 +7,14 @@
{% 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 class="banner" src="collage-privacy.png"/> <img width="800px" src="collage-privacy.png"/>
# Privacy Policy for {{ config.mail_domain }} # Privacy Policy for {{ config.mail_domain }}