Compare commits

...

52 Commits

Author SHA1 Message Date
missytake
86e545358a www: added privacy_intro config option 2024-05-06 13:32:51 +02:00
link2xt
462e92cca0 Add changelog entry for 281 2024-05-05 21:21:06 +00:00
link2xt
e1b1a945b1 Authenticate echobot by passing /run/echobot/password to doveauth 2024-05-05 15:25:44 +00:00
link2xt
0493e27312 Move echobot into /var/lib/echobot 2024-05-05 15:25:44 +00:00
link2xt
e4f8c78efe Merge pull request #276 from deltachat/acmetool-tos
acmetool: accept new terms of services
2024-05-02 13:29:28 +00:00
missytake
e2cbf4e3e4 changelog for #276 2024-05-02 13:28:42 +00:00
missytake
f35d98bb40 acmetool: enable debugging 2024-05-01 10:45:21 +02:00
missytake
7ce1a5e841 ci: don't fail if /var/lib/acme isn't present 2024-05-01 00:41:11 +02:00
missytake
0a72c2fba7 acmetool: accept new terms of services
closes #275
2024-05-01 00:21:58 +02:00
link2xt
824f70f463 Document email authentication requirements 2024-04-19 21:12:54 +02:00
link2xt
39f5f64998 Reload Dovecot and Postfix when TLS certificate updates (#271) 2024-04-15 14:08:32 +00:00
Christian Hagenest
1752803199 changelog for #270 2024-04-11 19:41:43 +02:00
Christian Hagenest
e372599ce7 change location of changes per nami's recommendation 2024-04-11 19:15:28 +02:00
Christian Hagenest
ce9fb02a75 correct key for obs home deltachat 2024-04-11 19:15:28 +02:00
Christian Hagenest
4526f5e772 apt update after adding new repository 2024-04-11 19:15:28 +02:00
Christian Hagenest
616a42c8f3 add our obs repo to cmdeploy init 2024-04-11 19:15:28 +02:00
holger krekel
ecb5ef8a10 start new untagged section post 1.2.0 2024-04-04 18:30:11 +02:00
holger krekel
824c3dc1d7 prepare tagging 1.2.0 2024-04-04 18:28:35 +02:00
holger krekel
9b76d46558 refinements and fixes 2024-04-04 12:57:49 +02:00
holger krekel
cc4920ddc7 a bit of renaming 2024-04-04 12:57:49 +02:00
holger krekel
2af10175fa ignore and remove .tmp files in notification_dir 2024-04-04 12:57:49 +02:00
holger krekel
ae455fa9e1 avoid float with time, and be safe against crashes during file writing 2024-04-04 12:57:49 +02:00
holger krekel
60d7e516dd implemented suggestion fopr using an absolute deadline instead of retrying but choose 5 hours for now because if our own notification server is down/buggy we have at least a bit of time to fix it 2024-04-04 12:57:49 +02:00
holger krekel
bf18905e02 address typo-level review comments 2024-04-04 12:57:49 +02:00
holger krekel
4d6f520f18 finally use persistent queue items with random file names, simplifying the flows 2024-04-04 12:57:49 +02:00
holger krekel
9da626dfc8 proper doc string for Notifier 2024-04-04 12:57:49 +02:00
holger krekel
1cca9aa441 fix failing CI (uncovering real bug) 2024-04-04 12:57:49 +02:00
holger krekel
3d054847a0 split metadata and notifier into separate files 2024-04-04 12:57:49 +02:00
holger krekel
a31d998e67 separate notification thread into own class, and test start_notification_threads 2024-04-04 12:57:49 +02:00
holger krekel
d313bea97f some more renaming 2024-04-04 12:57:49 +02:00
holger krekel
da04226594 fix 2024-04-04 12:57:49 +02:00
holger krekel
eb2de26638 fix changelog 2024-04-04 12:57:49 +02:00
holger krekel
f5652cdbc4 better naming 2024-04-04 12:57:49 +02:00
holger krekel
13172c92f3 some refinements and extending the tests 2024-04-04 12:57:49 +02:00
holger krekel
09df636183 extend testing 2024-04-04 12:57:49 +02:00
holger krekel
2b45ace3ba refine testing and code 2024-04-04 12:57:49 +02:00
holger krekel
9e05a7d1eb more precision 2024-04-04 12:57:49 +02:00
holger krekel
21e7c09c43 remove redundant test code for requests mocking 2024-04-04 12:57:49 +02:00
holger krekel
14d96e0a9b snap somewhat working again 2024-04-04 12:57:49 +02:00
holger krekel
459ffcabd6 better preserve notification order, using a queue again 2024-04-04 12:57:49 +02:00
missytake
75cc3fdab0 DNS: add changelog entry 2024-04-03 15:12:52 +02:00
missytake
2d26a40c2b DNS: lint 2024-04-03 15:12:52 +02:00
missytake
a78d4e6198 DNS: optimize dnsutils installation command 2024-04-03 15:12:52 +02:00
missytake
2a1e004962 DNS: ensure dig is installed 2024-04-03 15:12:52 +02:00
link2xt
5e55cc205d Run chatmail-metadata and doveauth as vmail 2024-03-30 23:08:42 +01:00
missytake
476c732373 CI: use [] consistently 2024-03-30 21:42:19 +01:00
missytake
71c50b7936 CI: fix local paths (this time\!) 2024-03-30 21:42:19 +01:00
missytake
79cb390f16 CI: fix local paths 2024-03-30 21:42:19 +01:00
missytake
c1452c9c6f CI: fix paths on ns.testrun.org 2024-03-30 21:42:19 +01:00
missytake
6e903d7498 CI: restore ACME & DKIM state from ns.testrun.org 2024-03-30 21:42:19 +01:00
link2xt
221f4a2b0c Apply systemd restrictions to echobot
These options are suggested by
`systemd-analyze security echobot.service`
2024-03-30 14:17:48 +00:00
link2xt
080ae058d8 Remove non-existent file pattern from MANIFEST.in 2024-03-30 09:14:01 +00:00
24 changed files with 611 additions and 231 deletions

View File

@@ -24,8 +24,12 @@ jobs:
echo "${{ secrets.STAGING_SSH_KEY }}" >> ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan staging.testrun.org > ~/.ssh/known_hosts
# save previous acme & dkim state
rsync -avz root@staging.testrun.org:/var/lib/acme . || true
rsync -avz root@staging.testrun.org:/etc/dkimkeys . || true
# store previous acme & dkim state on ns.testrun.org, if it contains useful certs
if [ -f dkimkeys/opendkim.private ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys root@ns.testrun.org:/tmp/ || true; fi
if [ -z "$(ls -A acme/certs)" ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" acme root@ns.testrun.org:/tmp/ || true; fi
- name: rebuild staging.testrun.org to have a clean VPS
run: |
@@ -46,9 +50,13 @@ jobs:
rm ~/.ssh/known_hosts
while ! ssh -o ConnectTimeout=180 -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org id -u ; do sleep 1 ; done
ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org id -u
rsync -avz acme/ root@staging.testrun.org:/var/lib/acme || true
rsync -avz dkimkeys/ root@staging.testrun.org:/etc/dkimkeys || true
ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org chown root:root -R /var/lib/acme
# download acme & dkim state from ns.testrun.org
rsync -e "ssh -o StrictHostKeyChecking=accept-new" -avz root@ns.testrun.org:/tmp/acme acme-restore || true
rsync -avz root@ns.testrun.org:/tmp/dkimkeys dkimkeys-restore || true
# restore acme & dkim state to staging.testrun.org
rsync -avz acme-restore/acme/ root@staging.testrun.org:/var/lib/acme || true
rsync -avz dkimkeys-restore/dkimkeys/ root@staging.testrun.org:/etc/dkimkeys || true
ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org chown root:root -R /var/lib/acme || true
- name: run formatting checks
run: cmdeploy fmt -v
@@ -66,7 +74,7 @@ jobs:
cmdeploy dns --zonefile staging-generated.zone
cat staging-generated.zone >> .github/workflows/staging.testrun.org-default.zone
cat .github/workflows/staging.testrun.org-default.zone
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging.testrun.org.zone
scp .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging.testrun.org.zone
ssh root@ns.testrun.org nsd-checkzone staging.testrun.org /etc/nsd/staging.testrun.org.zone
ssh root@ns.testrun.org systemctl reload nsd

View File

@@ -1,6 +1,36 @@
# Changelog for chatmail deployment
## untagged
## untagged
- Added a config option for an intro to the privacy policy
([#285](https://github.com/deltachat/chatmail/pull/285))
- Move echobot `into /var/lib/echobot`
([#281](https://github.com/deltachat/chatmail/pull/281))
- Accept Let's Encrypt's new Terms of Services
([#275](https://github.com/deltachat/chatmail/pull/276))
- Reload Dovecot and Postfix when TLS certificate updates
([#271](https://github.com/deltachat/chatmail/pull/271))
- Use forked version of dovecot without hardcoded delays
([#270](https://github.com/deltachat/chatmail/pull/270))
## 1.2.0 - 2024-04-04
- Install dig on the server to resolve DNS records
([#267](https://github.com/deltachat/chatmail/pull/267))
- preserve notification order and exponentially backoff with
retries for tokens where we didn't get a successful return
([#265](https://github.com/deltachat/chatmail/pull/263))
- Run chatmail-metadata and doveauth as vmail
([#261](https://github.com/deltachat/chatmail/pull/261))
- Apply systemd restrictions to echobot
([#259](https://github.com/deltachat/chatmail/pull/259))
- re-enable running the CI in pull requests, but not concurrently
([#258](https://github.com/deltachat/chatmail/pull/258))

View File

@@ -159,4 +159,27 @@ While this file is present, account creation will be blocked.
Delta Chat apps will, however, discover all ports and configurations
automatically by reading the [autoconfig XML file](https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html) from the chatmail service.
## Email authentication
chatmail servers rely on [DKIM](https://www.rfc-editor.org/rfc/rfc6376)
to authenticate incoming emails.
Incoming emails must have a valid DKIM signature with
Signing Domain Identifier (SDID, `d=` parameter in the DKIM-Signature header)
equal to the `From:` header domain.
This property is checked by OpenDKIM screen policy script
before validating the signatures.
This correpsonds to strict [DMARC](https://www.rfc-editor.org/rfc/rfc7489) alignment (`adkim=s`),
but chatmail does not rely on DMARC and does not consult the sender policy published in DMARC records.
Other legacy authentication mechanisms such as [iprev](https://www.rfc-editor.org/rfc/rfc8601#section-2.7.3)
and [SPF](https://www.rfc-editor.org/rfc/rfc7208) are also not taken into account.
If there is no valid DKIM signature on the incoming email,
the sender receives a "5.7.1 No valid DKIM signature found" error.
Outgoing emails must be sent over authenticated connection
with envelope MAIL FROM (return path) corresponding to the login.
This is ensured by Postfix which maps login username
to MAIL FROM with
[`smtpd_sender_login_maps`](https://www.postfix.org/postconf.5.html#smtpd_sender_login_maps)
and rejects incorrectly authenticated emails with [`reject_sender_login_mismatch`](reject_sender_login_mismatch) policy.
`From:` header must correspond to envelope MAIL FROM,
this is ensured by `filtermail` proxy.

View File

@@ -1,4 +1,3 @@
include src/chatmaild/*.f
include src/chatmaild/ini/*.ini.f
include src/chatmaild/ini/*.ini
include src/chatmaild/tests/mail-data/*

View File

@@ -24,6 +24,7 @@ class Config:
self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo")
self.privacy_supervisor = params.get("privacy_supervisor")
self.privacy_intro = params.get("privacy_intro")
def _getbytefile(self):
return open(self._inipath, "rb")

View File

@@ -4,12 +4,12 @@ import time
import sys
import json
import crypt
from pathlib import Path
from socketserver import (
UnixStreamServer,
StreamRequestHandler,
ThreadingMixIn,
)
import pwd
from .database import Database
from .config import read_config, Config
@@ -46,23 +46,32 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
return False
localpart, domain = parts
if localpart == "echo":
# echobot account should not be created in the database
return False
if (
len(localpart) > config.username_max_length
or len(localpart) < config.username_min_length
):
if localpart != "echo":
logging.warning(
"localpart %s has to be between %s and %s chars long",
localpart,
config.username_min_length,
config.username_max_length,
)
return False
logging.warning(
"localpart %s has to be between %s and %s chars long",
localpart,
config.username_min_length,
config.username_max_length,
)
return True
def get_user_data(db, config: Config, user):
if user == f"echo@{config.mail_domain}":
return dict(
home=f"/home/vmail/mail/{config.mail_domain}/echo@{config.mail_domain}",
uid="vmail",
gid="vmail",
)
with db.read_connection() as conn:
result = conn.get_user(user)
if result:
@@ -77,6 +86,21 @@ def lookup_userdb(db, config: Config, user):
def lookup_passdb(db, config: Config, user, cleartext_password):
if user == f"echo@{config.mail_domain}":
# Echobot writes password it wants to log in with into /run/echobot/password
try:
password = Path("/run/echobot/password").read_text()
except Exception:
logging.exception("Exception when trying to read /run/echobot/password")
return None
return dict(
home=f"/home/vmail/mail/{config.mail_domain}/echo@{config.mail_domain}",
uid="vmail",
gid="vmail",
password=encrypt_password(password),
)
with db.write_transaction() as conn:
userdata = conn.get_user(user)
if userdata:
@@ -191,9 +215,8 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
def main():
socket = sys.argv[1]
passwd_entry = pwd.getpwnam(sys.argv[2])
db = Database(sys.argv[3])
config = read_config(sys.argv[4])
db = Database(sys.argv[2])
config = read_config(sys.argv[3])
class Handler(StreamRequestHandler):
def handle(self):
@@ -209,7 +232,6 @@ def main():
pass
with ThreadedUnixStreamServer(socket, Handler) as server:
os.chown(socket, uid=passwd_entry.pw_uid, gid=passwd_entry.pw_gid)
try:
server.serve_forever()
except KeyboardInterrupt:

View File

@@ -3,14 +3,17 @@
it will echo back any message that has non-empty text and also supports the /help command.
"""
import logging
import os
import sys
import subprocess
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
from pathlib import Path
from chatmaild.newemail import create_newemail_dict
from chatmaild.config import read_config
from chatmaild.newemail import create_newemail_dict
hooks = events.HookCollection()
@@ -75,9 +78,23 @@ def main():
account = accounts[0] if accounts else deltachat.add_account()
bot = Bot(account, hooks)
config = read_config(sys.argv[1])
# Create password file
if bot.is_configured():
password = bot.account.get_config("mail_pw")
else:
password = create_newemail_dict(config)["password"]
Path("/run/echobot/password").write_text(password)
# Give the user which doveauth runs as access to the password file.
subprocess.run(
["/usr/bin/setfacl", "-m", "user:vmail:r", "/run/echobot/password"],
check=True,
)
if not bot.is_configured():
config = read_config(sys.argv[1])
password = create_newemail_dict(config).get("password")
email = "echo@" + config.mail_domain
bot.configure(email, password)
bot.run_forever()

View File

@@ -61,3 +61,4 @@ privacy_pdo =
# postal address of the privacy supervisor
privacy_supervisor =
privacy_intro =

View File

@@ -14,3 +14,7 @@ privacy_pdo =
privacy_supervisor =
State Commissioner for Data Protection and Freedom of Information of
Baden-Württemberg in 70173 Stuttgart, Germany.
privacy_intro =
This is the default onboarding server for Delta Chat.
If you don't choose to login to an existing email account,
you can get one on this server.

View File

@@ -1,7 +1,4 @@
import pwd
from pathlib import Path
from threading import Thread, Event
from socketserver import (
UnixStreamServer,
StreamRequestHandler,
@@ -10,9 +7,9 @@ from socketserver import (
import sys
import logging
import os
import requests
from .filedict import FileDict
from .notifier import Notifier
DICTPROXY_HELLO_CHAR = "H"
@@ -23,84 +20,49 @@ DICTPROXY_SET_CHAR = "S"
DICTPROXY_COMMIT_TRANSACTION_CHAR = "C"
DICTPROXY_TRANSACTION_CHARS = "BSC"
# each SETMETADATA on this key appends to a list of unique device tokens
# which only ever get removed if the upstream indicates the token is invalid
METADATA_TOKEN_KEY = "devicetoken"
class Metadata:
# each SETMETADATA on this key appends to a list of unique device tokens
# which only ever get removed if the upstream indicates the token is invalid
DEVICETOKEN_KEY = "devicetoken"
class Notifier:
def __init__(self, vmail_dir):
self.vmail_dir = vmail_dir
self.notification_dir = vmail_dir / "pending_notifications"
if not self.notification_dir.exists():
self.notification_dir.mkdir()
self.message_arrived_event = Event()
def get_metadata_dict(self, addr):
return FileDict(self.vmail_dir / addr / "metadata.json")
def add_token(self, addr, token):
def add_token_to_addr(self, addr, token):
with self.get_metadata_dict(addr).modify() as data:
tokens = data.get(METADATA_TOKEN_KEY)
if tokens is None:
data[METADATA_TOKEN_KEY] = [token]
elif token not in tokens:
tokens = data.setdefault(self.DEVICETOKEN_KEY, [])
if token not in tokens:
tokens.append(token)
def remove_token(self, addr, token):
def remove_token_from_addr(self, addr, token):
with self.get_metadata_dict(addr).modify() as data:
tokens = data.get(METADATA_TOKEN_KEY, [])
try:
tokens = data.get(self.DEVICETOKEN_KEY, [])
if token in tokens:
tokens.remove(token)
except ValueError:
pass
def get_tokens(self, addr):
return self.get_metadata_dict(addr).read().get(METADATA_TOKEN_KEY, [])
def new_message_for_addr(self, addr):
self.notification_dir.joinpath(addr).touch()
self.message_arrived_event.set()
def thread_run_loop(self):
requests_session = requests.Session()
while 1:
self.message_arrived_event.wait()
self.message_arrived_event.clear()
self.thread_run_one(requests_session)
def thread_run_one(self, requests_session):
for addr_path in self.notification_dir.iterdir():
addr = addr_path.name
if "@" not in addr:
continue
for token in self.get_tokens(addr):
response = requests_session.post(
"https://notifications.delta.chat/notify",
data=token,
timeout=60,
)
if response.status_code == 410:
# 410 Gone status code
# means the token is no longer valid.
self.remove_token(addr, token)
addr_path.unlink()
def get_tokens_for_addr(self, addr):
mdict = self.get_metadata_dict(addr).read()
return mdict.get(self.DEVICETOKEN_KEY, [])
def handle_dovecot_protocol(rfile, wfile, notifier):
def handle_dovecot_protocol(rfile, wfile, notifier, metadata):
transactions = {}
while True:
msg = rfile.readline().strip().decode()
if not msg:
break
res = handle_dovecot_request(msg, transactions, notifier)
res = handle_dovecot_request(msg, transactions, notifier, metadata)
if res:
wfile.write(res.encode("ascii"))
wfile.flush()
def handle_dovecot_request(msg, transactions, notifier):
def handle_dovecot_request(msg, transactions, notifier, metadata):
# see https://doc.dovecot.org/3.0/developer_manual/design/dict_protocol/
short_command = msg[0]
parts = msg[1:].split("\t")
@@ -110,8 +72,8 @@ def handle_dovecot_request(msg, transactions, notifier):
if keyparts[0] == "priv":
keyname = keyparts[2]
addr = parts[1]
if keyname == METADATA_TOKEN_KEY:
res = " ".join(notifier.get_tokens(addr))
if keyname == metadata.DEVICETOKEN_KEY:
res = " ".join(metadata.get_tokens_for_addr(addr))
return f"O{res}\n"
logging.warning("lookup ignored: %r", msg)
return "N\n"
@@ -144,10 +106,10 @@ def handle_dovecot_request(msg, transactions, notifier):
keyname = parts[1].split("/")
value = parts[2] if len(parts) > 2 else ""
addr = transactions[transaction_id]["addr"]
if keyname[0] == "priv" and keyname[2] == METADATA_TOKEN_KEY:
notifier.add_token(addr, value)
if keyname[0] == "priv" and keyname[2] == metadata.DEVICETOKEN_KEY:
metadata.add_token_to_addr(addr, value)
elif keyname[0] == "priv" and keyname[2] == "messagenew":
notifier.new_message_for_addr(addr)
notifier.new_message_for_addr(addr, metadata)
else:
# Transaction failed.
transactions[transaction_id]["res"] = "F\n"
@@ -158,21 +120,23 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
def main():
socket, username, vmail_dir = sys.argv[1:]
passwd_entry = pwd.getpwnam(username)
socket, vmail_dir = sys.argv[1:]
vmail_dir = Path(vmail_dir)
if not vmail_dir.exists():
logging.error("vmail dir does not exist: %r", vmail_dir)
return 1
notifier = Notifier(vmail_dir)
queue_dir = vmail_dir / "pending_notifications"
queue_dir.mkdir(exist_ok=True)
metadata = Metadata(vmail_dir)
notifier = Notifier(queue_dir)
notifier.start_notification_threads(metadata.remove_token_from_addr)
class Handler(StreamRequestHandler):
def handle(self):
try:
handle_dovecot_protocol(self.rfile, self.wfile, notifier)
handle_dovecot_protocol(self.rfile, self.wfile, notifier, metadata)
except Exception:
logging.exception("Exception in the dovecot dictproxy handler")
raise
@@ -182,17 +146,7 @@ def main():
except FileNotFoundError:
pass
# start notifier thread for signalling new messages to
# Delta Chat notification server
t = Thread(target=notifier.thread_run_loop)
t.setDaemon(True)
t.start()
# let notifier thread run once for any pending notifications from last run
notifier.message_arrived_event.set()
with ThreadedUnixStreamServer(socket, Handler) as server:
os.chown(socket, uid=passwd_entry.pw_uid, gid=passwd_entry.pw_gid)
try:
server.serve_forever()
except KeyboardInterrupt:

View File

@@ -0,0 +1,165 @@
"""
This modules provides notification machinery for transmitting device tokens to
a central notification server which in turn contacts a phone provider's notification server
to trigger Delta Chat apps to retrieve messages and provide instant notifications to users.
The Notifier class arranges the queuing of tokens in separate PriorityQueues
from which NotifyThreads take and transmit them via HTTPS
to the `notifications.delta.chat` service.
The current lack of proper HTTP/2-support in Python leads us
to use multiple threads and connections to the Rust-implemented `notifications.delta.chat`
which itself uses HTTP/2 and thus only a single connection to phone-notification providers.
If a token fails to cause a successful notification
it is moved to a retry-number specific PriorityQueue
which handles all tokens that failed a particular number of times
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
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 `notification.delta.chat` service.
"""
import os
import time
import math
import logging
from uuid import uuid4
from threading import Thread
from pathlib import Path
from queue import PriorityQueue
from dataclasses import dataclass
import requests
@dataclass
class PersistentQueueItem:
path: Path
addr: str
start_ts: int
token: str
def delete(self):
self.path.unlink(missing_ok=True)
@classmethod
def create(cls, queue_dir, addr, start_ts, token):
queue_id = uuid4().hex
start_ts = int(start_ts)
path = queue_dir.joinpath(queue_id)
tmp_path = path.with_name(path.name + ".tmp")
tmp_path.write_text(f"{addr}\n{start_ts}\n{token}")
os.rename(tmp_path, path)
return cls(path, addr, start_ts, token)
@classmethod
def read_from_path(cls, path):
addr, start_ts, token = path.read_text().split("\n", maxsplit=2)
return cls(path, addr, int(start_ts), token)
def __lt__(self, other):
return self.start_ts < other.start_ts
class Notifier:
URL = "https://notifications.delta.chat/notify"
CONNECTION_TIMEOUT = 60.0 # seconds until http-request is given up
BASE_DELAY = 8.0 # base seconds for exponential back-off delay
DROP_DEADLINE = 5 * 60 * 60 # drop notifications after 5 hours
def __init__(self, queue_dir):
self.queue_dir = queue_dir
max_tries = int(math.log(self.DROP_DEADLINE, self.BASE_DELAY)) + 1
self.retry_queues = [PriorityQueue() for _ in range(max_tries)]
def compute_delay(self, retry_num):
return 0 if retry_num == 0 else pow(self.BASE_DELAY, retry_num)
def new_message_for_addr(self, addr, metadata):
start_ts = int(time.time())
for token in metadata.get_tokens_for_addr(addr):
queue_item = PersistentQueueItem.create(
self.queue_dir, addr, start_ts, token
)
self.queue_for_retry(queue_item)
def requeue_persistent_queue_items(self):
for queue_path in self.queue_dir.iterdir():
if queue_path.name.endswith(".tmp"):
logging.warning("removing spurious queue item: %r", queue_path)
queue_path.unlink()
continue
queue_item = PersistentQueueItem.read_from_path(queue_path)
self.queue_for_retry(queue_item)
def queue_for_retry(self, queue_item, retry_num=0):
delay = self.compute_delay(retry_num)
when = int(time.time()) + delay
deadline = queue_item.start_ts + self.DROP_DEADLINE
if retry_num >= len(self.retry_queues) or when > deadline:
queue_item.delete()
logging.error("notification exceeded deadline: %r", queue_item.token)
return
self.retry_queues[retry_num].put((when, queue_item))
def start_notification_threads(self, remove_token_from_addr):
self.requeue_persistent_queue_items()
threads = {}
for retry_num in range(len(self.retry_queues)):
# use 4 threads for first-try tokens and less for subsequent tries
num_threads = 4 if retry_num == 0 else 2
threads[retry_num] = []
for _ in range(num_threads):
thread = NotifyThread(self, retry_num, remove_token_from_addr)
threads[retry_num].append(thread)
thread.start()
return threads
class NotifyThread(Thread):
def __init__(self, notifier, retry_num, remove_token_from_addr):
super().__init__(daemon=True)
self.notifier = notifier
self.retry_num = retry_num
self.remove_token_from_addr = remove_token_from_addr
def stop(self):
self.notifier.retry_queues[self.retry_num].put((None, None))
def run(self):
requests_session = requests.Session()
while self.retry_one(requests_session):
pass
def retry_one(self, requests_session, sleep=time.sleep):
when, queue_item = self.notifier.retry_queues[self.retry_num].get()
if when is None:
return False
wait_time = when - int(time.time())
if wait_time > 0:
sleep(wait_time)
self.perform_request_to_notification_server(requests_session, queue_item)
return True
def perform_request_to_notification_server(self, requests_session, queue_item):
timeout = self.notifier.CONNECTION_TIMEOUT
token = queue_item.token
try:
res = requests_session.post(self.notifier.URL, data=token, timeout=timeout)
except requests.exceptions.RequestException as e:
res = e
else:
if res.status_code in (200, 410):
if res.status_code == 410:
self.remove_token_from_addr(queue_item.addr, token)
queue_item.delete()
return
logging.warning("Notification request failed: %r", res)
self.notifier.queue_for_retry(queue_item, retry_num=self.retry_num + 1)

View File

@@ -1,18 +1,32 @@
import io
import pytest
import requests
import time
from chatmaild.metadata import (
handle_dovecot_request,
handle_dovecot_protocol,
Metadata,
)
from chatmaild.notifier import (
Notifier,
NotifyThread,
PersistentQueueItem,
)
@pytest.fixture
def notifier(tmp_path):
def notifier(metadata):
queue_dir = metadata.vmail_dir.joinpath("pending_notifications")
queue_dir.mkdir()
return Notifier(queue_dir)
@pytest.fixture
def metadata(tmp_path):
vmail_dir = tmp_path.joinpath("vmaildir")
vmail_dir.mkdir()
return Notifier(vmail_dir)
return Metadata(vmail_dir)
@pytest.fixture
@@ -25,72 +39,100 @@ def testaddr2():
return "user2@example.org"
def test_notifier_persistence(tmp_path, testaddr, testaddr2):
notifier1 = Notifier(tmp_path)
notifier2 = Notifier(tmp_path)
assert not notifier1.get_tokens(testaddr)
assert not notifier2.get_tokens(testaddr)
notifier1.add_token(testaddr, "01234")
notifier1.add_token(testaddr2, "456")
assert notifier2.get_tokens(testaddr) == ["01234"]
assert notifier2.get_tokens(testaddr2) == ["456"]
notifier2.remove_token(testaddr, "01234")
assert not notifier1.get_tokens(testaddr)
assert notifier1.get_tokens(testaddr2) == ["456"]
@pytest.fixture
def token():
return "01234"
def test_remove_nonexisting(tmp_path, testaddr):
notifier1 = Notifier(tmp_path)
notifier1.add_token(testaddr, "123")
notifier1.remove_token(testaddr, "1l23k1l2k3")
assert notifier1.get_tokens(testaddr) == ["123"]
def get_mocked_requests(statuslist):
class ReqMock:
requests = []
def post(self, url, data, timeout):
self.requests.append((url, data, timeout))
res = statuslist.pop(0)
if isinstance(res, Exception):
raise res
class Result:
status_code = res
return Result()
return ReqMock()
def test_notifier_delete_without_set(notifier, testaddr):
notifier.remove_token(testaddr, "123")
assert not notifier.get_tokens(testaddr)
def test_metadata_persistence(tmp_path, testaddr, testaddr2):
metadata1 = Metadata(tmp_path)
metadata2 = Metadata(tmp_path)
assert not metadata1.get_tokens_for_addr(testaddr)
assert not metadata2.get_tokens_for_addr(testaddr)
metadata1.add_token_to_addr(testaddr, "01234")
metadata1.add_token_to_addr(testaddr2, "456")
assert metadata2.get_tokens_for_addr(testaddr) == ["01234"]
assert metadata2.get_tokens_for_addr(testaddr2) == ["456"]
metadata2.remove_token_from_addr(testaddr, "01234")
assert not metadata1.get_tokens_for_addr(testaddr)
assert metadata1.get_tokens_for_addr(testaddr2) == ["456"]
def test_handle_dovecot_request_lookup_fails(notifier, testaddr):
res = handle_dovecot_request(f"Lpriv/123/chatmail\t{testaddr}", {}, notifier)
def test_remove_nonexisting(metadata, tmp_path, testaddr):
metadata.add_token_to_addr(testaddr, "123")
metadata.remove_token_from_addr(testaddr, "1l23k1l2k3")
assert metadata.get_tokens_for_addr(testaddr) == ["123"]
def test_notifier_remove_without_set(metadata, testaddr):
metadata.remove_token_from_addr(testaddr, "123")
assert not metadata.get_tokens_for_addr(testaddr)
def test_handle_dovecot_request_lookup_fails(notifier, metadata, testaddr):
res = handle_dovecot_request(
f"Lpriv/123/chatmail\t{testaddr}", {}, notifier, metadata
)
assert res == "N\n"
def test_handle_dovecot_request_happy_path(notifier, testaddr):
def test_handle_dovecot_request_happy_path(notifier, metadata, testaddr, token):
transactions = {}
# set device token in a transaction
tx = "1111"
msg = f"B{tx}\t{testaddr}"
res = handle_dovecot_request(msg, transactions, notifier)
assert not res and not notifier.get_tokens(testaddr)
res = handle_dovecot_request(msg, transactions, notifier, metadata)
assert not res and not metadata.get_tokens_for_addr(testaddr)
assert transactions == {tx: dict(addr=testaddr, res="O\n")}
msg = f"S{tx}\tpriv/guid00/devicetoken\t01234"
res = handle_dovecot_request(msg, transactions, notifier)
msg = f"S{tx}\tpriv/guid00/devicetoken\t{token}"
res = handle_dovecot_request(msg, transactions, notifier, metadata)
assert not res
assert len(transactions) == 1
assert notifier.get_tokens(testaddr) == ["01234"]
assert metadata.get_tokens_for_addr(testaddr) == [token]
msg = f"C{tx}"
res = handle_dovecot_request(msg, transactions, notifier)
res = handle_dovecot_request(msg, transactions, notifier, metadata)
assert res == "O\n"
assert len(transactions) == 0
assert notifier.get_tokens(testaddr) == ["01234"]
assert metadata.get_tokens_for_addr(testaddr) == [token]
# trigger notification for incoming message
tx2 = "2222"
assert handle_dovecot_request(f"B{tx2}\t{testaddr}", transactions, notifier) is None
assert (
handle_dovecot_request(f"B{tx2}\t{testaddr}", transactions, notifier, metadata)
is None
)
msg = f"S{tx2}\tpriv/guid00/messagenew"
assert handle_dovecot_request(msg, transactions, notifier) is None
assert notifier.message_arrived_event.is_set()
assert handle_dovecot_request(f"C{tx2}", transactions, notifier) == "O\n"
assert handle_dovecot_request(msg, transactions, notifier, metadata) is None
queue_item = notifier.retry_queues[0].get()[1]
assert queue_item.token == token
assert handle_dovecot_request(f"C{tx2}", transactions, notifier, metadata) == "O\n"
assert not transactions
assert notifier.notification_dir.joinpath(testaddr).exists()
assert queue_item.path.exists()
def test_handle_dovecot_protocol_set_devicetoken(notifier):
def test_handle_dovecot_protocol_set_devicetoken(metadata, notifier):
rfile = io.BytesIO(
b"\n".join(
[
@@ -102,12 +144,12 @@ def test_handle_dovecot_protocol_set_devicetoken(notifier):
)
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
handle_dovecot_protocol(rfile, wfile, notifier, metadata)
assert wfile.getvalue() == b"O\n"
assert notifier.get_tokens("user@example.org") == ["01234"]
assert metadata.get_tokens_for_addr("user@example.org") == ["01234"]
def test_handle_dovecot_protocol_set_get_devicetoken(notifier):
def test_handle_dovecot_protocol_set_get_devicetoken(metadata, notifier):
rfile = io.BytesIO(
b"\n".join(
[
@@ -119,19 +161,19 @@ def test_handle_dovecot_protocol_set_get_devicetoken(notifier):
)
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
assert notifier.get_tokens("user@example.org") == ["01234"]
handle_dovecot_protocol(rfile, wfile, notifier, metadata)
assert metadata.get_tokens_for_addr("user@example.org") == ["01234"]
assert wfile.getvalue() == b"O\n"
rfile = io.BytesIO(
b"\n".join([b"HELLO", b"Lpriv/0123/devicetoken\tuser@example.org"])
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
handle_dovecot_protocol(rfile, wfile, notifier, metadata)
assert wfile.getvalue() == b"O01234\n"
def test_handle_dovecot_protocol_iterate(notifier):
def test_handle_dovecot_protocol_iterate(metadata, notifier):
rfile = io.BytesIO(
b"\n".join(
[
@@ -141,90 +183,116 @@ def test_handle_dovecot_protocol_iterate(notifier):
)
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
handle_dovecot_protocol(rfile, wfile, notifier, metadata)
assert wfile.getvalue() == b"\n"
def test_handle_dovecot_protocol_messagenew(notifier):
rfile = io.BytesIO(
b"\n".join(
[
b"HELLO",
b"Btx01\tuser@example.org",
b"Stx01\tpriv/guid00/messagenew",
b"Ctx01",
]
)
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
assert wfile.getvalue() == b"O\n"
assert notifier.message_arrived_event.is_set()
assert notifier.notification_dir.joinpath("user@example.org").exists()
def test_notifier_thread_run(notifier, testaddr):
requests = []
class ReqMock:
def post(self, url, data, timeout):
requests.append((url, data, timeout))
class Result:
status_code = 200
return Result()
notifier.add_token(testaddr, "01234")
notifier.new_message_for_addr(testaddr)
notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0]
def test_notifier_thread_deletes_persistent_file(metadata, notifier, testaddr):
reqmock = get_mocked_requests([200])
metadata.add_token_to_addr(testaddr, "01234")
notifier.new_message_for_addr(testaddr, metadata)
NotifyThread(notifier, 0, None).retry_one(reqmock)
url, data, timeout = reqmock.requests[0]
assert data == "01234"
assert notifier.get_tokens(testaddr) == ["01234"]
assert metadata.get_tokens_for_addr(testaddr) == ["01234"]
notifier.requeue_persistent_queue_items()
assert notifier.retry_queues[0].qsize() == 0
def test_multi_device_notifier(notifier, testaddr):
requests = []
@pytest.mark.parametrize("status", [requests.exceptions.RequestException(), 404, 500])
def test_notifier_thread_connection_failures(
metadata, notifier, testaddr, status, caplog
):
"""test that tokens keep getting retried until they are given up."""
metadata.add_token_to_addr(testaddr, "01234")
notifier.new_message_for_addr(testaddr, metadata)
notifier.NOTIFICATION_RETRY_DELAY = 5
max_tries = len(notifier.retry_queues)
for i in range(max_tries):
caplog.clear()
reqmock = get_mocked_requests([status])
sleep_calls = []
NotifyThread(notifier, i, None).retry_one(reqmock, sleep=sleep_calls.append)
assert notifier.retry_queues[i].qsize() == 0
assert "request failed" in caplog.records[0].msg
if i > 0:
assert len(sleep_calls) == 1
if i + 1 < max_tries:
assert notifier.retry_queues[i + 1].qsize() == 1
assert len(caplog.records) == 1
else:
assert len(caplog.records) == 2
assert "deadline" in caplog.records[1].msg
notifier.requeue_persistent_queue_items()
assert notifier.retry_queues[0].qsize() == 0
class ReqMock:
def post(self, url, data, timeout):
requests.append((url, data, timeout))
class Result:
status_code = 200
def test_requeue_removes_tmp_files(notifier, metadata, testaddr, caplog):
metadata.add_token_to_addr(testaddr, "01234")
notifier.new_message_for_addr(testaddr, metadata)
p = notifier.queue_dir.joinpath("1203981203.tmp")
p.touch()
notifier2 = notifier.__class__(notifier.queue_dir)
notifier2.requeue_persistent_queue_items()
assert "spurious" in caplog.records[0].msg
assert not p.exists()
assert notifier2.retry_queues[0].qsize() == 1
when, queue_item = notifier2.retry_queues[0].get()
assert when <= int(time.time())
assert queue_item.addr == testaddr
return Result()
notifier.add_token(testaddr, "01234")
notifier.add_token(testaddr, "56789")
notifier.new_message_for_addr(testaddr)
notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0]
def test_start_and_stop_notification_threads(notifier, testaddr):
threads = notifier.start_notification_threads(None)
for retry_num, threadlist in threads.items():
for t in threadlist:
t.stop()
t.join()
def test_multi_device_notifier(metadata, notifier, testaddr):
metadata.add_token_to_addr(testaddr, "01234")
metadata.add_token_to_addr(testaddr, "56789")
notifier.new_message_for_addr(testaddr, metadata)
reqmock = get_mocked_requests([200, 200])
NotifyThread(notifier, 0, None).retry_one(reqmock)
NotifyThread(notifier, 0, None).retry_one(reqmock)
assert notifier.retry_queues[0].qsize() == 0
assert notifier.retry_queues[1].qsize() == 0
url, data, timeout = reqmock.requests[0]
assert data == "01234"
url, data, timeout = requests[1]
url, data, timeout = reqmock.requests[1]
assert data == "56789"
assert notifier.get_tokens(testaddr) == ["01234", "56789"]
assert metadata.get_tokens_for_addr(testaddr) == ["01234", "56789"]
def test_notifier_thread_run_gone_removes_token(notifier, testaddr):
requests = []
def test_notifier_thread_run_gone_removes_token(metadata, notifier, testaddr):
metadata.add_token_to_addr(testaddr, "01234")
metadata.add_token_to_addr(testaddr, "45678")
notifier.new_message_for_addr(testaddr, metadata)
class ReqMock:
def post(self, url, data, timeout):
requests.append((url, data, timeout))
class Result:
status_code = 410 if data == "01234" else 200
return Result()
notifier.add_token(testaddr, "01234")
notifier.new_message_for_addr(testaddr)
assert notifier.get_tokens(testaddr) == ["01234"]
notifier.add_token(testaddr, "45678")
notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0]
reqmock = get_mocked_requests([410, 200])
NotifyThread(notifier, 0, metadata.remove_token_from_addr).retry_one(reqmock)
NotifyThread(notifier, 0, None).retry_one(reqmock)
url, data, timeout = reqmock.requests[0]
assert data == "01234"
url, data, timeout = requests[1]
url, data, timeout = reqmock.requests[1]
assert data == "45678"
assert notifier.get_tokens(testaddr) == ["45678"]
assert metadata.get_tokens_for_addr(testaddr) == ["45678"]
assert notifier.retry_queues[0].qsize() == 0
assert notifier.retry_queues[1].qsize() == 0
def test_persistent_queue_items(tmp_path, testaddr, token):
queue_item = PersistentQueueItem.create(tmp_path, testaddr, 432, token)
assert queue_item.addr == testaddr
assert queue_item.start_ts == 432
assert queue_item.token == token
item2 = PersistentQueueItem.read_from_path(queue_item.path)
assert item2.addr == testaddr
assert item2.start_ts == 432
assert item2.token == token
assert item2 == queue_item
item2.delete()
assert not item2.path.exists()
assert not queue_item < item2 and not item2 < queue_item

View File

@@ -477,6 +477,29 @@ def deploy_chatmail(config_path: Path) -> None:
groups=["opendkim"],
system=True,
)
server.user(name="Create echobot user", user="echobot", system=True)
server.shell(
name="Fix file owner in /home/vmail",
commands=["test -d /home/vmail && chown -R vmail:vmail /home/vmail"],
)
# Add our OBS repository for dovecot_no_delay
files.put(
name = "Add Deltachat OBS GPG key to apt keyring",
src = importlib.resources.files(__package__).joinpath("obs-home-deltachat.gpg"),
dest = "/etc/apt/keyrings/obs-home-deltachat.gpg",
user="root",
group="root",
mode="644",
)
files.line(
name = "Add DeltaChat OBS home repository to sources.list",
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/ ./",
ensure_newline = True,
)
apt.update(name="apt update", cache_time=24 * 3600)
@@ -508,7 +531,6 @@ def deploy_chatmail(config_path: Path) -> None:
# Deploy acmetool to have TLS certificates.
deploy_acmetool(
nginx_hook=True,
domains=[mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"],
)

View File

@@ -5,7 +5,7 @@ from pyinfra import host
from pyinfra.facts.systemd import SystemdStatus
def deploy_acmetool(nginx_hook=False, email="", domains=[]):
def deploy_acmetool(email="", domains=[]):
"""Deploy acmetool."""
apt.packages(
name="Install acmetool",
@@ -20,16 +20,13 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
mode="644",
)
if nginx_hook:
files.put(
src=importlib.resources.files(__package__)
.joinpath("acmetool.hook")
.open("rb"),
dest="/usr/lib/acme/hooks/nginx",
user="root",
group="root",
mode="744",
)
files.put(
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
dest="/usr/lib/acme/hooks/nginx",
user="root",
group="root",
mode="744",
)
files.template(
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
@@ -74,5 +71,5 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
server.shell(
name=f"Request certificate for: { ', '.join(domains) }",
commands=[f"acmetool want { ' '.join(domains)}"],
commands=[f"acmetool want --xlog.severity=debug { ' '.join(domains)}"],
)

View File

@@ -3,3 +3,5 @@ set -e
EVENT_NAME="$1"
[ "$EVENT_NAME" = "live-updated" ] || exit 42
systemctl restart nginx.service
systemctl reload dovecot.service
systemctl reload postfix.service

View File

@@ -1,2 +1,2 @@
"acme-enter-email": "{{ email }}"
"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf": true
"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf": true

View File

@@ -11,6 +11,11 @@ class DNS:
self.session = requests.Session()
self.out = out
self.ssh = f"ssh root@{mail_domain} -- "
self.out.shell_output(
f"{ self.ssh }'apt-get update && apt-get install -y dnsutils'",
timeout=60,
no_print=True,
)
try:
self.shell(f"unbound-control flush_zone {mail_domain}")
except subprocess.CalledProcessError:

View File

@@ -1,4 +1,4 @@
uri = proxy:/run/dovecot/doveauth.socket:auth
uri = proxy:/run/doveauth/doveauth.socket:auth
iterate_disable = yes
default_pass_scheme = plain
# %E escapes characters " (double quote), ' (single quote) and \ (backslash) with \ (backslash).

View File

@@ -78,7 +78,7 @@ mail_privileged_group = vmail
##
# Pass all IMAP METADATA requests to the server implementing Dovecot's dict protocol.
mail_attribute_dict = proxy:/run/dovecot/metadata.socket:metadata
mail_attribute_dict = proxy:/run/chatmail-metadata/metadata.socket:metadata
# Enable IMAP COMPRESS (RFC 4978).
# <https://datatracker.ietf.org/doc/html/rfc4978.html>

Binary file not shown.

View File

@@ -2,9 +2,11 @@
Description=Chatmail dict proxy for IMAP METADATA
[Service]
ExecStart={execpath} /run/dovecot/metadata.socket vmail /home/vmail/mail/{mail_domain}
ExecStart={execpath} /run/chatmail-metadata/metadata.socket /home/vmail/mail/{mail_domain}
Restart=always
RestartSec=30
User=vmail
RuntimeDirectory=chatmail-metadata
[Install]
WantedBy=multi-user.target

View File

@@ -2,9 +2,11 @@
Description=Chatmail dict authentication proxy for dovecot
[Service]
ExecStart={execpath} /run/dovecot/doveauth.socket vmail /home/vmail/passdb.sqlite {config_path}
ExecStart={execpath} /run/doveauth/doveauth.socket /home/vmail/passdb.sqlite {config_path}
Restart=always
RestartSec=30
User=vmail
RuntimeDirectory=doveauth
[Install]
WantedBy=multi-user.target

View File

@@ -7,5 +7,61 @@ Environment="PATH={remote_venv_dir}:$PATH"
Restart=always
RestartSec=30
User=echobot
Group=echobot
# Create /var/lib/echobot
StateDirectory=echobot
# Create /run/echobot
#
# echobot stores /run/echobot/password
# with a password there, which doveauth then reads.
RuntimeDirectory=echobot
WorkingDirectory=/var/lib/echobot
# Apply security restrictions suggested by
# systemd-analyze security echobot.service
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateMounts=true
PrivateTmp=true
# We need to know about doveauth user to give it access to /run/echobot/password
PrivateUsers=false
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=noaccess
# Should be "strict", but we currently write /accounts folder in a protected path
ProtectSystem=full
RemoveIPC=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=~@clock
SystemCallFilter=~@cpu-emulation
SystemCallFilter=~@debug
SystemCallFilter=~@module
SystemCallFilter=~@mount
SystemCallFilter=~@obsolete
SystemCallFilter=~@raw-io
SystemCallFilter=~@reboot
SystemCallFilter=~@resources
SystemCallFilter=~@swap
UMask=0077
[Install]
WantedBy=multi-user.target

View File

@@ -2,6 +2,8 @@
# Privacy Policy for {{ config.mail_domain }}
{{ config.privacy_intro }}
We want to show you in a fair and transparent way
what personal data is processed by us.
We follow a strict privacy-by-design approach