mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 00:14:36 +00:00
Compare commits
64 Commits
hpk/increa
...
markdown-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62f028bc67 | ||
|
|
0ed7c360a9 | ||
|
|
af272545dd | ||
|
|
7725a73cf5 | ||
|
|
e65311c0df | ||
|
|
d091b865c7 | ||
|
|
6e28cf9ca1 | ||
|
|
9b6dfa9cdc | ||
|
|
44ab006dca | ||
|
|
c56805211f | ||
|
|
05ec64bf4a | ||
|
|
290e80e795 | ||
|
|
56fab1b071 | ||
|
|
00ab53800e | ||
|
|
fc65072edb | ||
|
|
7bf2dfd62e | ||
|
|
b801838b69 | ||
|
|
abd50e20ed | ||
|
|
d6fb38750a | ||
|
|
3b73457de3 | ||
|
|
ba06a4ff70 | ||
|
|
7fdaffe829 | ||
|
|
73831c74d9 | ||
|
|
d8cbe9d6af | ||
|
|
180ddb8168 | ||
|
|
a1eeea4632 | ||
|
|
a49aa0e655 | ||
|
|
7e81495b51 | ||
|
|
6fde062613 | ||
|
|
84e0376762 | ||
|
|
d690c22c06 | ||
|
|
5410c1bebc | ||
|
|
915bd39dd5 | ||
|
|
2de8b155c2 | ||
|
|
c975aa3bd1 | ||
|
|
6b73f6933a | ||
|
|
3ce350de9e | ||
|
|
1e05974970 | ||
|
|
577c04d537 | ||
|
|
d880937d44 | ||
|
|
46d2334e9c | ||
|
|
0ba94dc613 | ||
|
|
d379feea4f | ||
|
|
e82abee1b9 | ||
|
|
94060ff254 | ||
|
|
1b5cbfbc3d | ||
|
|
f1dcecaa8f | ||
|
|
650338925a | ||
|
|
44f653ccca | ||
|
|
6c686da937 | ||
|
|
387532cfca | ||
|
|
68904f8f61 | ||
|
|
740fe8b146 | ||
|
|
162dc85635 | ||
|
|
b699be3ac8 | ||
|
|
b4122beec4 | ||
|
|
1596b2517c | ||
|
|
1f5b2e947c | ||
|
|
8a59d94105 | ||
|
|
96a1dbac08 | ||
|
|
5215e1dc2b | ||
|
|
624a33a61e | ||
|
|
6bc751213f | ||
|
|
4b721bfcd4 |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Mutual Help Chat Group
|
||||
url: https://i.delta.chat/#C2846EB4C1CB8DF84B1818F5E3A638FC3FBDC981&a=stalebot1%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=d7s1HvOsk5UrSf9AoqRZggg4&s=XmX_9BAW6-g5Ao5E8PyaeKNB
|
||||
url: https://i.delta.chat/#6CBFF8FFD505C0FDEA20A66674F2916EA8FBEE99&a=invitebot%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=X69wTFfvCfs3d-JzqP0kVA3i&s=ibp-447dU-wUq-52QanwAtWc
|
||||
about: If you have troubles setting up the relay server, feel free to ask here.
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -10,6 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Checkout pull request HEAD commit instead of merge commit
|
||||
# Otherwise `test_deployed_state` will be unhappy.
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: run chatmaild tests
|
||||
working-directory: chatmaild
|
||||
|
||||
@@ -70,9 +70,6 @@ jobs:
|
||||
rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true
|
||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true
|
||||
|
||||
- name: run formatting checks
|
||||
run: cmdeploy fmt -v
|
||||
|
||||
- name: run deploy-chatmail offline tests
|
||||
run: pytest --pyargs cmdeploy
|
||||
|
||||
|
||||
3
.github/workflows/test-and-deploy.yaml
vendored
3
.github/workflows/test-and-deploy.yaml
vendored
@@ -70,9 +70,6 @@ jobs:
|
||||
rsync -avz dkimkeys-restore/dkimkeys root@staging2.testrun.org:/etc/ || true
|
||||
ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org chown root:root -R /var/lib/acme || true
|
||||
|
||||
- name: run formatting checks
|
||||
run: cmdeploy fmt -v
|
||||
|
||||
- name: run deploy-chatmail offline tests
|
||||
run: pytest --pyargs cmdeploy
|
||||
|
||||
|
||||
50
ARCHITECTURE.md
Normal file
50
ARCHITECTURE.md
Normal file
@@ -0,0 +1,50 @@
|
||||
This diagram shows components of the chatmail server; this is a draft
|
||||
overview as of mid-August 2025:
|
||||
|
||||
```mermaid
|
||||
graph LR;
|
||||
cmdeploy --- sshd;
|
||||
letsencrypt --- |80|acmetool-redirector;
|
||||
acmetool-redirector --- |443|nginx-right(["`nginx
|
||||
(external)`"]);
|
||||
nginx-external --- |465|postfix;
|
||||
nginx-external(["`nginx
|
||||
(external)`"]) --- |8443|nginx-internal["`nginx
|
||||
(internal)`"];
|
||||
nginx-internal --- website["`Website
|
||||
/var/www/html`"];
|
||||
nginx-internal --- newemail.py;
|
||||
nginx-internal --- autoconfig.xml;
|
||||
certs-nginx[("`TLS certs
|
||||
/var/lib/acme`")] --> nginx-internal;
|
||||
cron --- chatmail-metrics;
|
||||
cron --- acmetool;
|
||||
cron --- expunge;
|
||||
chatmail-metrics --- website;
|
||||
acmetool --> certs[("`TLS certs
|
||||
/var/lib/acme`")];
|
||||
nginx-external --- |993|dovecot;
|
||||
autoconfig.xml --- postfix;
|
||||
autoconfig.xml --- dovecot;
|
||||
postfix --- echobot;
|
||||
postfix --- |10080,10081|filtermail;
|
||||
postfix --- users["`User data
|
||||
home/vmail/mail`"];
|
||||
postfix --- |doveauth.socket|doveauth;
|
||||
dovecot --- |doveauth.socket|doveauth;
|
||||
dovecot --- users;
|
||||
dovecot --- |metadata.socket|chatmail-metadata;
|
||||
doveauth --- users;
|
||||
expunge --- users;
|
||||
chatmail-metadata --- iroh-relay;
|
||||
certs-nginx --> postfix;
|
||||
certs-nginx --> dovecot;
|
||||
style certs fill:#ff6;
|
||||
style certs-nginx fill:#ff6;
|
||||
style nginx-external fill:#fc9;
|
||||
style nginx-right fill:#fc9;
|
||||
```
|
||||
|
||||
The edges in this graph should not be taken too literally; they
|
||||
reflect some sort of communication path or dependency relationship
|
||||
between components of the chatmail server.
|
||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -2,6 +2,48 @@
|
||||
|
||||
## untagged
|
||||
|
||||
- Update iroh-relay to 0.35.0
|
||||
([#650](https://github.com/chatmail/relay/pull/650))
|
||||
|
||||
- Ignore all RCPT TO: parameters
|
||||
([#651](https://github.com/chatmail/relay/pull/651))
|
||||
|
||||
- Use max username length in newemail.py, not min
|
||||
([#648](https://github.com/chatmail/relay/pull/648))
|
||||
|
||||
- Increase maxproc for reinjecting ports from 10 to 100
|
||||
([#646](https://github.com/chatmail/relay/pull/646))
|
||||
|
||||
- Add markdown tabs blocks for rendering multilingual pages.
|
||||
Add russian language support to `index.md`, `privacy.md`, and `info.md`.
|
||||
([#658](https://github.com/chatmail/relay/pull/658))
|
||||
|
||||
- Allow ports 143 and 993 to be used by `dovecot` process
|
||||
([#639](https://github.com/chatmail/relay/pull/639))
|
||||
|
||||
## 1.7.0 2025-09-11
|
||||
|
||||
- Make www upload path configurable
|
||||
([#618](https://github.com/chatmail/relay/pull/618))
|
||||
|
||||
- Check whether GCC is installed in initenv.sh
|
||||
([#608](https://github.com/chatmail/relay/pull/608))
|
||||
|
||||
- Expire push notification tokens after 90 days
|
||||
([#583](https://github.com/chatmail/relay/pull/583))
|
||||
|
||||
- Use official `mtail` binary instead of `mtail` package
|
||||
([#581](https://github.com/chatmail/relay/pull/581))
|
||||
|
||||
- dovecot: install from download.delta.chat instead of openSUSE Build Service
|
||||
([#590](https://github.com/chatmail/relay/pull/590))
|
||||
|
||||
- Reconfigure Dovecot imap-login service to high-performance mode
|
||||
([#578](https://github.com/chatmail/relay/pull/578))
|
||||
|
||||
- Set timezone to improve dovecot performance
|
||||
([#584](https://github.com/chatmail/relay/pull/584))
|
||||
|
||||
- Increase nginx connection limits
|
||||
([#576](https://github.com/chatmail/relay/pull/576))
|
||||
|
||||
@@ -11,6 +53,9 @@
|
||||
- filtermail: respect config message size limit
|
||||
([#572](https://github.com/chatmail/relay/pull/572))
|
||||
|
||||
- Don't deploy if one of the ports used for chatmail relay services is occupied by an unexpected process
|
||||
([#568](https://github.com/chatmail/relay/pull/568))
|
||||
|
||||
- Add config value after how many days large files are deleted
|
||||
([#555](https://github.com/chatmail/relay/pull/555))
|
||||
|
||||
|
||||
30
README.md
30
README.md
@@ -69,7 +69,7 @@ Please substitute it with your own domain.
|
||||
mta-sts.chat.example.com. 3600 IN CNAME chat.example.com.
|
||||
```
|
||||
|
||||
2. Clone the repository and bootstrap the Python virtualenv.
|
||||
2. On your local PC, clone the repository and bootstrap the Python virtualenv.
|
||||
|
||||
```
|
||||
git clone https://github.com/chatmail/relay
|
||||
@@ -77,30 +77,29 @@ Please substitute it with your own domain.
|
||||
scripts/initenv.sh
|
||||
```
|
||||
|
||||
3. Create chatmail configuration file `chatmail.ini`:
|
||||
3. On your local PC, create chatmail configuration file `chatmail.ini`:
|
||||
|
||||
```
|
||||
scripts/cmdeploy init chat.example.org # <-- use your domain
|
||||
```
|
||||
|
||||
4. Verify that SSH root login works:
|
||||
4. Verify that SSH root login to your remote server works:
|
||||
|
||||
```
|
||||
ssh root@chat.example.org # <-- use your domain
|
||||
ssh root@chat.example.org # <-- use your domain
|
||||
```
|
||||
|
||||
|
||||
5. Deploy the remote chatmail relay server:
|
||||
5. From your local PC, deploy the remote chatmail relay server:
|
||||
|
||||
```
|
||||
scripts/cmdeploy run
|
||||
```
|
||||
This script will check that you have all necessary DNS records.
|
||||
This script will also check that you have all necessary DNS records.
|
||||
If DNS records are missing, it will recommend
|
||||
which you should configure at your DNS provider
|
||||
(it can take some time until they are public).
|
||||
|
||||
### Other helpful commands:
|
||||
### Other helpful commands
|
||||
|
||||
To check the status of your remotely running chatmail service:
|
||||
|
||||
@@ -256,6 +255,18 @@ This starts a local live development cycle for chatmail web pages:
|
||||
|
||||
- Starts a browser window automatically where you can "refresh" as needed.
|
||||
|
||||
#### Custom web pages
|
||||
|
||||
You can skip uploading a web page
|
||||
by setting `www_folder=disabled` in `chatmail.ini`.
|
||||
|
||||
If you want to manage your web pages outside this git repository,
|
||||
you can set `www_folder` in `chatmail.ini` to a custom directory on your computer.
|
||||
`cmdeploy run` will upload it as the server's home page,
|
||||
and if it contains a `src/index.md` file,
|
||||
will build it with hugo.
|
||||
|
||||
|
||||
## Mailbox directory layout
|
||||
|
||||
Fresh chatmail addresses have a mailbox directory that contains:
|
||||
@@ -542,3 +553,6 @@ Here are some related projects that you may be interested in:
|
||||
progress](https://github.com/mjl-/mox/issues/251) to modify it to support all
|
||||
of the features and configuration settings required to operate as a chatmail
|
||||
relay.
|
||||
- [Maddy-Chatmail](https://github.com/sadraiiali/maddy_chatmail): a plugin for the
|
||||
[Maddy email server](https://maddy.email/) which aims to implement the
|
||||
chatmail relay features and configuration options.
|
||||
|
||||
@@ -48,6 +48,9 @@ lint.select = [
|
||||
"PLE", # Pylint Error
|
||||
"PLW", # Pylint Warning
|
||||
]
|
||||
lint.ignore = [
|
||||
"PLC0415" # import-outside-top-level
|
||||
]
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
|
||||
@@ -33,6 +33,11 @@ class Config:
|
||||
self.password_min_length = int(params["password_min_length"])
|
||||
self.passthrough_senders = params["passthrough_senders"].split()
|
||||
self.passthrough_recipients = params["passthrough_recipients"].split()
|
||||
self.is_development_instance = (
|
||||
params.get("is_development_instance", "true").lower() == "true"
|
||||
)
|
||||
self.languages = (params.get("languages", "EN").split())
|
||||
self.www_folder = params.get("www_folder", "")
|
||||
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
||||
self.filtermail_smtp_port_incoming = int(
|
||||
params["filtermail_smtp_port_incoming"]
|
||||
|
||||
@@ -197,11 +197,13 @@ class HackedController(Controller):
|
||||
|
||||
class SMTPDiscardRCPTO_options(SMTP):
|
||||
def _getparams(self, params):
|
||||
# aiosmtpd's SMTP daemon fails to handle a request if there are RCPT TO options
|
||||
# We just ignore them for our incoming filtermail purposes
|
||||
if len(params) == 1 and params[0].startswith("ORCPT"):
|
||||
return {}
|
||||
return super()._getparams(params)
|
||||
# Ignore RCPT TO parameters.
|
||||
#
|
||||
# Otherwise parameters such as `ORCPT=...`
|
||||
# or `NOTIFY=DELAY,FAILURE` (generated by Stalwart)
|
||||
# make aiosmtpd reject the message here:
|
||||
# <https://github.com/aio-libs/aiosmtpd/blob/98f578389ae86e5345cc343fa4e5a17b21d9c96d/aiosmtpd/smtp.py#L1379-L1384>
|
||||
return {}
|
||||
|
||||
|
||||
class OutgoingBeforeQueueHandler:
|
||||
|
||||
@@ -49,6 +49,12 @@ passthrough_recipients = xstore@testrun.org echo@{mail_domain}
|
||||
# Deployment Details
|
||||
#
|
||||
|
||||
# A space-separated list of languages to be displayed on the site.
|
||||
# Now available languages: EN RU
|
||||
# You can also use the keyword "ALL"
|
||||
# NOTE: The order of languages affects their order on the page
|
||||
languages = EN
|
||||
|
||||
# SMTP outgoing filtermail and reinjection
|
||||
filtermail_smtp_port = 10080
|
||||
postfix_reinject_port = 10025
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .config import read_config
|
||||
from .dictproxy import DictProxy
|
||||
@@ -7,8 +9,15 @@ from .filedict import FileDict
|
||||
from .notifier import Notifier
|
||||
|
||||
|
||||
def _is_valid_token_timestamp(timestamp, now):
|
||||
# Token if invalid after 90 days
|
||||
# or if the timestamp is in the future.
|
||||
return timestamp > now - 3600 * 24 * 90 and timestamp < now + 60
|
||||
|
||||
|
||||
class Metadata:
|
||||
# each SETMETADATA on this key appends to a list of unique device tokens
|
||||
# each SETMETADATA on this key appends to dictionary
|
||||
# mapping of unique device tokens
|
||||
# which only ever get removed if the upstream indicates the token is invalid
|
||||
DEVICETOKEN_KEY = "devicetoken"
|
||||
|
||||
@@ -18,21 +27,51 @@ class Metadata:
|
||||
def get_metadata_dict(self, addr):
|
||||
return FileDict(self.vmail_dir / addr / "metadata.json")
|
||||
|
||||
def add_token_to_addr(self, addr, token):
|
||||
@contextmanager
|
||||
def _modify_tokens(self, addr):
|
||||
with self.get_metadata_dict(addr).modify() as data:
|
||||
tokens = data.setdefault(self.DEVICETOKEN_KEY, [])
|
||||
if token not in tokens:
|
||||
tokens.append(token)
|
||||
tokens = data.setdefault(self.DEVICETOKEN_KEY, {})
|
||||
now = int(time.time())
|
||||
if isinstance(tokens, list):
|
||||
data[self.DEVICETOKEN_KEY] = tokens = {t: now for t in tokens}
|
||||
|
||||
expired_tokens = [
|
||||
token
|
||||
for token, timestamp in tokens.items()
|
||||
if not _is_valid_token_timestamp(tokens[token], now)
|
||||
]
|
||||
for expired_token in expired_tokens:
|
||||
del tokens[expired_token]
|
||||
|
||||
yield tokens
|
||||
|
||||
def add_token_to_addr(self, addr, token):
|
||||
with self._modify_tokens(addr) as tokens:
|
||||
tokens[token] = int(time.time())
|
||||
|
||||
def remove_token_from_addr(self, addr, token):
|
||||
with self.get_metadata_dict(addr).modify() as data:
|
||||
tokens = data.get(self.DEVICETOKEN_KEY, [])
|
||||
with self._modify_tokens(addr) as tokens:
|
||||
if token in tokens:
|
||||
tokens.remove(token)
|
||||
del tokens[token]
|
||||
|
||||
def get_tokens_for_addr(self, addr):
|
||||
mdict = self.get_metadata_dict(addr).read()
|
||||
return mdict.get(self.DEVICETOKEN_KEY, [])
|
||||
tokens = mdict.get(self.DEVICETOKEN_KEY, {})
|
||||
|
||||
now = int(time.time())
|
||||
if isinstance(tokens, dict):
|
||||
token_list = [
|
||||
token
|
||||
for token, timestamp in tokens.items()
|
||||
if _is_valid_token_timestamp(timestamp, now)
|
||||
]
|
||||
if len(token_list) < len(tokens):
|
||||
# Some tokens have expired, remove them.
|
||||
with self._modify_tokens(addr) as _tokens:
|
||||
pass
|
||||
else:
|
||||
token_list = []
|
||||
return token_list
|
||||
|
||||
|
||||
class MetadataDictProxy(DictProxy):
|
||||
|
||||
@@ -15,7 +15,7 @@ ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
|
||||
|
||||
|
||||
def create_newemail_dict(config: Config):
|
||||
user = "".join(random.choices(ALPHANUMERIC, k=config.username_min_length))
|
||||
user = "".join(random.choices(ALPHANUMERIC, k=config.username_max_length))
|
||||
password = "".join(
|
||||
secrets.choice(ALPHANUMERIC_PUNCT)
|
||||
for _ in range(config.password_min_length + 3)
|
||||
|
||||
@@ -17,11 +17,11 @@ and which are scheduled for retry using exponential back-off timing.
|
||||
If a token notification would be scheduled more than DROP_DEADLINE seconds
|
||||
after its first attempt, it is dropped with a log error.
|
||||
|
||||
Note that tokens are completely opaque to the notification machinery here
|
||||
and will in the future be encrypted foreclosing all ability to distinguish
|
||||
Note that tokens are opaque to the notification machinery here
|
||||
and are encrypted foreclosing all ability to distinguish
|
||||
which device token ultimately goes to which phone-provider notification service,
|
||||
or to understand the relation of "device tokens" and chatmail addresses.
|
||||
The meaning and format of tokens is basically a matter of Delta-Chat Core and
|
||||
The meaning and format of tokens is basically a matter of chatmail Core and
|
||||
the `notification.delta.chat` service.
|
||||
"""
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ dependencies = [
|
||||
"pytest-xdist",
|
||||
"execnet",
|
||||
"imap_tools",
|
||||
"pymdown-extensions",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
@@ -41,3 +42,6 @@ lint.select = [
|
||||
"PLE", # Pylint Error
|
||||
"PLW", # Pylint Warning
|
||||
]
|
||||
lint.ignore = [
|
||||
"PLC0415" # import-outside-top-level
|
||||
]
|
||||
|
||||
@@ -11,9 +11,9 @@ from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
from chatmaild.config import Config, read_config
|
||||
from pyinfra import facts, host
|
||||
from pyinfra import facts, host, logger
|
||||
from pyinfra.api import FactBase
|
||||
from pyinfra.facts.files import File
|
||||
from pyinfra.facts.files import File, Sha256File
|
||||
from pyinfra.facts.server import Sysctl
|
||||
from pyinfra.facts.systemd import SystemdEnabled
|
||||
from pyinfra.operations import apt, files, pip, server, systemd
|
||||
@@ -318,6 +318,40 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
|
||||
return need_restart
|
||||
|
||||
|
||||
def _install_dovecot_package(package: str, arch: str):
|
||||
arch = "amd64" if arch == "x86_64" else arch
|
||||
arch = "arm64" if arch == "aarch64" else arch
|
||||
url = f"https://download.delta.chat/dovecot/dovecot-{package}_2.3.21%2Bdfsg1-3_{arch}.deb"
|
||||
deb_filename = "/root/" + url.split("/")[-1]
|
||||
|
||||
match (package, arch):
|
||||
case ("core", "amd64"):
|
||||
sha256 = "43f593332e22ac7701c62d58b575d2ca409e0f64857a2803be886c22860f5587"
|
||||
case ("core", "arm64"):
|
||||
sha256 = "4d21eba1a83f51c100f08f2e49f0c9f8f52f721ebc34f75018e043306da993a7"
|
||||
case ("imapd", "amd64"):
|
||||
sha256 = "8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86"
|
||||
case ("imapd", "arm64"):
|
||||
sha256 = "178fa877ddd5df9930e8308b518f4b07df10e759050725f8217a0c1fb3fd707f"
|
||||
case ("lmtpd", "amd64"):
|
||||
sha256 = "2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab"
|
||||
case ("lmtpd", "arm64"):
|
||||
sha256 = "89f52fb36524f5877a177dff4a713ba771fd3f91f22ed0af7238d495e143b38f"
|
||||
case _:
|
||||
apt.packages(packages=[f"dovecot-{package}"])
|
||||
return
|
||||
|
||||
files.download(
|
||||
name=f"Download dovecot-{package}",
|
||||
src=url,
|
||||
dest=deb_filename,
|
||||
sha256sum=sha256,
|
||||
cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package
|
||||
)
|
||||
|
||||
apt.deb(name=f"Install dovecot-{package}", src=deb_filename)
|
||||
|
||||
|
||||
def _configure_dovecot(config: Config, debug: bool = False) -> bool:
|
||||
"""Configures Dovecot IMAP server."""
|
||||
need_restart = False
|
||||
@@ -376,6 +410,13 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
|
||||
persist=True,
|
||||
)
|
||||
|
||||
timezone_env = files.line(
|
||||
name="Set TZ environment variable",
|
||||
path="/etc/environment",
|
||||
line="TZ=:/etc/localtime",
|
||||
)
|
||||
need_restart |= timezone_env.changed
|
||||
|
||||
return need_restart
|
||||
|
||||
|
||||
@@ -457,9 +498,26 @@ def check_config(config):
|
||||
|
||||
|
||||
def deploy_mtail(config):
|
||||
apt.packages(
|
||||
name="Install mtail",
|
||||
packages=["mtail"],
|
||||
# Uninstall mtail package, we are going to install a static binary.
|
||||
apt.packages(name="Uninstall mtail", packages=["mtail"], present=False)
|
||||
|
||||
(url, sha256sum) = {
|
||||
"x86_64": (
|
||||
"https://github.com/google/mtail/releases/download/v3.0.8/mtail_3.0.8_linux_amd64.tar.gz",
|
||||
"123c2ee5f48c3eff12ebccee38befd2233d715da736000ccde49e3d5607724e4",
|
||||
),
|
||||
"aarch64": (
|
||||
"https://github.com/google/mtail/releases/download/v3.0.8/mtail_3.0.8_linux_arm64.tar.gz",
|
||||
"aa04811c0929b6754408676de520e050c45dddeb3401881888a092c9aea89cae",
|
||||
),
|
||||
}[host.get_fact(facts.server.Arch)]
|
||||
|
||||
server.shell(
|
||||
name="Download mtail",
|
||||
commands=[
|
||||
f"(echo '{sha256sum} /usr/local/bin/mtail' | sha256sum -c) || (curl -L {url} | gunzip | tar -x -f - mtail -O >/usr/local/bin/mtail.new && mv /usr/local/bin/mtail.new /usr/local/bin/mtail)",
|
||||
"chmod 755 /usr/local/bin/mtail",
|
||||
],
|
||||
)
|
||||
|
||||
# Using our own systemd unit instead of `/usr/lib/systemd/system/mtail.service`.
|
||||
@@ -497,12 +555,12 @@ def deploy_mtail(config):
|
||||
def deploy_iroh_relay(config) -> None:
|
||||
(url, sha256sum) = {
|
||||
"x86_64": (
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-x86_64-unknown-linux-musl.tar.gz",
|
||||
"2ffacf7c0622c26b67a5895ee8e07388769599f60e5f52a3bd40a3258db89b2c",
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-x86_64-unknown-linux-musl.tar.gz",
|
||||
"45c81199dbd70f8c4c30fef7f3b9727ca6e3cea8f2831333eeaf8aa71bf0fac1",
|
||||
),
|
||||
"aarch64": (
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-aarch64-unknown-linux-musl.tar.gz",
|
||||
"b915037bcc1ff1110cc9fcb5de4a17c00ff576fd2f568cd339b3b2d54c420dc4",
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-aarch64-unknown-linux-musl.tar.gz",
|
||||
"f8ef27631fac213b3ef668d02acd5b3e215292746a3fc71d90c63115446008b1",
|
||||
),
|
||||
}[host.get_fact(facts.server.Arch)]
|
||||
|
||||
@@ -511,16 +569,19 @@ def deploy_iroh_relay(config) -> None:
|
||||
packages=["curl"],
|
||||
)
|
||||
|
||||
server.shell(
|
||||
name="Download iroh-relay",
|
||||
commands=[
|
||||
f"(echo '{sha256sum} /usr/local/bin/iroh-relay' | sha256sum -c) || (curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay.new && mv /usr/local/bin/iroh-relay.new /usr/local/bin/iroh-relay)",
|
||||
"chmod 755 /usr/local/bin/iroh-relay",
|
||||
],
|
||||
)
|
||||
|
||||
need_restart = False
|
||||
|
||||
existing_sha256sum = host.get_fact(Sha256File, "/usr/local/bin/iroh-relay")
|
||||
if existing_sha256sum != sha256sum:
|
||||
server.shell(
|
||||
name="Download iroh-relay",
|
||||
commands=[
|
||||
f"(curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay.new && (echo '{sha256sum} /usr/local/bin/iroh-relay.new' | sha256sum -c) && mv /usr/local/bin/iroh-relay.new /usr/local/bin/iroh-relay)",
|
||||
"chmod 755 /usr/local/bin/iroh-relay",
|
||||
],
|
||||
)
|
||||
need_restart = True
|
||||
|
||||
systemd_unit = files.put(
|
||||
name="Upload iroh-relay systemd unit",
|
||||
src=importlib.resources.files(__package__).joinpath("iroh-relay.service"),
|
||||
@@ -560,7 +621,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
check_config(config)
|
||||
mail_domain = config.mail_domain
|
||||
|
||||
from .www import build_webpages
|
||||
from .www import build_webpages, get_paths
|
||||
|
||||
server.group(name="Create vmail group", group="vmail", system=True)
|
||||
server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
|
||||
@@ -595,7 +656,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
path="/etc/apt/sources.list",
|
||||
line="deb [signed-by=/etc/apt/keyrings/obs-home-deltachat.gpg] https://download.opensuse.org/repositories/home:/deltachat/Debian_12/ ./",
|
||||
escape_regex_characters=True,
|
||||
ensure_newline=True,
|
||||
present=False,
|
||||
)
|
||||
|
||||
if host.get_fact(Port, port=53) != "unbound":
|
||||
@@ -617,10 +678,32 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
# to use 127.0.0.1 as the resolver.
|
||||
from cmdeploy.cmdeploy import Out
|
||||
|
||||
process_on_53 = host.get_fact(Port, port=53)
|
||||
if process_on_53 not in (None, "unbound"):
|
||||
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
|
||||
exit(1)
|
||||
port_services = [
|
||||
(["master", "smtpd"], 25),
|
||||
("unbound", 53),
|
||||
("acmetool", 80),
|
||||
(["imap-login", "dovecot"], 143),
|
||||
("nginx", 443),
|
||||
(["master", "smtpd"], 465),
|
||||
(["master", "smtpd"], 587),
|
||||
(["imap-login", "dovecot"], 993),
|
||||
("iroh-relay", 3340),
|
||||
("nginx", 8443),
|
||||
(["master", "smtpd"], config.postfix_reinject_port),
|
||||
(["master", "smtpd"], config.postfix_reinject_port_incoming),
|
||||
("filtermail", config.filtermail_smtp_port),
|
||||
("filtermail", config.filtermail_smtp_port_incoming),
|
||||
]
|
||||
for service, port in port_services:
|
||||
print(f"Checking if port {port} is available for {service}...")
|
||||
running_service = host.get_fact(Port, port=port)
|
||||
if running_service:
|
||||
if running_service not in service:
|
||||
Out().red(
|
||||
f"Deploy failed: port {port} is occupied by: {running_service}"
|
||||
)
|
||||
exit(1)
|
||||
|
||||
apt.packages(
|
||||
name="Install unbound",
|
||||
packages=["unbound", "unbound-anchor", "dnsutils"],
|
||||
@@ -658,10 +741,10 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
packages="postfix",
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
name="Install Dovecot",
|
||||
packages=["dovecot-imapd", "dovecot-lmtpd"],
|
||||
)
|
||||
if not "dovecot.service" in host.get_fact(SystemdEnabled):
|
||||
_install_dovecot_package("core", host.get_fact(facts.server.Arch))
|
||||
_install_dovecot_package("imapd", host.get_fact(facts.server.Arch))
|
||||
_install_dovecot_package("lmtpd", host.get_fact(facts.server.Arch))
|
||||
|
||||
apt.packages(
|
||||
name="Install nginx",
|
||||
@@ -673,12 +756,16 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
packages=["fcgiwrap"],
|
||||
)
|
||||
|
||||
www_path = importlib.resources.files(__package__).joinpath("../../../www").resolve()
|
||||
|
||||
build_dir = www_path.joinpath("build")
|
||||
src_dir = www_path.joinpath("src")
|
||||
build_webpages(src_dir, build_dir, config)
|
||||
files.rsync(f"{build_dir}/", "/var/www/html", flags=["-avz"])
|
||||
www_path, src_dir, build_dir = get_paths(config)
|
||||
# if www_folder was set to a non-existing folder, skip upload
|
||||
if not www_path.is_dir():
|
||||
logger.warning("Building web pages is disabled in chatmail.ini, skipping")
|
||||
else:
|
||||
# if www_folder is a hugo page, build it
|
||||
if build_dir:
|
||||
www_path = build_webpages(src_dir, build_dir, config)
|
||||
# if it is not a hugo page, upload it as is
|
||||
files.rsync(f"{www_path}/", "/var/www/html", flags=["-avz"])
|
||||
|
||||
_install_remote_venv_with_chatmaild(config)
|
||||
debug = False
|
||||
@@ -726,6 +813,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
restarted=nginx_need_restart,
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
name="Restart echobot if postfix and dovecot were just started",
|
||||
service="echobot.service",
|
||||
restarted=postfix_need_restart and dovecot_need_restart,
|
||||
)
|
||||
|
||||
# This file is used by auth proxy.
|
||||
# https://wiki.debian.org/EtcMailName
|
||||
server.shell(
|
||||
@@ -758,8 +851,14 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
name="Ensure cron is installed",
|
||||
packages=["cron"],
|
||||
)
|
||||
git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode()
|
||||
git_diff = subprocess.check_output(["git", "diff"]).decode()
|
||||
try:
|
||||
git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode()
|
||||
except Exception:
|
||||
git_hash = "unknown\n"
|
||||
try:
|
||||
git_diff = subprocess.check_output(["git", "diff"]).decode()
|
||||
except Exception:
|
||||
git_diff = ""
|
||||
files.put(
|
||||
name="Upload chatmail relay git commiit hash",
|
||||
src=StringIO(git_hash + git_diff),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import importlib.resources
|
||||
|
||||
from pyinfra import host
|
||||
from pyinfra.facts.systemd import SystemdStatus
|
||||
from pyinfra.operations import apt, files, server, systemd
|
||||
|
||||
|
||||
@@ -54,12 +52,6 @@ def deploy_acmetool(email="", domains=[]):
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
if host.get_fact(SystemdStatus).get("nginx.service"):
|
||||
systemd.service(
|
||||
name="Stop nginx service to free port 80",
|
||||
service="nginx",
|
||||
running=False,
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
name="Setup acmetool-redirector service",
|
||||
|
||||
@@ -89,6 +89,14 @@ def run_cmd(args, out):
|
||||
try:
|
||||
retcode = out.check_call(cmd, env=env)
|
||||
if retcode == 0:
|
||||
print("\nYou can try out the relay by talking to this echo bot: ")
|
||||
sshexec = SSHExec(args.config.mail_domain, verbose=args.verbose)
|
||||
print(
|
||||
sshexec(
|
||||
call=remote.rshell.shell,
|
||||
kwargs=dict(command="cat /var/lib/echobot/invite-link.txt"),
|
||||
)
|
||||
)
|
||||
out.green("Deploy completed, call `cmdeploy dns` next.")
|
||||
elif not remote_data["acme_account_url"]:
|
||||
out.red("Deploy completed but letsencrypt not configured")
|
||||
|
||||
@@ -177,20 +177,34 @@ service auth-worker {
|
||||
}
|
||||
|
||||
service imap-login {
|
||||
# High-security mode.
|
||||
# Each process serves a single connection and exits afterwards.
|
||||
# This is the default, but we set it explicitly to be sure.
|
||||
# See <https://doc.dovecot.org/admin_manual/login_processes/#high-security-mode> for details.
|
||||
service_count = 1
|
||||
|
||||
# Inrease the number of simultaneous connections.
|
||||
# High-performance mode as described in
|
||||
# <https://doc.dovecot.org/2.3/admin_manual/login_processes/#high-performance-mode>
|
||||
#
|
||||
# As of Dovecot 2.3.19.1 the default is 100 processes.
|
||||
# Combined with `service_count = 1` it means only 100 connections
|
||||
# can be handled simultaneously.
|
||||
process_limit = 10000
|
||||
# So-called high-security mode described in
|
||||
# <https://doc.dovecot.org/2.3/admin_manual/login_processes/#high-security-mode>
|
||||
# and enabled by default with `service_count = 1` starts one process per connection
|
||||
# and has problems logging in thousands of users after Dovecot restart.
|
||||
service_count = 0
|
||||
|
||||
# Increase virtual memory size limit.
|
||||
# Since imap-login processes handle TLS connections
|
||||
# even after logging users in
|
||||
# and many connections are handled by each process,
|
||||
# memory size limit should be increased.
|
||||
#
|
||||
# Otherwise the whole process eventually dies
|
||||
# with an error similar to
|
||||
# imap-login: Fatal: master: service(imap-login):
|
||||
# child 1422951 returned error 83
|
||||
# (Out of memory (service imap-login { vsz_limit=256 MB },
|
||||
# you may need to increase it)
|
||||
# and takes down all its TLS connections at once.
|
||||
vsz_limit = 1G
|
||||
|
||||
# Avoid startup latency for new connections.
|
||||
#
|
||||
# Should be set to at least the number of CPU cores
|
||||
# according to the documentation.
|
||||
process_min_avail = 10
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Description=mtail
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/bin/sh -c "journalctl -f -o short-iso -n 0 | /usr/bin/mtail --address={{ address }} --port={{ port }} --progs /etc/mtail --logtostderr --logs /dev/stdin"
|
||||
ExecStart=/bin/sh -c "journalctl -f -o short-iso -n 0 | /usr/local/bin/mtail --address={{ address }} --port={{ port }} --progs /etc/mtail --logtostderr --logs -"
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -77,13 +77,13 @@ scache unix - - y - 1 scache
|
||||
postlog unix-dgram n - n - 1 postlogd
|
||||
filter unix - n n - - lmtp
|
||||
# Local SMTP server for reinjecting outgoing filtered mail.
|
||||
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
|
||||
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
||||
-o syslog_name=postfix/reinject
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
-o cleanup_service_name=authclean
|
||||
|
||||
# Local SMTP server for reinjecting incoming filtered mail
|
||||
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 10 smtpd
|
||||
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 100 smtpd
|
||||
-o syslog_name=postfix/reinject_incoming
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
import smtplib
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
@@ -64,6 +65,14 @@ class TestSSHExecutor:
|
||||
assert (now - since_date).total_seconds() < 60 * 60 * 51
|
||||
|
||||
|
||||
def test_timezone_env(remote):
|
||||
for line in remote.iter_output("env"):
|
||||
print(line)
|
||||
if line == "tz=:/etc/localtime":
|
||||
return True
|
||||
pytest.fail("TZ is not set")
|
||||
|
||||
|
||||
def test_remote(remote, imap_or_smtp):
|
||||
lineproducer = remote.iter_output(imap_or_smtp.logcmd)
|
||||
imap_or_smtp.connect()
|
||||
@@ -118,14 +127,19 @@ def test_authenticated_from(cmsetup, maildata):
|
||||
|
||||
@pytest.mark.parametrize("from_addr", ["fake@example.org", "fake@testrun.org"])
|
||||
def test_reject_missing_dkim(cmsetup, maildata, from_addr):
|
||||
domain = cmsetup.maildomain
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
try:
|
||||
sock.connect((domain, 25))
|
||||
except socket.timeout:
|
||||
pytest.skip(f"port 25 not reachable for {domain}")
|
||||
|
||||
recipient = cmsetup.gen_users(1)[0]
|
||||
msg = maildata(
|
||||
"encrypted.eml", from_addr=from_addr, to_addr=recipient.addr
|
||||
).as_string()
|
||||
try:
|
||||
conn = smtplib.SMTP(cmsetup.maildomain, 25, timeout=10)
|
||||
except TimeoutError:
|
||||
pytest.skip(f"port 25 not reachable for {cmsetup.maildomain}")
|
||||
conn = smtplib.SMTP(cmsetup.maildomain, 25, timeout=10)
|
||||
|
||||
with conn as s:
|
||||
with pytest.raises(smtplib.SMTPDataError, match="No valid DKIM signature"):
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import importlib
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from cmdeploy.cmdeploy import get_parser, main
|
||||
from cmdeploy.www import get_paths
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -27,3 +29,28 @@ class TestCmdline:
|
||||
assert main(["init", "chat.example.org"]) == 1
|
||||
out, err = capsys.readouterr()
|
||||
assert "path exists" in out.lower()
|
||||
|
||||
|
||||
def test_www_folder(example_config, tmp_path):
|
||||
reporoot = importlib.resources.files(__package__).joinpath("../../../../").resolve()
|
||||
assert not example_config.www_folder
|
||||
www_path, src_dir, build_dir = get_paths(example_config)
|
||||
assert www_path.absolute() == reporoot.joinpath("www").absolute()
|
||||
assert src_dir == reporoot.joinpath("www").joinpath("src")
|
||||
assert build_dir == reporoot.joinpath("www").joinpath("build")
|
||||
example_config.www_folder = "disabled"
|
||||
www_path, _, _ = get_paths(example_config)
|
||||
assert not www_path.is_dir()
|
||||
example_config.www_folder = str(tmp_path)
|
||||
www_path, src_dir, build_dir = get_paths(example_config)
|
||||
assert www_path == tmp_path
|
||||
assert not src_dir.exists()
|
||||
assert not build_dir
|
||||
src_path = tmp_path.joinpath("src")
|
||||
os.mkdir(src_path)
|
||||
with open(src_path / "index.md", "w") as f:
|
||||
f.write("# Test")
|
||||
www_path, src_dir, build_dir = get_paths(example_config)
|
||||
assert www_path == tmp_path
|
||||
assert src_dir == src_path
|
||||
assert build_dir == tmp_path.joinpath("build")
|
||||
|
||||
@@ -3,6 +3,7 @@ import importlib.resources
|
||||
import time
|
||||
import traceback
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from chatmaild.config import read_config
|
||||
@@ -10,6 +11,13 @@ from jinja2 import Template
|
||||
|
||||
from .genqr import gen_qr_png_data
|
||||
|
||||
LANGUAGE_NAMES = {
|
||||
"EN": " 🇬🇧 English",
|
||||
"RU": " 🇷🇺 Русский",
|
||||
# "UA": "Українська",
|
||||
# "FR": "Français",
|
||||
# "DE": "Deutsch",
|
||||
}
|
||||
|
||||
def snapshot_dir_stats(somedir):
|
||||
d = {}
|
||||
@@ -21,18 +29,81 @@ def snapshot_dir_stats(somedir):
|
||||
return d
|
||||
|
||||
|
||||
def prepare_template(source):
|
||||
assert source.exists(), source
|
||||
render_vars = {}
|
||||
render_vars["pagename"] = "home" if source.stem == "index" else source.stem
|
||||
render_vars["markdown_html"] = markdown.markdown(source.read_text())
|
||||
page_layout = source.with_name("page-layout.html").read_text()
|
||||
def prepare_template(source, locales_dir, languages=["EN"]):
|
||||
assert source.exists(), f"Template {source} not found."
|
||||
assert locales_dir.exists(), f"Locales directory {locales_dir} not found."
|
||||
base_name = source.stem
|
||||
render_vars = {
|
||||
"pagename": "home" if base_name == "index" else base_name
|
||||
}
|
||||
|
||||
selected_langs = (
|
||||
sorted([d.name.upper() for d in locales_dir.iterdir() if d.is_dir()])
|
||||
if "ALL" in [l.upper() for l in languages]
|
||||
else [l.upper() for l in languages]
|
||||
)
|
||||
|
||||
markdown_blocks = []
|
||||
|
||||
tabs_enabled = False
|
||||
if len(selected_langs) > 1:
|
||||
tabs_enabled = True
|
||||
|
||||
for lang_code in selected_langs:
|
||||
lang_folder = locales_dir / lang_code
|
||||
lang_file = lang_folder / f"{base_name}.md"
|
||||
lang_name = LANGUAGE_NAMES.get(lang_code, lang_code)
|
||||
|
||||
if lang_file.exists():
|
||||
content = lang_file.read_text().strip()
|
||||
else:
|
||||
print(f"[WARNING]: Missing file {lang_file}. Inserting fallback message.")
|
||||
content = "Content for this language is not available, please contact your server administrator."
|
||||
|
||||
if tabs_enabled:
|
||||
markdown_blocks.append(f"/// tab | {lang_name}\n{content}\n///")
|
||||
continue
|
||||
|
||||
markdown_blocks.append(content)
|
||||
|
||||
if not markdown_blocks:
|
||||
print("[WARNING] No valid language content found. Skipping file.")
|
||||
return None, None
|
||||
|
||||
original_markdown = source.read_text()
|
||||
combined_markdown = original_markdown.replace("%content placeholder%", "\n\n".join(markdown_blocks))
|
||||
|
||||
render_vars["markdown_html"] = markdown.markdown(
|
||||
combined_markdown,
|
||||
extensions=["pymdownx.blocks.tab"]
|
||||
)
|
||||
|
||||
page_layout_path = source.with_name("page-layout.html")
|
||||
assert page_layout_path.exists(), f"Missing template: {page_layout_path}"
|
||||
page_layout = page_layout_path.read_text()
|
||||
|
||||
return render_vars, page_layout
|
||||
|
||||
|
||||
def build_webpages(src_dir, build_dir, config):
|
||||
def get_paths(config) -> (Path, Path, Path):
|
||||
reporoot = importlib.resources.files(__package__).joinpath("../../../").resolve()
|
||||
www_path = Path(config.www_folder)
|
||||
# if www_folder was not set, use default directory
|
||||
if config.www_folder == "":
|
||||
www_path = reporoot.joinpath("www")
|
||||
src_dir = www_path.joinpath("src")
|
||||
# if www_folder is a hugo page, build it
|
||||
if src_dir.joinpath("index.md").is_file():
|
||||
build_dir = www_path.joinpath("build")
|
||||
# if it is not a hugo page, upload it as is
|
||||
else:
|
||||
build_dir = None
|
||||
return www_path, src_dir, build_dir
|
||||
|
||||
|
||||
def build_webpages(src_dir, build_dir, config) -> Path:
|
||||
try:
|
||||
_build_webpages(src_dir, build_dir, config)
|
||||
return _build_webpages(src_dir, build_dir, config)
|
||||
except Exception:
|
||||
print(traceback.format_exc())
|
||||
|
||||
@@ -63,6 +134,7 @@ def int_to_english(number):
|
||||
|
||||
def _build_webpages(src_dir, build_dir, config):
|
||||
mail_domain = config.mail_domain
|
||||
languages = config.languages
|
||||
assert src_dir.exists(), src_dir
|
||||
if not build_dir.exists():
|
||||
build_dir.mkdir()
|
||||
@@ -70,18 +142,19 @@ def _build_webpages(src_dir, build_dir, config):
|
||||
qr_path = build_dir.joinpath(f"qr-chatmail-invite-{mail_domain}.png")
|
||||
qr_path.write_bytes(gen_qr_png_data(mail_domain).read())
|
||||
|
||||
locales_dir = src_dir / "locales"
|
||||
|
||||
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, content = prepare_template(path, locales_dir, languages)
|
||||
|
||||
if render_vars is None:
|
||||
continue
|
||||
|
||||
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)
|
||||
|
||||
target = build_dir.joinpath(path.stem + ".html")
|
||||
|
||||
# recursive jinja2 rendering
|
||||
@@ -93,9 +166,11 @@ def _build_webpages(src_dir, build_dir, config):
|
||||
|
||||
with target.open("w") as f:
|
||||
f.write(content)
|
||||
elif path.name != "page-layout.html":
|
||||
|
||||
elif path.name != "page-layout.html" and path.name != "locales":
|
||||
target = build_dir.joinpath(path.name)
|
||||
target.write_bytes(path.read_bytes())
|
||||
|
||||
return build_dir
|
||||
|
||||
|
||||
@@ -106,15 +181,11 @@ def main():
|
||||
config = read_config(inipath)
|
||||
config.webdev = True
|
||||
assert config.mail_domain
|
||||
www_path = reporoot.joinpath("www")
|
||||
src_path = www_path.joinpath("src")
|
||||
stats = None
|
||||
build_dir = www_path.joinpath("build")
|
||||
src_dir = www_path.joinpath("src")
|
||||
index_path = build_dir.joinpath("index.html")
|
||||
|
||||
# start web page generation, open a browser and wait for changes
|
||||
build_webpages(src_dir, build_dir, config)
|
||||
www_path, src_path, build_dir = get_paths(config)
|
||||
build_dir = build_webpages(src_path, build_dir, config)
|
||||
index_path = build_dir.joinpath("index.html")
|
||||
webbrowser.open(str(index_path))
|
||||
stats = snapshot_dir_stats(src_path)
|
||||
print(f"\nOpened URL: file://{index_path.resolve()}\n")
|
||||
@@ -135,7 +206,7 @@ def main():
|
||||
changenum += 1
|
||||
|
||||
stats = newstats
|
||||
build_webpages(src_dir, build_dir, config)
|
||||
build_webpages(src_path, build_dir, config)
|
||||
print(f"[{changenum}] regenerated web pages at: {index_path}")
|
||||
print(f"URL: file://{index_path.resolve()}\n\n")
|
||||
count = 0
|
||||
|
||||
@@ -9,6 +9,11 @@ if command -v lsb_release 2>&1 >/dev/null; then
|
||||
echo "You need to install python3-dev for installing the other dependencies."
|
||||
exit 1
|
||||
fi
|
||||
if ! gcc --version 2>&1 >/dev/null
|
||||
then
|
||||
echo "You need to install gcc for building Python dependencies."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
|
||||
<img class="banner" src="collage-top.png"/>
|
||||
|
||||
## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
|
||||
%content placeholder%
|
||||
|
||||
{% if config.mail_domain != "nine.testrun.org" %}
|
||||
Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :)
|
||||
{% else %}
|
||||
Welcome to the default onboarding server ({{ config.mail_domain }})
|
||||
for Delta Chat users. For details how it avoids storing personal information
|
||||
please see our [privacy policy](privacy.html).
|
||||
{% endif %}
|
||||
|
||||
<a class="cta-button" href="DCACCOUNT:https://{{ config.mail_domain }}/new">Get a {{config.mail_domain}} chat profile</a>
|
||||
|
||||
If you are viewing this page on a different device
|
||||
without a Delta Chat app,
|
||||
you can also **scan this QR code** with Delta Chat:
|
||||
|
||||
<a href="DCACCOUNT:https://{{ config.mail_domain }}/new">
|
||||
<img width=300 style="float: none;" src="qr-chatmail-invite-{{config.mail_domain}}.png" /></a>
|
||||
|
||||
🐣 **Choose** your Avatar and Name
|
||||
|
||||
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
||||
|
||||
{% if config.mail_domain != "nine.testrun.org" %}
|
||||
{% if config.is_development_instance == True %}
|
||||
<div class="experimental">Note: this is only a temporary development chatmail service</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,43 +1,3 @@
|
||||
<img class="banner" src="collage-info.png"/>
|
||||
|
||||
## More information
|
||||
|
||||
{{ config.mail_domain }} 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.
|
||||
|
||||
|
||||
### Rate and storage limits
|
||||
|
||||
- Un-encrypted messages are blocked to recipients outside
|
||||
{{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 {{ config.max_user_send_per_minute }} messages per minute.
|
||||
|
||||
- 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).
|
||||
|
||||
- Messages are unconditionally removed latest {{ config.delete_mails_after }} days after arriving on the server.
|
||||
Earlier, if storage may exceed otherwise.
|
||||
|
||||
|
||||
### <a name="account-deletion"></a> Account deletion
|
||||
|
||||
If you remove a {{ config.mail_domain }} profile from within the Delta Chat app,
|
||||
then the according account on the server, along with all associated data,
|
||||
is automatically deleted {{ config.delete_inactive_users_after }} days afterwards.
|
||||
|
||||
If you use multiple devices
|
||||
then you need to remove the according chat profile from each device
|
||||
in order for all account data to be removed on the server side.
|
||||
|
||||
If you have any further questions or requests regarding account deletion
|
||||
please send a message from your account to {{ config.privacy_mail }}.
|
||||
|
||||
|
||||
### Who are the operators? Which software is running?
|
||||
|
||||
This chatmail provider is run by a small voluntary group of devs and sysadmins,
|
||||
who [publically develop chatmail provider setups](https://github.com/deltachat/chatmail).
|
||||
Chatmail setups aim to be very low-maintenance, resource efficient and
|
||||
interoperable with any other standards-compliant e-mail service.
|
||||
%content placeholder%
|
||||
@@ -84,3 +84,57 @@ code {
|
||||
color: white !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tabbed-set {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 1em 0;
|
||||
border-radius: 0.1rem;
|
||||
}
|
||||
|
||||
.tabbed-set > input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabbed-set label {
|
||||
width: auto;
|
||||
padding: 0.9375em 1.25em 0.78125em;
|
||||
font-weight: 700;
|
||||
font-size: 0.84em;
|
||||
white-space: nowrap;
|
||||
border-bottom: 0.15rem solid transparent;
|
||||
border-top-left-radius: 0.1rem;
|
||||
border-top-right-radius: 0.1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 250ms, color 250ms;
|
||||
}
|
||||
|
||||
.tabbed-set .tabbed-content {
|
||||
width: 100%;
|
||||
display: none;
|
||||
box-shadow: 0 -.05rem #ddd;
|
||||
}
|
||||
|
||||
.tabbed-set input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tabbed-set input:checked:nth-child(n+1) + label {
|
||||
color: red;
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
.tabbed-set input:nth-child(n+1):checked + label + .tabbed-content {
|
||||
order: 99;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.tabbed-content {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,271 +1,3 @@
|
||||
<img class="banner" src="collage-privacy.png"/>
|
||||
|
||||
# Privacy Policy for {{ config.mail_domain }}
|
||||
|
||||
{% if config.mail_domain == "nine.testrun.org" %}
|
||||
Welcome to `{{config.mail_domain}}`, the default chatmail onboarding server for Delta Chat users.
|
||||
It is operated on the side by a small sysops team
|
||||
on a voluntary basis.
|
||||
See [other chatmail servers](https://delta.chat/en/chatmail) for alternative server operators.
|
||||
{% endif %}
|
||||
|
||||
|
||||
## Summary: No personal data asked or collected
|
||||
|
||||
This chatmail server neither asks for nor retains personal information.
|
||||
Chatmail servers exist to reliably transmit (store and deliver) end-to-end encrypted messages
|
||||
between user's devices running the Delta Chat messenger app.
|
||||
Technically, you may think of a Chatmail server as
|
||||
an end-to-end encrypted "messaging router" at Internet-scale.
|
||||
|
||||
A chatmail server is very unlike classic e-mail servers (for example Google Mail servers)
|
||||
that ask for personal data and permanently store messages.
|
||||
A chatmail server behaves more like the Signal messaging server
|
||||
but does not know about phone numbers and securely and automatically interoperates
|
||||
with other chatmail and classic e-mail servers.
|
||||
|
||||
Unlike classic e-mail servers, this chatmail server
|
||||
|
||||
- unconditionally removes messages after {{ config.delete_mails_after }} days,
|
||||
|
||||
- prohibits sending out un-encrypted messages,
|
||||
|
||||
- does not store Internet addresses ("IP addresses"),
|
||||
|
||||
- does not process IP addresses in relation to email addresses.
|
||||
|
||||
Due to the resulting lack of personal data processing
|
||||
this chatmail server may not require a privacy policy.
|
||||
|
||||
Nevertheless, we provide legal details below to make life easier
|
||||
for data protection specialists and lawyers scrutinizing chatmail operations.
|
||||
|
||||
|
||||
|
||||
## 1. Name and contact information
|
||||
|
||||
Responsible for the processing of your personal data is:
|
||||
```
|
||||
{{ config.privacy_postal }}
|
||||
```
|
||||
|
||||
E-mail: {{ config.privacy_mail }}
|
||||
|
||||
We have appointed a data protection officer:
|
||||
|
||||
```
|
||||
{{ config.privacy_pdo }}
|
||||
```
|
||||
|
||||
## 2. Processing when using chat e-mail services
|
||||
|
||||
We provide services optimized for the use from [Delta Chat](https://delta.chat) apps
|
||||
and process only the data necessary
|
||||
for the setup and technical execution of message delivery.
|
||||
The purpose of the processing is that users can
|
||||
read, write, manage, delete, send, and receive chat messages.
|
||||
For this purpose,
|
||||
we operate server-side software
|
||||
that enables us to send and receive messages.
|
||||
|
||||
We process the following data and details:
|
||||
|
||||
- Outgoing and incoming messages (SMTP) are stored for transit
|
||||
on behalf of their users until the message can be delivered.
|
||||
|
||||
- E-Mail-Messages are stored for the recipient and made accessible via IMAP protocols,
|
||||
until explicitly deleted by the user or until a fixed time period is exceeded,
|
||||
(*usually 4-8 weeks*).
|
||||
|
||||
- IMAP and SMTP protocols are password protected with unique credentials for each account.
|
||||
|
||||
- Users can retrieve or delete all stored messages
|
||||
without intervention from the operators using standard IMAP client tools.
|
||||
|
||||
- Users can connect to a "realtime relay service"
|
||||
to establish Peer-to-Peer connection between user devices,
|
||||
allowing them to send and retrieve ephemeral messages
|
||||
which are never stored on the chatmail server, also not in encrypted form.
|
||||
|
||||
|
||||
### 2.1 Account setup
|
||||
|
||||
Creating an account happens in one of two ways on our mail servers:
|
||||
|
||||
- with a QR invitation token
|
||||
which is scanned using the Delta Chat app
|
||||
and then the account is created.
|
||||
|
||||
- by letting Delta Chat otherwise create an account
|
||||
and register it with a {{ config.mail_domain }} mail server.
|
||||
|
||||
In either case, we process the newly created email address.
|
||||
No phone numbers,
|
||||
other email addresses,
|
||||
or other identifiable data
|
||||
is currently required.
|
||||
The legal basis for the processing is
|
||||
Art. 6 (1) lit. b GDPR,
|
||||
as you have a usage contract with us
|
||||
by using our services.
|
||||
|
||||
### 2.2 Processing of E-Mail-Messages
|
||||
|
||||
In addition,
|
||||
we will process data
|
||||
to keep the server infrastructure operational
|
||||
for purposes of e-mail dispatch
|
||||
and abuse prevention.
|
||||
|
||||
- Therefore,
|
||||
it is necessary to process the content and/or metadata
|
||||
(e.g., headers of the email as well as smtp chatter)
|
||||
of E-Mail-Messages in transit.
|
||||
|
||||
- We will keep logs of messages in transit for a limited time.
|
||||
These logs are used to debug delivery problems and software bugs.
|
||||
|
||||
In addition,
|
||||
we process data to protect the systems from excessive use.
|
||||
Therefore, limits are enforced:
|
||||
|
||||
- rate limits
|
||||
|
||||
- storage limits
|
||||
|
||||
- message size limits
|
||||
|
||||
- any other limit necessary for the whole server to function in a healthy way
|
||||
and to prevent abuse.
|
||||
|
||||
The processing and use of the above permissions
|
||||
are performed to provide the service.
|
||||
The data processing is necessary for the use of our services,
|
||||
therefore the legal basis of the processing is
|
||||
Art. 6 (1) lit. b GDPR,
|
||||
as you have a usage contract with us
|
||||
by using our services.
|
||||
The legal basis for the data processing
|
||||
for the purposes of security and abuse prevention is
|
||||
Art. 6 (1) lit. f GDPR.
|
||||
Our legitimate interest results
|
||||
from the aforementioned purposes.
|
||||
We will not use the collected data
|
||||
for the purpose of drawing conclusions
|
||||
about your person.
|
||||
|
||||
|
||||
## 3. Processing when using our Website
|
||||
|
||||
When you visit our website,
|
||||
the browser used on your end device
|
||||
automatically sends information to the server of our website.
|
||||
This information is temporarily stored in a so-called log file.
|
||||
The following information is collected and stored
|
||||
until it is automatically deleted
|
||||
(*usually 7 days*):
|
||||
|
||||
- used type of browser,
|
||||
|
||||
- used operating system,
|
||||
|
||||
- access date and time as well as
|
||||
|
||||
- country of origin and IP address,
|
||||
|
||||
- the requested file name or HTTP resource,
|
||||
|
||||
- the amount of data transferred,
|
||||
|
||||
- the access status (file transferred, file not found, etc.) and
|
||||
|
||||
- the page from which the file was requested.
|
||||
|
||||
This website is hosted by an external service provider (hoster).
|
||||
The personal data collected on this website is stored
|
||||
on the hoster's servers.
|
||||
Our hoster will process your data
|
||||
only to the extent necessary to fulfill its obligations
|
||||
to perform under our instructions.
|
||||
In order to ensure data protection-compliant processing,
|
||||
we have concluded a data processing agreement with our hoster.
|
||||
|
||||
The aforementioned data is processed by us for the following purposes:
|
||||
|
||||
- Ensuring a reliable connection setup of the website,
|
||||
|
||||
- ensuring a convenient use of our website,
|
||||
|
||||
- checking and ensuring system security and stability, and
|
||||
|
||||
- for other administrative purposes.
|
||||
|
||||
The legal basis for the data processing is
|
||||
Art. 6 (1) lit. f GDPR.
|
||||
Our legitimate interest results
|
||||
from the aforementioned purposes of data collection.
|
||||
We will not use the collected data
|
||||
for the purpose of drawing conclusions about your person.
|
||||
|
||||
## 4. Transfer of Data
|
||||
|
||||
We do not retain any personal data but e-mail messages waiting to be delivered
|
||||
may contain personal data.
|
||||
Any such residual personal data will not be transferred to third parties
|
||||
for purposes other than those listed below:
|
||||
|
||||
a) you have given your express consent
|
||||
in accordance with Art. 6 para. 1 sentence 1 lit. a GDPR,
|
||||
|
||||
b) the disclosure is necessary for the assertion, exercise or defence of legal claims
|
||||
pursuant to Art. 6 (1) sentence 1 lit. f GDPR
|
||||
and there is no reason to assume that you have
|
||||
an overriding interest worthy of protection
|
||||
in the non-disclosure of your data,
|
||||
|
||||
c) in the event that there is a legal obligation to disclose your data
|
||||
pursuant to Art. 6 para. 1 sentence 1 lit. c GDPR,
|
||||
as well as
|
||||
|
||||
d) this is legally permissible and necessary
|
||||
in accordance with Art. 6 Para. 1 S. 1 lit. b GDPR
|
||||
for the processing of contractual relationships with you,
|
||||
|
||||
e) this is carried out by a service provider
|
||||
acting on our behalf and on our exclusive instructions,
|
||||
whom we have carefully selected (Art. 28 (1) GDPR)
|
||||
and with whom we have concluded a corresponding contract on commissioned processing (Art. 28 (3) GDPR),
|
||||
which obliges our contractor,
|
||||
among other things,
|
||||
to implement appropriate security measures
|
||||
and grants us comprehensive control powers.
|
||||
|
||||
## 5. Rights of the data subject
|
||||
|
||||
The rights arise from Articles 12 to 23 GDPR.
|
||||
Since no personal data is stored on our servers,
|
||||
even in encrypted form,
|
||||
there is no need to provide information
|
||||
on these or possible objections.
|
||||
A deletion can be made
|
||||
directly in the Delta Chat email messenger.
|
||||
|
||||
If you have any questions or complaints,
|
||||
please feel free to contact us by email:
|
||||
{{ config.privacy_mail }}
|
||||
|
||||
As a rule, you can contact the supervisory authority of your usual place of residence
|
||||
or workplace
|
||||
or our registered office for this purpose.
|
||||
The supervisory authority responsible for our place of business
|
||||
is the `{{ config.privacy_supervisor }}`.
|
||||
|
||||
|
||||
## 6. Validity of this privacy policy
|
||||
|
||||
This data protection declaration is valid
|
||||
as of *October 2024*.
|
||||
Due to the further development of our service and offers
|
||||
or due to changed legal or official requirements,
|
||||
it may become necessary to revise this data protection declaration from time to time.
|
||||
|
||||
|
||||
%content placeholder%
|
||||
Reference in New Issue
Block a user