Compare commits

..

1 Commits

Author SHA1 Message Date
missytake
290ffa13d2 www: add favicon 2023-12-11 18:23:43 +01:00
27 changed files with 111 additions and 265 deletions

View File

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

View File

@@ -11,15 +11,9 @@ class Config:
self._inipath = inipath self._inipath = inipath
self.mail_domain = params["mail_domain"] 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")

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

@@ -7,48 +7,18 @@ 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,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 mail_domain_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(mail_domain_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

@@ -1,16 +1,17 @@
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.mail_domain == "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.mail_domain == "chat.example.org"
def test_read_config_testrun(make_config): def test_read_config_testrun(make_config):
@@ -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, "mail_domain_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

@@ -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/
@@ -422,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

@@ -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, "cmdeploy/src/", "-n4", "-rs", "-x", "-vrx", "--durations=5"]
pytest_args.append("--slow") )
ret = out.run_ret(pytest_args)
return ret return ret

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 +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 +1,6 @@
import requests
from cmdeploy.genqr import gen_qr_png_data from cmdeploy.genqr import gen_qr_png_data
def test_gen_qr_png_data(maildomain): def test_gen_qr_png_data(maildomain):
data = gen_qr_png_data(maildomain) data = gen_qr_png_data(maildomain)
assert data 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, 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")

View File

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

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

View File

@@ -4,3 +4,6 @@ python3 -m venv venv
venv/bin/pip install -e chatmaild venv/bin/pip install -e chatmaild
venv/bin/pip install -e cmdeploy 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 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,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,16 @@
{% 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="icon" href="/logo.svg">
<link rel=”mask-icon” href=”/logo.svg” color=”#000000"> <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 }}