mirror of
https://github.com/chatmail/relay.git
synced 2026-05-14 01:44:38 +00:00
Compare commits
2 Commits
fix_metric
...
tweak-read
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bfbff4400 | ||
|
|
0a42fd1a9f |
18
README.md
18
README.md
@@ -15,8 +15,8 @@ after which the initially specified password is required for using them.
|
|||||||
|
|
||||||
## Deploying your own chatmail server
|
## Deploying your own chatmail server
|
||||||
|
|
||||||
We subsequently use `CHATMAIL_DOMAIN` as a placeholder for your fully qualified
|
We use `chat.example.org` as the chatmail domain in the following steps.
|
||||||
DNS domain name (FQDN), for example `chat.example.org`.
|
Please substitute it with your own domain.
|
||||||
|
|
||||||
1. Install the `cmdeploy` command in a virtualenv
|
1. Install the `cmdeploy` command in a virtualenv
|
||||||
|
|
||||||
@@ -25,19 +25,19 @@ DNS domain name (FQDN), for example `chat.example.org`.
|
|||||||
cd chatmail
|
cd chatmail
|
||||||
scripts/initenv.sh
|
scripts/initenv.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create chatmail configuration file `chatmail.ini`:
|
2. Create chatmail configuration file `chatmail.ini`:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy init CHATMAIL_DOMAIN
|
scripts/cmdeploy init chat.example.org # <-- use your domain
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Setup first DNS records for your `CHATMAIL_DOMAIN`,
|
3. Setup first DNS records for your chatmail domain,
|
||||||
according to the hints provided by `cmdeploy init`.
|
according to the hints provided by `cmdeploy init`.
|
||||||
Verify that SSH root login works:
|
Verify that SSH root login works:
|
||||||
|
|
||||||
```
|
```
|
||||||
ssh root@CHATMAIL_DOMAIN
|
ssh root@chat.example.org # <-- use your domain
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Deploy to the remote chatmail server:
|
4. Deploy to the remote chatmail server:
|
||||||
@@ -75,7 +75,7 @@ scripts/cmdeploy bench
|
|||||||
|
|
||||||
## Overview of this repository
|
## Overview of this repository
|
||||||
|
|
||||||
This repository drives the development of "chatmail instances",
|
This repository drives the development of chatmail services,
|
||||||
comprised of minimal setups of
|
comprised of minimal setups of
|
||||||
|
|
||||||
- [postfix smtp server](https://www.postfix.org)
|
- [postfix smtp server](https://www.postfix.org)
|
||||||
@@ -91,7 +91,7 @@ as well as custom services that are integrated with these two:
|
|||||||
to send mails for them.
|
to send mails for them.
|
||||||
|
|
||||||
- `chatmaild/src/chatmaild/filtermail.py` prevents
|
- `chatmaild/src/chatmaild/filtermail.py` prevents
|
||||||
unencrypted e-mail from leaving the chatmail instance
|
unencrypted e-mail from leaving the chatmail service
|
||||||
and is integrated into postfix's outbound mail pipelines.
|
and is integrated into postfix's outbound mail pipelines.
|
||||||
|
|
||||||
There is also the `cmdeploy/src/cmdeploy/cmdeploy.py` command line tool
|
There is also the `cmdeploy/src/cmdeploy/cmdeploy.py` command line tool
|
||||||
@@ -104,7 +104,7 @@ to automatically install all chatmail components on a server.
|
|||||||
### Home page and getting started for users
|
### Home page and getting started for users
|
||||||
|
|
||||||
`cmdeploy run` also creates default static Web pages and deploys them
|
`cmdeploy run` also creates default static Web pages and deploys them
|
||||||
to an nginx web server under `https://CHATMAIL_DOMAIN`.
|
to a nginx web server with:
|
||||||
|
|
||||||
- a default `index.html` along with a QR code that users can click to
|
- a default `index.html` along with a QR code that users can click to
|
||||||
create accounts on your chatmail provider,
|
create accounts on your chatmail provider,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ where = ['src']
|
|||||||
doveauth = "chatmaild.doveauth:main"
|
doveauth = "chatmaild.doveauth:main"
|
||||||
filtermail = "chatmaild.filtermail:main"
|
filtermail = "chatmaild.filtermail:main"
|
||||||
echobot = "chatmaild.echo:main"
|
echobot = "chatmaild.echo:main"
|
||||||
chatmail-metrics = "chatmaild.metrics:main"
|
|
||||||
|
|
||||||
[project.entry-points.pytest11]
|
[project.entry-points.pytest11]
|
||||||
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ max_user_send_per_minute = 60
|
|||||||
# maximum mailbox size of a chatmail account
|
# maximum mailbox size of a chatmail account
|
||||||
max_mailbox_size = 100M
|
max_mailbox_size = 100M
|
||||||
|
|
||||||
# days after which mails are unconditionally deleted
|
# time after which seen mails are deleted
|
||||||
delete_mails_after = 40
|
delete_mails_after = 40d
|
||||||
|
|
||||||
# minimum length a username must have
|
# minimum length a username must have
|
||||||
username_min_length = 9
|
username_min_length = 9
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -24,7 +24,7 @@ def test_read_config_testrun(make_config):
|
|||||||
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.max_mailbox_size == "100M"
|
||||||
assert config.delete_mails_after == "40"
|
assert config.delete_mails_after == "40d"
|
||||||
assert config.username_min_length == 9
|
assert config.username_min_length == 9
|
||||||
assert config.username_max_length == 9
|
assert config.username_max_length == 9
|
||||||
assert config.password_min_length == 9
|
assert config.password_min_length == 9
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -84,18 +84,6 @@ 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
|
# install systemd units
|
||||||
for fn in (
|
for fn in (
|
||||||
"doveauth",
|
"doveauth",
|
||||||
@@ -126,7 +114,7 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
|
def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
|
||||||
"""Configures OpenDKIM"""
|
"""Configures OpenDKIM"""
|
||||||
need_restart = False
|
need_restart = False
|
||||||
|
|
||||||
@@ -364,22 +352,20 @@ def check_config(config):
|
|||||||
if mail_domain != "testrun.org" and not mail_domain.endswith(".testrun.org"):
|
if mail_domain != "testrun.org" and not mail_domain.endswith(".testrun.org"):
|
||||||
blocked_words = "merlinux schmieder testrun.org".split()
|
blocked_words = "merlinux schmieder testrun.org".split()
|
||||||
for value in config.__dict__.values():
|
for value in config.__dict__.values():
|
||||||
if any(x in str(value) for x in blocked_words):
|
if any(x in value for x in blocked_words):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"please set your own privacy contacts/addresses in {config._inipath}"
|
f"please set your own privacy contacts/addresses in {config._inipath}"
|
||||||
)
|
)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def deploy_chatmail(config_path: Path) -> None:
|
def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> None:
|
||||||
"""Deploy a chat-mail instance.
|
"""Deploy a chat-mail instance.
|
||||||
|
|
||||||
:param config_path: path to chatmail.ini
|
: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:
|
||||||
"""
|
"""
|
||||||
config = read_config(config_path)
|
|
||||||
check_config(config)
|
|
||||||
mail_domain = config.mail_domain
|
|
||||||
|
|
||||||
from .www import build_webpages
|
from .www import build_webpages
|
||||||
|
|
||||||
apt.update(name="apt update", cache_time=24 * 3600)
|
apt.update(name="apt update", cache_time=24 * 3600)
|
||||||
@@ -409,7 +395,7 @@ def deploy_chatmail(config_path: Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Deploy acmetool to have TLS certificates.
|
# Deploy acmetool to have TLS certificates.
|
||||||
deploy_acmetool(nginx_hook=True, domains=[mail_domain, f"mta-sts.{mail_domain}"])
|
deploy_acmetool(nginx_hook=True, domains=[mail_server, f"mta-sts.{mail_server}"])
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install Postfix",
|
name="Install Postfix",
|
||||||
@@ -439,7 +425,11 @@ def deploy_chatmail(config_path: Path) -> None:
|
|||||||
packages=["fcgiwrap"],
|
packages=["fcgiwrap"],
|
||||||
)
|
)
|
||||||
|
|
||||||
www_path = importlib.resources.files(__package__).joinpath("../../../www").resolve()
|
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()
|
||||||
|
|
||||||
build_dir = www_path.joinpath("build")
|
build_dir = www_path.joinpath("build")
|
||||||
src_dir = www_path.joinpath("src")
|
src_dir = www_path.joinpath("src")
|
||||||
@@ -450,7 +440,7 @@ def deploy_chatmail(config_path: Path) -> None:
|
|||||||
debug = False
|
debug = False
|
||||||
dovecot_need_restart = _configure_dovecot(config, debug=debug)
|
dovecot_need_restart = _configure_dovecot(config, debug=debug)
|
||||||
postfix_need_restart = _configure_postfix(config, debug=debug)
|
postfix_need_restart = _configure_postfix(config, debug=debug)
|
||||||
opendkim_need_restart = _configure_opendkim(mail_domain)
|
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()
|
||||||
nginx_need_restart = _configure_nginx(mail_domain)
|
nginx_need_restart = _configure_nginx(mail_domain)
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ 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_INI"] = args.inipath
|
env["CHATMAIL_DOMAIN"] = args.config.mail_domain
|
||||||
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
deploy_path = "cmdeploy/src/cmdeploy/deploy.py"
|
||||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
||||||
cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}"
|
cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}"
|
||||||
out.check_call(cmd, env=env)
|
out.check_call(cmd, env=env)
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import os
|
import os
|
||||||
import importlib.resources
|
|
||||||
import pyinfra
|
import pyinfra
|
||||||
from cmdeploy import deploy_chatmail
|
from cmdeploy import deploy_chatmail
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
config_path = os.getenv(
|
mail_domain = os.getenv("CHATMAIL_DOMAIN")
|
||||||
"CHATMAIL_INI",
|
mail_server = os.getenv("CHATMAIL_SERVER", mail_domain)
|
||||||
importlib.resources.files("cmdeploy").joinpath("../../../chatmail.ini"),
|
dkim_selector = os.getenv("CHATMAIL_DKIM_SELECTOR", "dkim")
|
||||||
)
|
|
||||||
|
|
||||||
deploy_chatmail(config_path)
|
assert mail_domain
|
||||||
|
assert mail_server
|
||||||
|
assert dkim_selector
|
||||||
|
|
||||||
|
deploy_chatmail(mail_domain, mail_server, dkim_selector)
|
||||||
|
|
||||||
|
|
||||||
if pyinfra.is_cli:
|
if pyinfra.is_cli:
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
# delete all mails after {{ config.delete_mails_after }} days, in the Inbox
|
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} INBOX
|
||||||
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/cur -mtime +{{ config.delete_mails_after }} -type f -delete
|
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} Deltachat
|
||||||
# or in any IMAP subfolder
|
2 0 * * * dovecot doveadm expunge -A SEEN BEFORE {{ config.delete_mails_after }} Trash
|
||||||
2 0 * * * dovecot find /home/vmail/mail/{{ config.mail_domain }}/*/.*/cur -mtime +{{ config.delete_mails_after }} -type f -delete
|
2 30 * * * dovecot doveadm purge -A
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
*/5 * * * * root {{ config.execpath }} /home/vmail/mail/{{ config.mail_domain }} >/var/www/html/metrics
|
|
||||||
@@ -41,10 +41,6 @@ http {
|
|||||||
try_files $uri $uri/ =404;
|
try_files $uri $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /metrics {
|
|
||||||
default_type text/plain;
|
|
||||||
}
|
|
||||||
|
|
||||||
# add cgi-bin support
|
# add cgi-bin support
|
||||||
include /usr/share/doc/fcgiwrap/examples/nginx.conf;
|
include /usr/share/doc/fcgiwrap/examples/nginx.conf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,29 @@ def build_webpages(src_dir, build_dir, config):
|
|||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def timespan_to_english(timespan):
|
||||||
|
val = int(timespan[:-1])
|
||||||
|
c = timespan[-1].lower()
|
||||||
|
match c:
|
||||||
|
case "y":
|
||||||
|
return f"{val} years"
|
||||||
|
case "m":
|
||||||
|
return f"{val} months"
|
||||||
|
case "w":
|
||||||
|
return f"{val} weeks"
|
||||||
|
case "d":
|
||||||
|
return f"{val} days"
|
||||||
|
case "h":
|
||||||
|
return f"{val} hours"
|
||||||
|
case "c":
|
||||||
|
return f"{val} seconds"
|
||||||
|
case _:
|
||||||
|
raise ValueError(
|
||||||
|
c
|
||||||
|
+ " is not a valid time unit. Try [y]ears, [w]eeks, [d]ays, or [h]ours"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def int_to_english(number):
|
def int_to_english(number):
|
||||||
if number >= 0 and number <= 12:
|
if number >= 0 and number <= 12:
|
||||||
a = [
|
a = [
|
||||||
@@ -81,6 +104,9 @@ def _build_webpages(src_dir, build_dir, config):
|
|||||||
render_vars["password_min_length"] = int_to_english(
|
render_vars["password_min_length"] = int_to_english(
|
||||||
config.password_min_length
|
config.password_min_length
|
||||||
)
|
)
|
||||||
|
render_vars["delete_mails_after"] = timespan_to_english(
|
||||||
|
config.delete_mails_after
|
||||||
|
)
|
||||||
target = build_dir.joinpath(path.stem + ".html")
|
target = build_dir.joinpath(path.stem + ".html")
|
||||||
|
|
||||||
# recursive jinja2 rendering
|
# recursive jinja2 rendering
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
|
|
||||||
## More information
|
## More information
|
||||||
|
|
||||||
`nine.testrun.org` provides a low-maintenance, resource efficient and
|
|
||||||
interoperable e-mail service for everyone. What's behind a `chatmail` is
|
|
||||||
effectively a normal e-mail address just like any other but optimized
|
|
||||||
for the usage in chats, especially DeltaChat.
|
|
||||||
|
|
||||||
### Choosing a chatmail address instead of using a random one
|
### Choosing a chatmail address instead of using a random one
|
||||||
|
|
||||||
In the Delta Chat account setup
|
In the Delta Chat account setup
|
||||||
@@ -42,7 +37,7 @@ The first login sets your password.
|
|||||||
|
|
||||||
- You may send up to {{ config.max_user_send_per_minute }} messages per minute.
|
- You may send up to {{ config.max_user_send_per_minute }} messages per minute.
|
||||||
|
|
||||||
- Messages are unconditionally removed {{ config.delete_mails_after }} days after arriving on the server.
|
- Seen messages are removed {{ delete_mails_after }} 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).
|
- 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).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user