Compare commits

...

21 Commits

Author SHA1 Message Date
holger krekel
d51a60be57 fix error string 2024-03-28 12:17:33 +01:00
holger krekel
0938b3a1b5 persist pending notifications to directory so that they survive a restart 2024-03-28 12:13:53 +01:00
holger krekel
4710e0d734 use json instead of python's marshal 2024-03-28 11:05:25 +01:00
holger krekel
d74b25adea test and fix for edge case 2024-03-28 10:49:57 +01:00
holger krekel
917da899c6 add changelog entry 2024-03-28 10:33:20 +01:00
holger krekel
92dbabc23d various naming refinements 2024-03-28 10:28:42 +01:00
holger krekel
277465462e remove timeout support, it's not needed 2024-03-27 18:34:01 +01:00
holger krekel
92b7273c71 refine logging 2024-03-27 18:20:00 +01:00
holger krekel
9c31d0762e more resilience 2024-03-27 18:14:43 +01:00
holger krekel
fab5e8a082 move persistentdict into own file, rename 2024-03-27 18:07:56 +01:00
holger krekel
1da5d91b71 extend imap online test to cover multi-device 2024-03-27 17:41:57 +01:00
holger krekel
c45e98d1dc back to using marshal, and a filelock 2024-03-27 17:25:29 +01:00
holger krekel
409b2b6919 add a persistent dict impl 2024-03-27 17:06:43 +01:00
holger krekel
e2a1ddb987 add multi-token support 2024-03-27 15:03:59 +01:00
holger krekel
89734d99cf fix target dir 2024-03-27 13:49:14 +01:00
holger krekel
193c8b2e85 use "devicetoken" consistently and take it from a var 2024-03-27 13:29:42 +01:00
holger krekel
8694dce7ec properly startup metadata service and add online test for metadata 2024-03-27 13:17:33 +01:00
holger krekel
0cf092abd5 store metadata in a per-mbox dir 2024-03-27 12:45:05 +01:00
holger krekel
419de239ac store tokens on a per-maildir basis 2024-03-27 12:27:12 +01:00
holger krekel
00bed66660 store tokens in guid-directories 2024-03-27 10:29:22 +01:00
link2xt
845ee42f76 Store raw tokens instead of dictionaries in metadata 2024-03-27 10:01:46 +01:00
10 changed files with 314 additions and 129 deletions

View File

