Compare commits

...

11 Commits

Author SHA1 Message Date
holger krekel
fcfc143f1a add changelog entries 2024-03-25 14:11:22 +01:00
holger krekel
be660a688d add a first changelog for the last week of changes 2024-03-21 09:07:20 +01:00
holger krekel
8d9019b1c5 fix runtime dovecot/sieve-compile error on every incoming message 2024-03-20 19:10:54 +01:00
holger krekel
63d3e05674 remove superflous check in tests 2024-03-20 19:10:44 +01:00
holger krekel
e466a03055 fixes 2024-03-20 19:10:44 +01:00
holger krekel
1819a276cb implement persistence via marshal 2024-03-20 19:10:44 +01:00
holger krekel
9ec6430b71 make notifier take a directory 2024-03-20 19:10:44 +01:00
missytake
2097233fd6 expunge: reset maildirsize after expunging old mails 2024-03-18 07:03:06 +01:00
link2xt
4bca7891a2 Switch SPF from fail to softfail (~all instead of -all)
This is recommended to prevent SPF failure
from rejecting the message early in case messages
are remailed without breaking DKIM.
2024-03-09 20:02:29 +00:00
link2xt
2e23e743fd dovecot: increase default_client_limit 2024-03-09 14:01:00 +01:00
link2xt
edc593586b Implement "iterate" command in metadata server
Otherwise Dovecot times out when trying to iterate over metadata
of the folder. Apparently it happens when attempting to delete
folder from the server over IMAP.
2024-03-08 05:39:59 +01:00
9 changed files with 134 additions and 30 deletions

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
# Changelog for chatmail deployment
## unreleased
### Changes since March 15th, 2024
- Fix various tests to pass again with "cmdeploy test".
([#245](https://github.com/deltachat/chatmail/pull/245),
[#242](https://github.com/deltachat/chatmail/pull/242)
- Ensure lets-encrypt certificates are reloaded after renewal
([#244]) https://github.com/deltachat/chatmail/pull/244
- Persist tokens to avoid iOS users loosing push-notifications when the
chatmail metadata service is restarted (happens regularly during deploys)
([#238](https://github.com/deltachat/chatmail/pull/239)
- Fix failing sieve-script compile errors on incoming messages
([#237](https://github.com/deltachat/chatmail/pull/239)
- Fix quota reporting after expunging of old mails
([#233](https://github.com/deltachat/chatmail/pull/239)

View File

@@ -2,7 +2,7 @@
Description=Chatmail dict proxy for IMAP METADATA
[Service]
ExecStart={execpath} /run/dovecot/metadata.socket vmail {config_path}
ExecStart={execpath} /run/dovecot/metadata.socket vmail {config_path} /home/vmail/metadata
Restart=always
RestartSec=30

View File

@@ -1,5 +1,6 @@
import pwd
import pathlib
from queue import Queue
from threading import Thread
from socketserver import (
@@ -12,9 +13,11 @@ import sys
import logging
import os
import requests
import marshal
DICTPROXY_LOOKUP_CHAR = "L"
DICTPROXY_ITERATE_CHAR = "I"
DICTPROXY_SET_CHAR = "S"
DICTPROXY_BEGIN_TRANSACTION_CHAR = "B"
DICTPROXY_COMMIT_TRANSACTION_CHAR = "C"
@@ -22,12 +25,37 @@ DICTPROXY_TRANSACTION_CHARS = "SBC"
class Notifier:
def __init__(self):
self.guid2token = {}
def __init__(self, metadata_dir):
self.metadata_dir = metadata_dir
self.to_notify_queue = Queue()
def get_metadata(self, guid):
guid_path = self.metadata_dir.joinpath(guid)
if guid_path.exists():
with guid_path.open("rb") as f:
return marshal.load(f)
return {}
def set_metadata(self, guid, guid_data):
guid_path = self.metadata_dir.joinpath(guid)
write_path = guid_path.with_suffix(".tmp")
with write_path.open("wb") as f:
marshal.dump(guid_data, f)
os.rename(write_path, guid_path)
def set_token(self, guid, token):
self.guid2token[guid] = token
guid_data = self.get_metadata(guid)
guid_data["token"] = token
self.set_metadata(guid, guid_data)
def del_token(self, guid):
guid_data = self.get_metadata(guid)
if "token" in guid_data:
del guid_data["token"]
self.set_metadata(guid, guid_data)
def get_token(self, guid):
return self.get_metadata(guid).get("token")
def new_message_for_guid(self, guid):
self.to_notify_queue.put(guid)
@@ -39,7 +67,7 @@ class Notifier:
def thread_run_one(self, requests_session):
guid = self.to_notify_queue.get()
token = self.guid2token.get(guid)
token = self.get_token(guid)
if token:
response = requests_session.post(
"https://notifications.delta.chat/notify",
@@ -49,7 +77,7 @@ class Notifier:
if response.status_code == 410:
# 410 Gone status code
# means the token is no longer valid.
del self.guid2token[guid]
self.del_token(guid)
def handle_dovecot_protocol(rfile, wfile, notifier):
@@ -74,6 +102,10 @@ def handle_dovecot_request(msg, transactions, notifier):
parts = msg[1:].split("\t")
if short_command == DICTPROXY_LOOKUP_CHAR:
return "N\n"
elif short_command == DICTPROXY_ITERATE_CHAR:
# Empty line means ITER_FINISHED.
# If we don't return empty line Dovecot will timeout.
return "\n"
if short_command not in (DICTPROXY_TRANSACTION_CHARS):
return
@@ -114,12 +146,15 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
def main():
socket, username, config = sys.argv[1:]
socket, username, config, metadata_dir = sys.argv[1:]
passwd_entry = pwd.getpwnam(username)
# XXX config is not currently used
config = read_config(config)
notifier = Notifier()
metadata_dir = pathlib.Path(metadata_dir)
if not metadata_dir.exists():
metadata_dir.mkdir()
notifier = Notifier(metadata_dir)
class Handler(StreamRequestHandler):
def handle(self):

View File

@@ -1,4 +1,5 @@
import io
import pytest
from chatmaild.metadata import (
handle_dovecot_request,
@@ -7,40 +8,60 @@ from chatmaild.metadata import (
)
def test_handle_dovecot_request_lookup_fails():
notifier = Notifier()
@pytest.fixture
def notifier(tmp_path):
metadata_dir = tmp_path.joinpath("metadata")
metadata_dir.mkdir()
return Notifier(metadata_dir)
def test_notifier_persistence(tmp_path):
metadata_dir = tmp_path.joinpath("metadata")
metadata_dir.mkdir()
notifier1 = Notifier(metadata_dir)
notifier2 = Notifier(metadata_dir)
assert notifier1.get_token(guid="guid00") is None
assert notifier2.get_token(guid="guid00") is None
notifier1.set_token("guid00", "01234")
notifier1.set_token("guid03", "456")
assert notifier2.get_token("guid00") == "01234"
assert notifier2.get_token("guid03") == "456"
notifier2.del_token("guid00")
assert notifier1.get_token("guid00") is None
def test_handle_dovecot_request_lookup_fails(notifier):
res = handle_dovecot_request("Lpriv/123/chatmail", {}, notifier)
assert res == "N\n"
def test_handle_dovecot_request_happy_path():
notifier = Notifier()
def test_handle_dovecot_request_happy_path(notifier):
transactions = {}
# lookups return the same NOTFOUND result
res = handle_dovecot_request("Lpriv/123/chatmail", transactions, notifier)
assert res == "N\n"
assert not notifier.guid2token and not transactions
assert notifier.get_token("guid00") is None and not transactions
# set device token in a transaction
tx = "1111"
msg = f"B{tx}\tuser"
res = handle_dovecot_request(msg, transactions, notifier)
assert not res and not notifier.guid2token
assert not res and notifier.get_token("guid00") is None
assert transactions == {tx: "O\n"}
msg = f"S{tx}\tpriv/guid00/devicetoken\t01234"
res = handle_dovecot_request(msg, transactions, notifier)
assert not res
assert len(transactions) == 1
assert len(notifier.guid2token) == 1
assert notifier.guid2token["guid00"] == "01234"
assert notifier.get_token("guid00") == "01234"
msg = f"C{tx}"
res = handle_dovecot_request(msg, transactions, notifier)
assert res == "O\n"
assert len(transactions) == 0
assert notifier.guid2token["guid00"] == "01234"
assert notifier.get_token("guid00") == "01234"
# trigger notification for incoming message
assert handle_dovecot_request(f"B{tx}\tuser", transactions, notifier) is None
@@ -52,7 +73,7 @@ def test_handle_dovecot_request_happy_path():
assert not transactions
def test_handle_dovecot_protocol_set_devicetoken():
def test_handle_dovecot_protocol_set_devicetoken(notifier):
rfile = io.BytesIO(
b"\n".join(
[
@@ -64,13 +85,26 @@ def test_handle_dovecot_protocol_set_devicetoken():
)
)
wfile = io.BytesIO()
notifier = Notifier()
handle_dovecot_protocol(rfile, wfile, notifier)
assert notifier.guid2token["guid00"] == "01234"
assert notifier.get_token("guid00") == "01234"
assert wfile.getvalue() == b"O\n"
def test_handle_dovecot_protocol_messagenew():
def test_handle_dovecot_protocol_iterate(notifier):
rfile = io.BytesIO(
b"\n".join(
[
b"H",
b"I9\t0\tpriv/5cbe730f146fea6535be0d003dd4fc98/\tci-2dzsrs@nine.testrun.org",
]
)
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
assert wfile.getvalue() == b"\n"
def test_handle_dovecot_protocol_messagenew(notifier):
rfile = io.BytesIO(
b"\n".join(
[
@@ -82,14 +116,13 @@ def test_handle_dovecot_protocol_messagenew():
)
)
wfile = io.BytesIO()
notifier = Notifier()
handle_dovecot_protocol(rfile, wfile, notifier)
assert wfile.getvalue() == b"O\n"
assert notifier.to_notify_queue.get() == "guid00"
assert notifier.to_notify_queue.qsize() == 0
def test_notifier_thread_run():
def test_notifier_thread_run(notifier):
requests = []
class ReqMock:
@@ -101,16 +134,15 @@ def test_notifier_thread_run():
return Result()
notifier = Notifier()
notifier.set_token("guid00", "01234")
notifier.new_message_for_guid("guid00")
notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0]
assert data == "01234"
assert len(notifier.guid2token) == 1
assert notifier.get_token("guid00") == "01234"
def test_notifier_thread_run_gone_removes_token():
def test_notifier_thread_run_gone_removes_token(notifier):
requests = []
class ReqMock:
@@ -122,11 +154,10 @@ def test_notifier_thread_run_gone_removes_token():
return Result()
notifier = Notifier()
notifier.set_token("guid00", "01234")
notifier.new_message_for_guid("guid00")
assert notifier.guid2token["guid00"] == "01234"
assert notifier.get_token("guid00") == "01234"
notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0]
assert data == "01234"
assert len(notifier.guid2token) == 0
assert notifier.get_token("guid00") is None

View File

@@ -359,6 +359,13 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
mode="644",
)
need_restart |= sieve_script.changed
if sieve_script.changed:
server.shell(
name=f"compile sieve script",
commands=[
f"/usr/bin/sievec /etc/dovecot/default.sieve"
],
)
files.template(
src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"),

View File

@@ -6,7 +6,7 @@ _submissions._tcp.{chatmail_domain}. SRV 0 1 465 {chatmail_domain}.
_imap._tcp.{chatmail_domain}. SRV 0 1 143 {chatmail_domain}.
_imaps._tcp.{chatmail_domain}. SRV 0 1 993 {chatmail_domain}.
{chatmail_domain}. CAA 128 issue "letsencrypt.org;accounturi={acme_account_url}"
{chatmail_domain}. TXT "v=spf1 a:{chatmail_domain} -all"
{chatmail_domain}. TXT "v=spf1 a:{chatmail_domain} ~all"
_dmarc.{chatmail_domain}. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
_mta-sts.{chatmail_domain}. TXT "v=STSv1; id={sts_id}"
mta-sts.{chatmail_domain}. CNAME {chatmail_domain}.

View File

@@ -1,5 +1,7 @@
require ["imap4flags"];
# flag the message so it doesn't cause a push notification
if header :is ["Auto-Submitted"] ["auto-replied", "auto-generated"] {
addflag "$Auto";
}

View File

@@ -13,6 +13,12 @@ auth_cache_size = 100M
mail_debug = yes
{% endif %}
# Prevent warnings similar to:
# config: Warning: service auth { client_limit=1000 } is lower than required under max. load (10200). Counted for protocol services with service_count != 1: service lmtp { process_limit=100 } + service imap-urlauth-login { process_limit=100 } + service imap-login { process_limit=10000 }
# config: Warning: service anvil { client_limit=1000 } is lower than required under max. load (10103). Counted with: service imap-urlauth-login { process_limit=100 } + service imap-login { process_limit=10000 } + service auth { process_limit=1 }
# master: Warning: service(stats): client_limit (1000) reached, client connections are being dropped
default_client_limit = 20000
mail_server_admin = mailto:root@{{ config.mail_domain }}
mail_server_comment = Chatmail server

View File

@@ -8,3 +8,4 @@
# or only temporary (but then they shouldn't be around after {{ config.delete_mails_after }} days anyway).
2 0 * * * vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/tmp/*' -mtime +{{ config.delete_mails_after }} -type f -delete
2 0 * * * vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/.*/tmp/*' -mtime +{{ config.delete_mails_after }} -type f -delete
3 0 * * * vmail find /home/vmail/mail/{{ config.mail_domain }} -name 'maildirsize' -type f -delete