Compare commits

..

10 Commits

Author SHA1 Message Date
holger krekel
a994a1b950 count ci accounts correctly 2023-12-16 16:47:46 +01:00
missytake
a1355c10ca fix: check config failed for non-testrun domains 2023-12-15 20:25:58 +01:00
link2xt
92ca3283fd Add metrics 2023-12-14 22:22:10 +00:00
missytake
cea1f3f5f7 dovecot: remove -depth from expunge find commands 2023-12-14 19:11:43 +01:00
missytake
39550d3096 small fixes 2023-12-14 19:11:43 +01:00
missytake
070003b983 dovecot: deleting mails with find instead of doveadm expunge 2023-12-14 19:11:43 +01:00
missytake
049ed79e59 dovecot: unconditionally delete all mails after 40 days 2023-12-14 19:11:43 +01:00
missytake
a9e55e3b25 cmdeploy: get cmdeploy run --config working 2023-12-14 18:50:14 +01:00
Septias
5a178ed235 feat: one more paragraph to explain chatmail
close #126
2023-12-14 16:39:41 +01:00
Floris Bruynooghe
8a338f1320 Use more characters for passwords (#124)
This expands the character set used for passwords generated for new
accounts.  The set it taken from the set used by the pass tool.  The
special characters is the full GNU grep [:punct:] set.
2023-12-14 11:51:22 +01:00
14 changed files with 101 additions and 60 deletions

View File

@@ -22,6 +22,7 @@ where = ['src']
doveauth = "chatmaild.doveauth:main"
filtermail = "chatmaild.filtermail:main"
echobot = "chatmaild.echo:main"
chatmail-metrics = "chatmaild.metrics:main"
[project.entry-points.pytest11]
"chatmaild.testplugin" = "chatmaild.tests.plugin"

View File

@@ -17,8 +17,8 @@ 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
# days after which mails are unconditionally deleted
delete_mails_after = 40
# minimum length a username must have
username_min_length = 9

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
from pathlib import Path
import time
import sys
def main(vmail_dir=None):
if vmail_dir is None:
vmail_dir = sys.argv[1]
accounts = 0
ci_accounts = 0
for path in Path(vmail_dir).iterdir():
accounts += 1
if path.name[:3] in ("ci-", "ac_"):
ci_accounts += 1
timestamp = int(time.time() * 1000)
print(f"accounts {accounts} {timestamp}")
print(f"ci_accounts {ci_accounts} {timestamp}")
if __name__ == "__main__":
main()

View File

@@ -4,16 +4,22 @@
import json
import random
import secrets
import string
from chatmaild.config import read_config, Config
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
ALPHANUMERIC = string.ascii_lowercase + string.digits
ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
def create_newemail_dict(config: Config):
alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890"
user = "".join(random.choices(alphanumeric, k=config.username_min_length))
password = "".join(random.choices(alphanumeric, k=config.password_min_length + 3))
user = "".join(random.choices(ALPHANUMERIC, k=config.username_min_length))
password = "".join(
secrets.choice(ALPHANUMERIC_PUNCT)
for _ in range(config.password_min_length + 3)
)
return dict(email=f"{user}@{config.mail_domain}", password=f"{password}")

View File

@@ -24,7 +24,7 @@ def test_read_config_testrun(make_config):
assert config.postfix_reinject_port == 10025
assert config.max_user_send_per_minute == 60
assert config.max_mailbox_size == "100M"
assert config.delete_mails_after == "40d"
assert config.delete_mails_after == "40"
assert config.username_min_length == 9
assert config.username_max_length == 9
assert config.password_min_length == 9

View File

@@ -0,0 +1,16 @@
from chatmaild.metrics import main
def test_main(tmp_path, capsys):
for x in ("ci-asllkj", "ac_12l3kj", "qweqwe", "ci-l1k2j31l2k3"):
tmp_path.joinpath(x).mkdir()
main(tmp_path)
out, _ = capsys.readouterr()
d = {}
for line in out.split("\n"):
if line.strip():
name, num, _ = line.split()
d[name] = int(num)
assert d["accounts"] == 4
assert d["ci_accounts"] == 3

View File

@@ -84,6 +84,18 @@ def _install_remote_venv_with_chatmaild(config) -> None:
],
)
files.template(
src=importlib.resources.files(__package__).joinpath("metrics.cron.j2"),
dest="/etc/cron.d/chatmail-metrics",
user="root",
group="root",
mode="644",
config={
"mail_domain": config.mail_domain,
"execpath": f"{remote_venv_dir}/bin/chatmail-metrics",
},
)
# install systemd units
for fn in (
"doveauth",
@@ -114,7 +126,7 @@ def _install_remote_venv_with_chatmaild(config) -> None:
)
def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
"""Configures OpenDKIM"""
need_restart = False
@@ -352,20 +364,22 @@ def check_config(config):
if mail_domain != "testrun.org" and not mail_domain.endswith(".testrun.org"):
blocked_words = "merlinux schmieder testrun.org".split()
for value in config.__dict__.values():
if any(x in value for x in blocked_words):
if any(x in str(value) for x in blocked_words):
raise ValueError(
f"please set your own privacy contacts/addresses in {config._inipath}"
)
return config
def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None:
def deploy_chatmail(config_path: Path) -> None:
"""Deploy a chat-mail instance.
:param mail_domain: domain part of your future email addresses
:param mail_server: the DNS name under which your mail server is reachable
:param dkim_selector:
:param config_path: path to chatmail.ini
"""
config = read_config(config_path)
check_config(config)
mail_domain = config.mail_domain
from .www import build_webpages
apt.update(name="apt update", cache_time=24 * 3600)
@@ -395,7 +409,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
)
# Deploy acmetool to have TLS certificates.
deploy_acmetool(nginx_hook=True, domains=[mail_server, f"mta-sts.{mail_server}"])
deploy_acmetool(nginx_hook=True, domains=[mail_domain, f"mta-sts.{mail_domain}"])
apt.packages(
name="Install Postfix",
@@ -425,11 +439,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
packages=["fcgiwrap"],
)
pkg_root = importlib.resources.files(__package__)
chatmail_ini = pkg_root.joinpath("../../../chatmail.ini").resolve()
config = read_config(chatmail_ini)
check_config(config)
www_path = pkg_root.joinpath("../../../www").resolve()
www_path = importlib.resources.files(__package__).joinpath("../../../www").resolve()
build_dir = www_path.joinpath("build")
src_dir = www_path.joinpath("src")
@@ -440,7 +450,7 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
debug = False
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)
opendkim_need_restart = _configure_opendkim(mail_domain)
mta_sts_need_restart = _install_mta_sts_daemon()
nginx_need_restart = _configure_nginx(mail_domain)