@@ -4,6 +4,9 @@
### Changes since March 15th, 2024 ### Changes since March 15th, 2024
- Persist push tokens and support multiple device per address
([#254](https://github.com/deltachat/chatmail/pull/254))
- Avoid warning for regular doveauth protocol's hello message. - Avoid warning for regular doveauth protocol's hello message.
([#250](https://github.com/deltachat/chatmail/pull/250)) ([#250](https://github.com/deltachat/chatmail/pull/250))

View File

@@ -10,6 +10,7 @@ dependencies = [
"iniconfig", "iniconfig",
"deltachat-rpc-server", "deltachat-rpc-server",
"deltachat-rpc-client", "deltachat-rpc-client",
"filelock",
"requests", "requests",
] ]

View File

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

View File

@@ -0,0 +1,35 @@
import os
import logging
import json
import filelock
from contextlib import contextmanager
class FileDict:
"""Concurrency-safe multi-reader/single-writer persistent dict."""
def __init__(self, path):
self.path = path
self.lock_path = path.with_name(path.name + ".lock")
@contextmanager
def modify(self):
# the OS will release the lock if the process dies,
# and the contextmanager will otherwise guarantee release
with filelock.FileLock(self.lock_path):
data = self.read()
yield data
write_path = self.path.with_name(self.path.name + ".tmp")
with write_path.open("w") as f:
json.dump(data, f)
os.rename(write_path, self.path)
def read(self):
try:
with self.path.open("r") as f:
return json.load(f)
except FileNotFoundError:
return {}
except Exception:
logging.warning("corrupt serialization state at: %r", self.path)
return {}

View File

@@ -1,89 +1,93 @@
import pwd import pwd
import pathlib from pathlib import Path
from queue import Queue from threading import Thread, Event
from threading import Thread
from socketserver import ( from socketserver import (
UnixStreamServer, UnixStreamServer,
StreamRequestHandler, StreamRequestHandler,
ThreadingMixIn, ThreadingMixIn,
) )
from .config import read_config
import sys import sys
import logging import logging
import os import os
import requests import requests
import marshal
from .filedict import FileDict
DICTPROXY_HELLO_CHAR = "H"
DICTPROXY_LOOKUP_CHAR = "L" DICTPROXY_LOOKUP_CHAR = "L"
DICTPROXY_ITERATE_CHAR = "I" DICTPROXY_ITERATE_CHAR = "I"
DICTPROXY_SET_CHAR = "S"
DICTPROXY_BEGIN_TRANSACTION_CHAR = "B" DICTPROXY_BEGIN_TRANSACTION_CHAR = "B"
DICTPROXY_SET_CHAR = "S"
DICTPROXY_COMMIT_TRANSACTION_CHAR = "C" DICTPROXY_COMMIT_TRANSACTION_CHAR = "C"
DICTPROXY_TRANSACTION_CHARS = "SBC" 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 Notifier: class Notifier:
def __init__(self, metadata_dir): def __init__(self, vmail_dir):
self.metadata_dir = metadata_dir self.vmail_dir = vmail_dir
self.to_notify_queue = Queue() 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(self, guid): def get_metadata_dict(self, addr):
guid_path = self.metadata_dir.joinpath(guid) return FileDict(self.vmail_dir / addr / "metadata.json")
if guid_path.exists():
with guid_path.open("rb") as f:
return marshal.load(f)
return {}
def set_metadata(self, guid, guid_data): def add_token(self, addr, token):
guid_path = self.metadata_dir.joinpath(guid) with self.get_metadata_dict(addr).modify() as data:
write_path = guid_path.with_suffix(".tmp") tokens = data.get(METADATA_TOKEN_KEY)
with write_path.open("wb") as f: if tokens is None:
marshal.dump(guid_data, f) data[METADATA_TOKEN_KEY] = [token]
os.rename(write_path, guid_path) elif token not in tokens:
tokens.append(token)
def set_token(self, guid, token): def remove_token(self, addr, token):
guid_data = self.get_metadata(guid) with self.get_metadata_dict(addr).modify() as data:
guid_data["token"] = token tokens = data.get(METADATA_TOKEN_KEY, [])
self.set_metadata(guid, guid_data) try:
tokens.remove(token)
except ValueError:
pass
def del_token(self, guid): def get_tokens(self, addr):
guid_data = self.get_metadata(guid) return self.get_metadata_dict(addr).read().get(METADATA_TOKEN_KEY, [])
if "token" in guid_data:
del guid_data["token"]
self.set_metadata(guid, guid_data)
def get_token(self, guid): def new_message_for_addr(self, addr):
return self.get_metadata(guid).get("token") self.notification_dir.joinpath(addr).touch()
self.message_arrived_event.set()
def new_message_for_guid(self, guid):
self.to_notify_queue.put(guid)
def thread_run_loop(self): def thread_run_loop(self):
requests_session = requests.Session() requests_session = requests.Session()
while 1: while 1:
self.message_arrived_event.wait()
self.message_arrived_event.clear()
self.thread_run_one(requests_session) self.thread_run_one(requests_session)
def thread_run_one(self, requests_session): def thread_run_one(self, requests_session):
guid = self.to_notify_queue.get() for addr_path in self.notification_dir.iterdir():
token = self.get_token(guid) addr = addr_path.name
if token: if "@" not in addr:
response = requests_session.post( continue
"https://notifications.delta.chat/notify", for token in self.get_tokens(addr):
data=token, response = requests_session.post(
timeout=60, "https://notifications.delta.chat/notify",
) data=token,
if response.status_code == 410: timeout=60,
# 410 Gone status code )
# means the token is no longer valid. if response.status_code == 410:
self.del_token(guid) # 410 Gone status code
# means the token is no longer valid.
self.remove_token(addr, token)
addr_path.unlink()
def handle_dovecot_protocol(rfile, wfile, notifier): def handle_dovecot_protocol(rfile, wfile, notifier):
# HELLO message, ignored.
msg = rfile.readline().strip().decode()
transactions = {} transactions = {}
while True: while True:
msg = rfile.readline().strip().decode() msg = rfile.readline().strip().decode()
@@ -101,44 +105,52 @@ def handle_dovecot_request(msg, transactions, notifier):
short_command = msg[0] short_command = msg[0]
parts = msg[1:].split("\t") parts = msg[1:].split("\t")
if short_command == DICTPROXY_LOOKUP_CHAR: if short_command == DICTPROXY_LOOKUP_CHAR:
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
keyparts = parts[0].split("/")
if keyparts[0] == "priv":
keyname = keyparts[2]
addr = parts[1]
if keyname == METADATA_TOKEN_KEY:
res = " ".join(notifier.get_tokens(addr))
return f"O{res}\n"
logging.warning("lookup ignored: %r", msg)
return "N\n" return "N\n"
elif short_command == DICTPROXY_ITERATE_CHAR: elif short_command == DICTPROXY_ITERATE_CHAR:
# Empty line means ITER_FINISHED. # Empty line means ITER_FINISHED.
# If we don't return empty line Dovecot will timeout. # If we don't return empty line Dovecot will timeout.
return "\n" return "\n"
elif short_command == DICTPROXY_HELLO_CHAR:
return # no version checking
if short_command not in (DICTPROXY_TRANSACTION_CHARS): if short_command not in (DICTPROXY_TRANSACTION_CHARS):
logging.warning("unknown dictproxy request: %r", msg)
return return
transaction_id = parts[0] transaction_id = parts[0]
if short_command == DICTPROXY_BEGIN_TRANSACTION_CHAR: if short_command == DICTPROXY_BEGIN_TRANSACTION_CHAR:
transactions[transaction_id] = "O\n" addr = parts[1]
transactions[transaction_id] = dict(addr=addr, res="O\n")
elif short_command == DICTPROXY_COMMIT_TRANSACTION_CHAR: elif short_command == DICTPROXY_COMMIT_TRANSACTION_CHAR:
# returns whether it failed or succeeded. # each set devicetoken operation persists directly
return transactions.pop(transaction_id, "N\n") # and does not wait until a "commit" comes
# because our dovecot config does not involve
# multiple set-operations in a single commit
return transactions.pop(transaction_id)["res"]
elif short_command == DICTPROXY_SET_CHAR: elif short_command == DICTPROXY_SET_CHAR:
# See header of # For documentation on key structure see
# <https://github.com/dovecot/core/blob/5e7965632395793d9355eb906b173bf28d2a10ca/src/lib-storage/mailbox-attribute.h> # https://github.com/dovecot/core/blob/main/src/lib-storage/mailbox-attribute.h
# for the documentation on the structure of the key.
# Request GETMETADATA "INBOX" /private/chatmail
# results in a query for
# priv/dd72550f05eadc65542a1200cac67ad7/chatmail
#
# Request GETMETADATA "" /private/chatmail
# results in
# priv/dd72550f05eadc65542a1200cac67ad7/vendor/vendor.dovecot/pvt/server/chatmail
keyname = parts[1].split("/") keyname = parts[1].split("/")
value = parts[2] if len(parts) > 2 else "" value = parts[2] if len(parts) > 2 else ""
if keyname[0] == "priv" and keyname[2] == "devicetoken": addr = transactions[transaction_id]["addr"]
notifier.set_token(keyname[1], value) if keyname[0] == "priv" and keyname[2] == METADATA_TOKEN_KEY:
notifier.add_token(addr, value)
elif keyname[0] == "priv" and keyname[2] == "messagenew": elif keyname[0] == "priv" and keyname[2] == "messagenew":
notifier.new_message_for_guid(keyname[1]) notifier.new_message_for_addr(addr)
else: else:
# Transaction failed. # Transaction failed.
transactions[transaction_id] = "F\n" transactions[transaction_id]["res"] = "F\n"
class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer): class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
@@ -146,22 +158,23 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
def main(): def main():
socket, username, config, metadata_dir = sys.argv[1:] socket, username, vmail_dir = sys.argv[1:]
passwd_entry = pwd.getpwnam(username) passwd_entry = pwd.getpwnam(username)
# XXX config is not currently used vmail_dir = Path(vmail_dir)
config = read_config(config)
metadata_dir = pathlib.Path(metadata_dir) if not vmail_dir.exists():
if not metadata_dir.exists(): logging.error("vmail dir does not exist: %r", vmail_dir)
metadata_dir.mkdir() return 1
notifier = Notifier(metadata_dir)
notifier = Notifier(vmail_dir)
class Handler(StreamRequestHandler): class Handler(StreamRequestHandler):
def handle(self): def handle(self):
try: try:
handle_dovecot_protocol(self.rfile, self.wfile, notifier) handle_dovecot_protocol(self.rfile, self.wfile, notifier)
except Exception: except Exception:
logging.exception("Exception in the handler") logging.exception("Exception in the dovecot dictproxy handler")
raise raise
try: try:
@@ -175,6 +188,8 @@ def main():
t = Thread(target=notifier.thread_run_loop) t = Thread(target=notifier.thread_run_loop)
t.setDaemon(True) t.setDaemon(True)
t.start() 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: with ThreadedUnixStreamServer(socket, Handler) as server:
os.chown(socket, uid=passwd_entry.pw_uid, gid=passwd_entry.pw_gid) os.chown(socket, uid=passwd_entry.pw_uid, gid=passwd_entry.pw_gid)

View File

@@ -0,0 +1,19 @@
from chatmaild.filedict import FileDict
def test_basic(tmp_path):
fdict = FileDict(tmp_path.joinpath("metadata"))
assert fdict.read() == {}
with fdict.modify() as d:
d["devicetoken"] = [1, 2, 3]
d["456"] = 4.2
new = fdict.read()
assert new["devicetoken"] == [1, 2, 3]
assert new["456"] == 4.2
def test_bad_marshal_file(tmp_path, caplog):
fdict1 = FileDict(tmp_path.joinpath("metadata"))
fdict1.path.write_bytes(b"l12k3l12k3l")
assert fdict1.read() == {}
assert "corrupt" in caplog.records[0].msg

View File

@@ -10,67 +10,84 @@ from chatmaild.metadata import (
@pytest.fixture @pytest.fixture
def notifier(tmp_path): def notifier(tmp_path):
metadata_dir = tmp_path.joinpath("metadata") vmail_dir = tmp_path.joinpath("vmaildir")
metadata_dir.mkdir() vmail_dir.mkdir()
return Notifier(metadata_dir) return Notifier(vmail_dir)
def test_notifier_persistence(tmp_path): @pytest.fixture
metadata_dir = tmp_path.joinpath("metadata") def testaddr():
metadata_dir.mkdir() return "user.name@example.org"
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): @pytest.fixture
res = handle_dovecot_request("Lpriv/123/chatmail", {}, notifier) 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"]
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 test_notifier_delete_without_set(notifier, testaddr):
notifier.remove_token(testaddr, "123")
assert not notifier.get_tokens(testaddr)
def test_handle_dovecot_request_lookup_fails(notifier, testaddr):
res = handle_dovecot_request(f"Lpriv/123/chatmail\t{testaddr}", {}, notifier)
assert res == "N\n" assert res == "N\n"
def test_handle_dovecot_request_happy_path(notifier): def test_handle_dovecot_request_happy_path(notifier, testaddr):
transactions = {} transactions = {}
# lookups return the same NOTFOUND result
res = handle_dovecot_request("Lpriv/123/chatmail", transactions, notifier)
assert res == "N\n"
assert notifier.get_token("guid00") is None and not transactions
# set device token in a transaction # set device token in a transaction
tx = "1111" tx = "1111"
msg = f"B{tx}\tuser" msg = f"B{tx}\t{testaddr}"
res = handle_dovecot_request(msg, transactions, notifier) res = handle_dovecot_request(msg, transactions, notifier)
assert not res and notifier.get_token("guid00") is None assert not res and not notifier.get_tokens(testaddr)
assert transactions == {tx: "O\n"} assert transactions == {tx: dict(addr=testaddr, res="O\n")}
msg = f"S{tx}\tpriv/guid00/devicetoken\t01234" msg = f"S{tx}\tpriv/guid00/devicetoken\t01234"
res = handle_dovecot_request(msg, transactions, notifier) res = handle_dovecot_request(msg, transactions, notifier)
assert not res assert not res
assert len(transactions) == 1 assert len(transactions) == 1
assert notifier.get_token("guid00") == "01234" assert notifier.get_tokens(testaddr) == ["01234"]
msg = f"C{tx}" msg = f"C{tx}"
res = handle_dovecot_request(msg, transactions, notifier) res = handle_dovecot_request(msg, transactions, notifier)
assert res == "O\n" assert res == "O\n"
assert len(transactions) == 0 assert len(transactions) == 0
assert notifier.get_token("guid00") == "01234" assert notifier.get_tokens(testaddr) == ["01234"]
# trigger notification for incoming message # trigger notification for incoming message
assert handle_dovecot_request(f"B{tx}\tuser", transactions, notifier) is None tx2 = "2222"
msg = f"S{tx}\tpriv/guid00/messagenew" assert handle_dovecot_request(f"B{tx2}\t{testaddr}", transactions, notifier) is None
msg = f"S{tx2}\tpriv/guid00/messagenew"
assert handle_dovecot_request(msg, transactions, notifier) is None assert handle_dovecot_request(msg, transactions, notifier) is None
assert notifier.to_notify_queue.get() == "guid00" assert notifier.message_arrived_event.is_set()
assert notifier.to_notify_queue.qsize() == 0 assert handle_dovecot_request(f"C{tx2}", transactions, notifier) == "O\n"
assert handle_dovecot_request(f"C{tx}\tuser", transactions, notifier) == "O\n"
assert not transactions assert not transactions
assert notifier.notification_dir.joinpath(testaddr).exists()
def test_handle_dovecot_protocol_set_devicetoken(notifier): def test_handle_dovecot_protocol_set_devicetoken(notifier):
@@ -78,7 +95,7 @@ def test_handle_dovecot_protocol_set_devicetoken(notifier):
b"\n".join( b"\n".join(
[ [
b"HELLO", b"HELLO",
b"Btx00\tuser", b"Btx00\tuser@example.org",
b"Stx00\tpriv/guid00/devicetoken\t01234", b"Stx00\tpriv/guid00/devicetoken\t01234",
b"Ctx00", b"Ctx00",
] ]
@@ -86,8 +103,32 @@ def test_handle_dovecot_protocol_set_devicetoken(notifier):
) )
wfile = io.BytesIO() wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier) handle_dovecot_protocol(rfile, wfile, notifier)
assert notifier.get_token("guid00") == "01234"
assert wfile.getvalue() == b"O\n" assert wfile.getvalue() == b"O\n"
assert notifier.get_tokens("user@example.org") == ["01234"]
def test_handle_dovecot_protocol_set_get_devicetoken(notifier):
rfile = io.BytesIO(
b"\n".join(
[
b"HELLO",
b"Btx00\tuser@example.org",
b"Stx00\tpriv/guid00/devicetoken\t01234",
b"Ctx00",
]
)
)
wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier)
assert notifier.get_tokens("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)
assert wfile.getvalue() == b"O01234\n"
def test_handle_dovecot_protocol_iterate(notifier): def test_handle_dovecot_protocol_iterate(notifier):
@@ -109,7 +150,7 @@ def test_handle_dovecot_protocol_messagenew(notifier):
b"\n".join( b"\n".join(
[ [
b"HELLO", b"HELLO",
b"Btx01\tuser", b"Btx01\tuser@example.org",
b"Stx01\tpriv/guid00/messagenew", b"Stx01\tpriv/guid00/messagenew",
b"Ctx01", b"Ctx01",
] ]
@@ -118,11 +159,11 @@ def test_handle_dovecot_protocol_messagenew(notifier):
wfile = io.BytesIO() wfile = io.BytesIO()
handle_dovecot_protocol(rfile, wfile, notifier) handle_dovecot_protocol(rfile, wfile, notifier)
assert wfile.getvalue() == b"O\n" assert wfile.getvalue() == b"O\n"
assert notifier.to_notify_queue.get() == "guid00" assert notifier.message_arrived_event.is_set()
assert notifier.to_notify_queue.qsize() == 0 assert notifier.notification_dir.joinpath("user@example.org").exists()
def test_notifier_thread_run(notifier): def test_notifier_thread_run(notifier, testaddr):
requests = [] requests = []
class ReqMock: class ReqMock:
@@ -134,15 +175,15 @@ def test_notifier_thread_run(notifier):
return Result() return Result()
notifier.set_token("guid00", "01234") notifier.add_token(testaddr, "01234")
notifier.new_message_for_guid("guid00") notifier.new_message_for_addr(testaddr)
notifier.thread_run_one(ReqMock()) notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0] url, data, timeout = requests[0]
assert data == "01234" assert data == "01234"
assert notifier.get_token("guid00") == "01234" assert notifier.get_tokens(testaddr) == ["01234"]
def test_notifier_thread_run_gone_removes_token(notifier): def test_multi_device_notifier(notifier, testaddr):
requests = [] requests = []
class ReqMock: class ReqMock:
@@ -150,14 +191,40 @@ def test_notifier_thread_run_gone_removes_token(notifier):
requests.append((url, data, timeout)) requests.append((url, data, timeout))
class Result: class Result:
status_code = 410 status_code = 200
return Result() return Result()
notifier.set_token("guid00", "01234") notifier.add_token(testaddr, "01234")
notifier.new_message_for_guid("guid00") notifier.add_token(testaddr, "56789")
assert notifier.get_token("guid00") == "01234" notifier.new_message_for_addr(testaddr)
notifier.thread_run_one(ReqMock()) notifier.thread_run_one(ReqMock())
url, data, timeout = requests[0] url, data, timeout = requests[0]
assert data == "01234" assert data == "01234"
assert notifier.get_token("guid00") is None url, data, timeout = requests[1]
assert data == "56789"
assert notifier.get_tokens(testaddr) == ["01234", "56789"]
def test_notifier_thread_run_gone_removes_token(notifier, testaddr):
requests = []
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]
assert data == "01234"
url, data, timeout = requests[1]
assert data == "45678"
assert notifier.get_tokens(testaddr) == ["45678"]

View File

@@ -19,6 +19,7 @@ dependencies = [
"black", "black",
"pytest", "pytest",
"pytest-xdist", "pytest-xdist",
"imap_tools",
] ]
[project.scripts] [project.scripts]

View File

@@ -108,6 +108,7 @@ def _install_remote_venv_with_chatmaild(config) -> None:
execpath=f"{remote_venv_dir}/bin/{fn}", execpath=f"{remote_venv_dir}/bin/{fn}",
config_path=remote_chatmail_inipath, config_path=remote_chatmail_inipath,
remote_venv_dir=remote_venv_dir, remote_venv_dir=remote_venv_dir,
mail_domain=config.mail_domain,
) )
source_path = importlib.resources.files("chatmaild").joinpath(f"{fn}.service.f") source_path = importlib.resources.files("chatmaild").joinpath(f"{fn}.service.f")
content = source_path.read_text().format(**params).encode() content = source_path.read_text().format(**params).encode()

