Compare commits

...

27 Commits

Author SHA1 Message Date
B. Petersen
5d46335066 slightly smaller font size 2023-12-13 15:31:04 +01:00
B. Petersen
da5fcd835f add 'viewport' instructions 2023-12-13 15:30:48 +01:00
holger krekel
b055736439 rename benchmarks for blog post 2023-12-13 11:27:52 +01:00
missytake
2817ffd411 www: get actual account restrictions from chatmail.ini 2023-12-13 00:40:01 +01:00
B. Petersen
db45dc071b extract really needed styles from water.css; skip darkmode as this adds quite some burden to site owners and is easily overseen (eg. already our banners are not-so-nice in tdarkmode), if ppl want to use darkmode, they should actively adapt 2023-12-13 00:20:58 +01:00
link2xt
1c0543cb46 Update README.md
Co-authored-by: missytake <missytake@systemli.org>
2023-12-12 23:37:16 +01:00
link2xt
dde879c7fc Add scripts/cmdeploy 2023-12-12 23:37:16 +01:00
link2xt
cf95dfd49d Setup unbound DNS resolver 2023-12-12 21:52:05 +00:00
link2xt
4a96f19faf s/fuff/ruff/ 2023-12-12 20:29:34 +00:00
link2xt
a2a78c0aff Do not attmpt to activate venv from scripts/initenv.sh
If you run it as scripts/initenv.sh,
activating venv is useless as bash will exit immediately afterwards.

If you `source` it as suggested by README.md,
`set -e` will set the flag for the current shell
and your shell will exit as soon as some command returns non-zero status,
e.g. cmdeploy fails or you simply do `ls /foo/bar/baz` and `ls`
complains that `/foo/bar/baz` does not exist.
2023-12-12 20:22:34 +00: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
31 changed files with 457 additions and 1812 deletions

View File

@@ -25,54 +25,56 @@ DNS domain name (FQDN), for example `chat.example.org`.
ssh root@CHATMAIL_DOMAIN
```
2. Install the `cmdeploy` command in a virtualenv
2. Install the `cmdeploy` command in a virtualenv
```
source scripts/initenv.sh
git clone https://github.com/deltachat/chatmail
cd chatmail
scripts/initenv.sh
```
3. Create chatmail configuration file `chatmail.ini`:
```
cmdeploy init CHATMAIL_DOMAIN
scripts/cmdeploy init CHATMAIL_DOMAIN
```
4. Deploy to the remote chatmail server:
```
cmdeploy run
scripts/cmdeploy run
```
5. To output a DNS zone file from which you can transfer DNS records
to your DNS provider:
```
cmdeploy dns
scripts/cmdeploy dns
```
6. To check status of your remotely running chatmail service:
```
cmdeploy status
scripts/cmdeploy status
```
7. To test your chatmail service:
```
cmdeploy test
scripts/cmdeploy test
```
8. To benchmark your chatmail service:
```
cmdeploy bench
scripts/cmdeploy bench
```
### Refining the web pages
```
cmdeploy webdev
scripts/cmdeploy webdev
```
This starts a local live development cycle for chatmail Web pages:

View File

@@ -11,9 +11,15 @@ class Config:
self._inipath = inipath
self.mail_domain = params["mail_domain"]
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.postfix_reinject_port = int(params["postfix_reinject_port"])
self.passthrough_recipients = params["passthrough_recipients"].split()
self.privacy_postal = params.get("privacy_postal")
self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo")

View File

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

View File

@@ -2,7 +2,7 @@
Description=Chatmail dict authentication proxy for dovecot
[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
RestartSec=30

View File

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

View File

@@ -7,18 +7,48 @@ mail_domain = {mail_domain}
# 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
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
passthrough_recipients =
#
# Deployment Details
#
# where the filtermail SMTP service listens
filtermail_smtp_port = 10080
# postfix accepts on the localhost reinject SMTP port
postfix_reinject_port = 10025
#
# Privacy Policy
#
# postal address of privacy contact
privacy_postal =

View File

@@ -1,23 +1,25 @@
#!/usr/bin/python3
#!/usr/local/lib/chatmaild/venv/bin/python3
""" CGI script for creating new accounts. """
import json
import random
mail_domain_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"
user = "".join(random.choices(alphanumeric, k=9))
password = "".join(random.choices(alphanumeric, k=12))
return dict(email=f"{user}@{domain}", password=f"{password}")
user = "".join(random.choices(alphanumeric, k=config.username_min_length))
password = "".join(random.choices(alphanumeric, k=config.password_min_length + 3))
return dict(email=f"{user}@{config.mail_domain}", password=f"{password}")
def print_new_account():
domain = open(mail_domain_path).read().strip()
creds = create_newemail_dict(domain=domain)
config = read_config(CONFIG_PATH)
creds = create_newemail_dict(config)
print("Content-Type: application/json")
print("")

View File

@@ -1,17 +1,16 @@
from chatmaild.config import read_config
def test_read_config_basic(make_config):
config = make_config("chat.example.org")
assert config.mail_domain == "chat.example.org"
assert not config.privacy_supervisor and not config.privacy_mail
assert not config.privacy_pdo and not config.privacy_postal
def test_read_config_basic(example_config):
assert example_config.mail_domain == "chat.example.org"
assert not example_config.privacy_supervisor and not example_config.privacy_mail
assert not example_config.privacy_pdo and not example_config.privacy_postal
inipath = config._inipath
inipath = example_config._inipath
inipath.write_text(inipath.read_text().replace("60", "37"))
config = read_config(inipath)
assert config.max_user_send_per_minute == 37
assert config.mail_domain == "chat.example.org"
example_config = read_config(inipath)
assert example_config.max_user_send_per_minute == 37
assert example_config.mail_domain == "chat.example.org"
def test_read_config_testrun(make_config):
@@ -24,4 +23,10 @@ def test_read_config_testrun(make_config):
assert config.filtermail_smtp_port == 10080
assert config.postfix_reinject_port == 10025
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
def test_basic(db):
lookup_passdb(db, "link2xt@c1.testrun.org", "Pieg9aeToe3eghuthe5u")
data = get_user_data(db, "link2xt@c1.testrun.org")
def test_basic(db, example_config):
lookup_passdb(db, example_config, "asdf12345@chat.example.org", "q9mr3faue")
data = get_user_data(db, "asdf12345@chat.example.org")
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
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"""
res = lookup_passdb(db, "newuser1@something.org", "kajdlkajsldk12l3kj1983")
res = lookup_passdb(
db, example_config, "newuser12@chat.example.org", "kajdlkajsldk12l3kj1983"
)
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.
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.write("")
monkeypatch.setattr(chatmaild.doveauth, "NOCREATE_FILE", str(p))
lookup_passdb(db, "newuser1@something.org", "zequ0Aimuchoodaechik")
assert not get_user_data(db, "newuser1@something.org")
lookup_passdb(
db, example_config, "newuser12@chat.example.org", "zequ0Aimuchoodaechik"
)
assert not get_user_data(db, "newuser12@chat.example.org")
def test_db_version(db):
@@ -45,21 +51,21 @@ def test_too_high_db_version(db):
db.ensure_tables()
def test_handle_dovecot_request(db):
def test_handle_dovecot_request(db, example_config):
msg = (
"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[0] == "O" and res.endswith("\n")
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["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
req_per_thread = 5
results = queue.Queue()
@@ -68,7 +74,7 @@ def test_50_concurrent_lookups_different_accounts(db, gencreds):
for i in range(req_per_thread):
addr, password = gencreds()
try:
lookup_passdb(db, addr, password)
lookup_passdb(db, example_config, addr, password)
except Exception:
results.put(traceback.format_exc())
else:

View File

@@ -127,3 +127,19 @@ def test_excempt_privacy(maildata, gencreds, handler):
content = msg.as_bytes()
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
def test_create_newemail_dict():
ac1 = create_newemail_dict(domain="example.org")
def test_create_newemail_dict(example_config):
ac1 = create_newemail_dict(example_config)
assert "@" in ac1["email"]
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["password"] != ac2["password"]
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir):
p = tmpdir.join("mailname")
p.write(maildomain)
monkeypatch.setattr(chatmaild.newemail, "mail_domain_path", str(p))
def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_config):
monkeypatch.setattr(chatmaild.newemail, "CONFIG_PATH", str(example_config._inipath))
print_new_account()
out, err = capsys.readouterr()
lines = out.split("\n")
assert lines[0] == "Content-Type: application/json"
assert not lines[1]
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

View File

@@ -243,7 +243,7 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
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."""
need_restart = False
@@ -253,7 +253,7 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
user="root",
group="root",
mode="644",
config={"hostname": mail_server},
config=config,
debug=debug,
)
need_restart |= main_config.changed
@@ -266,14 +266,13 @@ def _configure_dovecot(mail_server: str, debug: bool = False) -> bool:
)
need_restart |= auth_config.changed
files.put(
src=importlib.resources.files(__package__)
.joinpath("dovecot/expunge.cron")
.open("rb"),
files.template(
src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"),
dest="/etc/cron.d/expunge",
user="root",
group="root",
mode="644",
config=config,
)
# as per https://doc.dovecot.org/configuration_manual/os/
@@ -379,6 +378,20 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
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(nginx_hook=True, domains=[mail_server, f"mta-sts.{mail_server}"])
@@ -423,7 +436,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
_install_remote_venv_with_chatmaild(config)
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)
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
mta_sts_need_restart = _install_mta_sts_daemon()

View File

@@ -110,6 +110,15 @@ def status_cmd(args, out):
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):
"""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")
pytest_path = shutil.which("pytest")
ret = out.run_ret(
[pytest_path, "cmdeploy/src/", "-n4", "-rs", "-x", "-vrx", "--durations=5"]
)
pytest_args = [
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
@@ -145,7 +163,7 @@ def fmt_cmd_options(parser):
def fmt_cmd(args, out):
"""Run formattting fixes (fuff and black) on all chatmail source code."""
"""Run formattting fixes (ruff and black) on all chatmail source code."""
sources = [str(importlib.resources.files(x)) for x in ("chatmaild", "cmdeploy")]
black_args = [shutil.which("black")]

View File

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

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

@@ -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

@@ -39,22 +39,22 @@ class TestDC:
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
def ping_pong():
def dc_ping_pong():
chat.send_text("ping")
msg = ac2.wait_next_incoming_message()
msg.chat.send_text("pong")
ac1.wait_next_incoming_message()
benchmark(ping_pong, 5)
benchmark(dc_ping_pong, 5)
def test_send_10_receive_10(self, benchmark, cmfactory, lp):
ac1, ac2 = cmfactory.get_online_accounts(2)
chat = cmfactory.get_accepted_chat(ac1, ac2)
def send_10_receive_10():
def dc_send_10_receive_10():
for i in range(10):
chat.send_text(f"hello {i}")
for i in range(10):
ac2.wait_next_incoming_message()
benchmark(send_10_receive_10, 5)
benchmark(dc_send_10_receive_10, 5)

View File

@@ -1,6 +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
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."""
user1, user2 = cmsetup.gen_users(2)
mail = maildata(
"encrypted.eml", from_addr=user1.addr, to_addr=user2.addr
).as_string()
for i in range(100):
for i in range(chatmail_config.max_user_send_per_minute + 5):
print("Sending mail", str(i))
try:
user1.smtp.sendmail(user1.addr, [user2.addr], mail)
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}")
outcome = e.recipients[user2.addr]
assert outcome[0] == 450

View File

@@ -1,4 +1,5 @@
import time
import re
import random
import pytest
@@ -20,14 +21,24 @@ class TestEndToEndDeltaChat:
assert msg2.text == "message0"
@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
before quota is exceeded, and thus depends on the speed of the upload.
"""
ac1, ac2 = cmfactory.get_online_accounts(2)
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
num_to_send = quota // attachsize + 2
lp.sec(f"ac1: send {num_to_send} large files to ac2")

View File

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

View File

@@ -36,6 +36,53 @@ def build_webpages(src_dir, build_dir, config):
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):
mail_domain = config.mail_domain
assert src_dir.exists(), src_dir
@@ -48,6 +95,18 @@ def _build_webpages(src_dir, build_dir, config):
for path in src_dir.iterdir():
if path.suffix == ".md":
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")
# recursive jinja2 rendering

6
scripts/cmdeploy Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
#
# Wrapper for cmdelpoy to run it in activated virtualenv.
set -e
. venv/bin/activate
cmdeploy "$@"

View File

@@ -4,6 +4,3 @@ python3 -m venv venv
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

@@ -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,
@@ -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)
<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
@@ -9,10 +9,21 @@ In the Delta Chat account setup
you may tap `LOG INTO YOUR E-MAIL ACCOUNT`
and fill the two fields like this:
- `Address`: invent a word with *exactly* nine characters
and append `@{{config.mail_domain}}` to it.
- `Address`: invent a word with
{% 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.
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)
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?

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

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

@@ -0,0 +1,74 @@
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif;
line-height: 1.4;
font-size: 1.2em;
max-width: 800px;
margin: 20px auto;
padding: 0 10px;
color: #363636;
background: #fff;
}
h1 {
font-size: 2.2em;
margin-top: 0;
}
h1, h2, h3, h4, h5, h6 {
color: #000;
margin-bottom: 12px;
margin-top: 24px;
font-weight: 600;
}
a {
text-decoration: none;
color: #0076d1;
}
a:hover {
text-decoration: underline;
}
img, video {
max-width: 100%;
height: auto;
}
code {
background: #efefef;
padding: 2.5px 5px;
border-radius: 6px;
}
#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

@@ -5,16 +5,23 @@
{% if config.webdev %}
<meta http-equiv="refresh" content="3">
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>{{ config.mail_domain }} {{ pagename }}</title>
<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>
<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 }}
<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>
</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 }}

File diff suppressed because it is too large Load Diff