View File

@@ -52,8 +52,8 @@ def run_cmd(args, out):
"""Deploy chatmail services on the remote server."""
env = os.environ.copy()
env["CHATMAIL_DOMAIN"] = args.config.mail_domain
deploy_path = "cmdeploy/src/cmdeploy/deploy.py"
env["CHATMAIL_INI"] = args.inipath
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}"
out.check_call(cmd, env=env)

View File

@@ -1,18 +1,16 @@
import os
import importlib.resources
import pyinfra
from cmdeploy import deploy_chatmail
def main():
mail_domain = os.getenv("CHATMAIL_DOMAIN")
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "dkim")
config_path = os.getenv(
"CHATMAIL_INI",
importlib.resources.files("cmdeploy").joinpath("../../../chatmail.ini"),
)
assert mail_domain
assert mail_server
assert dkim_selector
deploy_chatmail(mail_domain, mail_server, dkim_selector)
deploy_chatmail(config_path)
if pyinfra.is_cli:

View File

@@ -1,4 +1,10 @@
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
# delete all mails after {{ config.delete_mails_after }} days, in the Inbox
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/cur -mtime +{{ config.delete_mails_after }} -type f -delete
# or in any IMAP subfolder
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/.*/cur -mtime +{{ config.delete_mails_after }} -type f -delete
# even if they are unseen
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/new -mtime +{{ config.delete_mails_after }} -type f -delete
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/.*/new -mtime +{{ config.delete_mails_after }} -type f -delete
# or only temporary (but then they shouldn't be around after {{ config.delete_mails_after }} days anyway).
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/tmp -mtime +{{ config.delete_mails_after }} -type f -delete
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/.*/tmp -mtime +{{ config.delete_mails_after }} -type f -delete

View File

@@ -0,0 +1 @@
*/5 * * * * root {{ config.execpath }} /home/vmail/mail/{{ config.mail_domain }} >/var/www/html/metrics

View File

@@ -41,6 +41,10 @@ http {
try_files $uri $uri/ =404;
}
location /metrics {
default_type text/plain;
}
# add cgi-bin support
include /usr/share/doc/fcgiwrap/examples/nginx.conf;
}

View File

@@ -36,29 +36,6 @@ 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 = [
@@ -104,9 +81,6 @@ def _build_webpages(src_dir, build_dir, config):
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

View File

@@ -42,7 +42,7 @@ The first login sets your password.
- You may send up to {{ config.max_user_send_per_minute }} messages per minute.
- Seen messages are removed {{ delete_mails_after }} after arriving on the server.
- Messages are unconditionally removed {{ config.delete_mails_after }} days after arriving on the 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).