View File

@@ -5,6 +5,46 @@ import random
import pytest import pytest
import requests import requests
import ipaddress import ipaddress
import imap_tools
@pytest.fixture
def imap_mailbox(cmfactory):
(ac1,) = cmfactory.get_online_accounts(1)
user = ac1.get_config("addr")
password = ac1.get_config("mail_pw")
mailbox = imap_tools.MailBox(user.split("@")[1])
mailbox.login(user, password)
return mailbox
class TestMetadataTokens:
"Tests that use Metadata extension for storing tokens"
def test_set_get_metadata(self, imap_mailbox):
"set and get metadata token for an account"
client = imap_mailbox.client
client.send(b'a01 SETMETADATA INBOX (/private/devicetoken "1111" )\n')
res = client.readline()
assert b"OK Setmetadata completed" in res
client.send(b"a02 GETMETADATA INBOX /private/devicetoken\n")
res = client.readline()
assert res[:1] == b"*"
res = client.readline().strip().rstrip(b")")
assert res == b"1111"
assert b"Getmetadata completed" in client.readline()
client.send(b'a01 SETMETADATA INBOX (/private/devicetoken "2222" )\n')
res = client.readline()
assert b"OK Setmetadata completed" in res
client.send(b"a02 GETMETADATA INBOX /private/devicetoken\n")
res = client.readline()
assert res[:1] == b"*"
res = client.readline().strip().rstrip(b")")
assert res == b"1111 2222"
assert b"Getmetadata completed" in client.readline()
class TestEndToEndDeltaChat: class TestEndToEndDeltaChat:
@@ -75,7 +115,10 @@ class TestEndToEndDeltaChat:
) )
lp.indent("good, message sending failed because quota was exceeded") lp.indent("good, message sending failed because quota was exceeded")
return return
if "stored mail into mailbox 'inbox'" in line or "saved mail to inbox" in line: if (
"stored mail into mailbox 'inbox'" in line
or "saved mail to inbox" in line
):
saved_ok += 1 saved_ok += 1
print(f"{saved_ok}: {line}") print(f"{saved_ok}: {line}")
if saved_ok >= num_to_send: if saved_ok >= num_to_send: