mirror of
https://github.com/chatmail/relay.git
synced 2026-05-13 17:34:38 +00:00
Compare commits
1 Commits
hagi/metri
...
link2xt/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5591920cdc |
5
.github/workflows/test-and-deploy.yaml
vendored
5
.github/workflows/test-and-deploy.yaml
vendored
@@ -7,9 +7,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'scripts/**'
|
- 'scripts/**'
|
||||||
- '**/README.md'
|
|
||||||
- 'CHANGELOG.md'
|
|
||||||
- 'LICENSE'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -59,7 +56,7 @@ jobs:
|
|||||||
# restore acme & dkim state to staging.testrun.org
|
# restore acme & dkim state to staging.testrun.org
|
||||||
rsync -avz acme-restore/acme/ root@staging.testrun.org:/var/lib/acme || true
|
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
|
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
|
ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org chown root:root -R /var/lib/acme
|
||||||
|
|
||||||
- name: run formatting checks
|
- name: run formatting checks
|
||||||
run: cmdeploy fmt -v
|
run: cmdeploy fmt -v
|
||||||
|
|||||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -2,33 +2,6 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
- run metrics generation with systemd-timer instead of cron
|
|
||||||
([#304](https://github.com/deltachat/chatmail/pull/304))
|
|
||||||
|
|
||||||
- change default for delete_mails_after from 40 to 20 days
|
|
||||||
([#300]https://github.com/deltachat/chatmail/pull/300)
|
|
||||||
|
|
||||||
- fix writing of multiple obs repositories in `/etc/apt/sources.list`
|
|
||||||
([#272](https://github.com/deltachat/chatmail/issues/272))
|
|
||||||
|
|
||||||
- metadata: add support for `/shared/vendor/deltachat/irohrelay`
|
|
||||||
([#284](https://github.com/deltachat/chatmail/pull/284))
|
|
||||||
|
|
||||||
- Emit "XCHATMAIL" capability from IMAP server
|
|
||||||
([#278](https://github.com/deltachat/chatmail/pull/278))
|
|
||||||
|
|
||||||
- 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
|
## 1.2.0 - 2024-04-04
|
||||||
|
|
||||||
- Install dig on the server to resolve DNS records
|
- Install dig on the server to resolve DNS records
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ after which the initially specified password is required for using them.
|
|||||||
|
|
||||||
## Deploying your own chatmail server
|
## Deploying your own chatmail server
|
||||||
|
|
||||||
To deploy chatmail on your own server, you must have set-up ssh authentication and need to use an ed25519 key, due to an [upstream bug in paramiko](https://github.com/paramiko/paramiko/issues/2191). You also need to add your private key to the local ssh-agent, because you can't type in your password during deployment.
|
|
||||||
|
|
||||||
We use `chat.example.org` as the chatmail domain in the following steps.
|
We use `chat.example.org` as the chatmail domain in the following steps.
|
||||||
Please substitute it with your own domain.
|
Please substitute it with your own domain.
|
||||||
|
|
||||||
|
|||||||
@@ -36,16 +36,6 @@ log_format = "%(asctime)s %(levelname)s %(message)s"
|
|||||||
log_date_format = "%Y-%m-%d %H:%M:%S"
|
log_date_format = "%Y-%m-%d %H:%M:%S"
|
||||||
log_level = "INFO"
|
log_level = "INFO"
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
lint.select = [
|
|
||||||
"F", # Pyflakes
|
|
||||||
"I", # isort
|
|
||||||
|
|
||||||
"PLC", # Pylint Convention
|
|
||||||
"PLE", # Pylint Error
|
|
||||||
"PLW", # Pylint Warning
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.tox]
|
[tool.tox]
|
||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
[tox]
|
[tox]
|
||||||
@@ -57,9 +47,10 @@ skipdist = True
|
|||||||
skip_install = True
|
skip_install = True
|
||||||
deps =
|
deps =
|
||||||
ruff
|
ruff
|
||||||
|
black
|
||||||
commands =
|
commands =
|
||||||
ruff format --quiet --diff src/
|
black --quiet --check --diff src/
|
||||||
ruff check src/
|
ruff src/
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps = pytest
|
deps = pytest
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class Config:
|
|||||||
self.passthrough_recipients = params["passthrough_recipients"].split()
|
self.passthrough_recipients = params["passthrough_recipients"].split()
|
||||||
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
|
||||||
self.postfix_reinject_port = int(params["postfix_reinject_port"])
|
self.postfix_reinject_port = int(params["postfix_reinject_port"])
|
||||||
self.iroh_relay = params.get("iroh_relay")
|
|
||||||
self.privacy_postal = params.get("privacy_postal")
|
self.privacy_postal = params.get("privacy_postal")
|
||||||
self.privacy_mail = params.get("privacy_mail")
|
self.privacy_mail = params.get("privacy_mail")
|
||||||
self.privacy_pdo = params.get("privacy_pdo")
|
self.privacy_pdo = params.get("privacy_pdo")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import contextlib
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
import contextlib
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
import crypt
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
import sys
|
||||||
|
import json
|
||||||
|
import crypt
|
||||||
from socketserver import (
|
from socketserver import (
|
||||||
|
UnixStreamServer,
|
||||||
StreamRequestHandler,
|
StreamRequestHandler,
|
||||||
ThreadingMixIn,
|
ThreadingMixIn,
|
||||||
UnixStreamServer,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from .config import Config, read_config
|
|
||||||
from .database import Database
|
from .database import Database
|
||||||
|
from .config import read_config, Config
|
||||||
|
|
||||||
NOCREATE_FILE = "/etc/chatmail-nocreate"
|
NOCREATE_FILE = "/etc/chatmail-nocreate"
|
||||||
|
|
||||||
@@ -46,32 +45,23 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
|
|||||||
return False
|
return False
|
||||||
localpart, domain = parts
|
localpart, domain = parts
|
||||||
|
|
||||||
if localpart == "echo":
|
|
||||||
# echobot account should not be created in the database
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
len(localpart) > config.username_max_length
|
len(localpart) > config.username_max_length
|
||||||
or len(localpart) < config.username_min_length
|
or len(localpart) < config.username_min_length
|
||||||
):
|
):
|
||||||
logging.warning(
|
if localpart != "echo":
|
||||||
"localpart %s has to be between %s and %s chars long",
|
logging.warning(
|
||||||
localpart,
|
"localpart %s has to be between %s and %s chars long",
|
||||||
config.username_min_length,
|
localpart,
|
||||||
config.username_max_length,
|
config.username_min_length,
|
||||||
)
|
config.username_max_length,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_user_data(db, config: Config, user):
|
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:
|
with db.read_connection() as conn:
|
||||||
result = conn.get_user(user)
|
result = conn.get_user(user)
|
||||||
if result:
|
if result:
|
||||||
@@ -86,21 +76,6 @@ def lookup_userdb(db, config: Config, user):
|
|||||||
|
|
||||||
|
|
||||||
def lookup_passdb(db, config: Config, user, cleartext_password):
|
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:
|
with db.write_transaction() as conn:
|
||||||
userdata = conn.get_user(user)
|
userdata = conn.get_user(user)
|
||||||
if userdata:
|
if userdata:
|
||||||
|
|||||||
@@ -3,17 +3,14 @@
|
|||||||
|
|
||||||
it will echo back any message that has non-empty text and also supports the /help command.
|
it will echo back any message that has non-empty text and also supports the /help command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||||
|
|
||||||
from chatmaild.config import read_config
|
|
||||||
from chatmaild.newemail import create_newemail_dict
|
from chatmaild.newemail import create_newemail_dict
|
||||||
|
from chatmaild.config import read_config
|
||||||
|
|
||||||
hooks = events.HookCollection()
|
hooks = events.HookCollection()
|
||||||
|
|
||||||
@@ -78,23 +75,9 @@ def main():
|
|||||||
account = accounts[0] if accounts else deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
bot = Bot(account, hooks)
|
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():
|
if not bot.is_configured():
|
||||||
|
config = read_config(sys.argv[1])
|
||||||
|
password = create_newemail_dict(config).get("password")
|
||||||
email = "echo@" + config.mail_domain
|
email = "echo@" + config.mail_domain
|
||||||
bot.configure(email, password)
|
bot.configure(email, password)
|
||||||
bot.run_forever()
|
bot.run_forever()
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
from contextlib import contextmanager
|
import logging
|
||||||
|
import json
|
||||||
import filelock
|
import filelock
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
class FileDict:
|
class FileDict:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from email import policy
|
import sys
|
||||||
from email.parser import BytesParser
|
from email.parser import BytesParser
|
||||||
|
from email import policy
|
||||||
from email.utils import parseaddr
|
from email.utils import parseaddr
|
||||||
from smtplib import SMTP as SMTPClient
|
|
||||||
|
|
||||||
from aiosmtpd.controller import Controller
|
from aiosmtpd.controller import Controller
|
||||||
|
from smtplib import SMTP as SMTPClient
|
||||||
|
|
||||||
from .config import read_config
|
from .config import read_config
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ max_user_send_per_minute = 60
|
|||||||
max_mailbox_size = 100M
|
max_mailbox_size = 100M
|
||||||
|
|
||||||
# days after which mails are unconditionally deleted
|
# days after which mails are unconditionally deleted
|
||||||
delete_mails_after = 20
|
delete_mails_after = 40
|
||||||
|
|
||||||
# minimum length a username must have
|
# minimum length a username must have
|
||||||
username_min_length = 9
|
username_min_length = 9
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socketserver import (
|
from socketserver import (
|
||||||
|
UnixStreamServer,
|
||||||
StreamRequestHandler,
|
StreamRequestHandler,
|
||||||
ThreadingMixIn,
|
ThreadingMixIn,
|
||||||
UnixStreamServer,
|
|
||||||
)
|
)
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from .config import read_config
|
|
||||||
from .filedict import FileDict
|
from .filedict import FileDict
|
||||||
from .notifier import Notifier
|
from .notifier import Notifier
|
||||||
|
|
||||||
|
|
||||||
DICTPROXY_HELLO_CHAR = "H"
|
DICTPROXY_HELLO_CHAR = "H"
|
||||||
DICTPROXY_LOOKUP_CHAR = "L"
|
DICTPROXY_LOOKUP_CHAR = "L"
|
||||||
DICTPROXY_ITERATE_CHAR = "I"
|
DICTPROXY_ITERATE_CHAR = "I"
|
||||||
@@ -49,40 +49,32 @@ class Metadata:
|
|||||||
return mdict.get(self.DEVICETOKEN_KEY, [])
|
return mdict.get(self.DEVICETOKEN_KEY, [])
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_protocol(rfile, wfile, notifier, metadata, iroh_relay=None):
|
def handle_dovecot_protocol(rfile, wfile, notifier, metadata):
|
||||||
transactions = {}
|
transactions = {}
|
||||||
while True:
|
while True:
|
||||||
msg = rfile.readline().strip().decode()
|
msg = rfile.readline().strip().decode()
|
||||||
if not msg:
|
if not msg:
|
||||||
break
|
break
|
||||||
|
|
||||||
res = handle_dovecot_request(msg, transactions, notifier, metadata, iroh_relay)
|
res = handle_dovecot_request(msg, transactions, notifier, metadata)
|
||||||
if res:
|
if res:
|
||||||
wfile.write(res.encode("ascii"))
|
wfile.write(res.encode("ascii"))
|
||||||
wfile.flush()
|
wfile.flush()
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_request(msg, transactions, notifier, metadata, iroh_relay=None):
|
def handle_dovecot_request(msg, transactions, notifier, metadata):
|
||||||
# see https://doc.dovecot.org/3.0/developer_manual/design/dict_protocol/
|
# see https://doc.dovecot.org/3.0/developer_manual/design/dict_protocol/
|
||||||
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
|
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
||||||
keyparts = parts[0].split("/", 2)
|
keyparts = parts[0].split("/")
|
||||||
if keyparts[0] == "priv":
|
if keyparts[0] == "priv":
|
||||||
keyname = keyparts[2]
|
keyname = keyparts[2]
|
||||||
addr = parts[1]
|
addr = parts[1]
|
||||||
if keyname == metadata.DEVICETOKEN_KEY:
|
if keyname == metadata.DEVICETOKEN_KEY:
|
||||||
res = " ".join(metadata.get_tokens_for_addr(addr))
|
res = " ".join(metadata.get_tokens_for_addr(addr))
|
||||||
return f"O{res}\n"
|
return f"O{res}\n"
|
||||||
elif keyparts[0] == "shared":
|
|
||||||
keyname = keyparts[2]
|
|
||||||
if (
|
|
||||||
keyname == "vendor/vendor.dovecot/pvt/server/vendor/deltachat/irohrelay"
|
|
||||||
and iroh_relay
|
|
||||||
):
|
|
||||||
# Handle `GETMETADATA "" /shared/vendor/deltachat/irohrelay`
|
|
||||||
return f"O{iroh_relay}\n"
|
|
||||||
logging.warning("lookup ignored: %r", msg)
|
logging.warning("lookup ignored: %r", msg)
|
||||||
return "N\n"
|
return "N\n"
|
||||||
elif short_command == DICTPROXY_ITERATE_CHAR:
|
elif short_command == DICTPROXY_ITERATE_CHAR:
|
||||||
@@ -128,10 +120,7 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
socket, vmail_dir, config_path = sys.argv[1:]
|
socket, vmail_dir = sys.argv[1:]
|
||||||
|
|
||||||
config = read_config(config_path)
|
|
||||||
iroh_relay = config.iroh_relay
|
|
||||||
|
|
||||||
vmail_dir = Path(vmail_dir)
|
vmail_dir = Path(vmail_dir)
|
||||||
if not vmail_dir.exists():
|
if not vmail_dir.exists():
|
||||||
@@ -147,9 +136,7 @@ def main():
|
|||||||
class Handler(StreamRequestHandler):
|
class Handler(StreamRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
try:
|
try:
|
||||||
handle_dovecot_protocol(
|
handle_dovecot_protocol(self.rfile, self.wfile, notifier, metadata)
|
||||||
self.rfile, self.wfile, notifier, metadata, iroh_relay
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Exception in the dovecot dictproxy handler")
|
logging.exception("Exception in the dovecot dictproxy handler")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main(vmail_dir=None):
|
def main(vmail_dir=None):
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/usr/local/lib/chatmaild/venv/bin/python3
|
#!/usr/local/lib/chatmaild/venv/bin/python3
|
||||||
|
|
||||||
"""CGI script for creating new accounts."""
|
""" CGI script for creating new accounts. """
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from chatmaild.config import Config, read_config
|
from chatmaild.config import read_config, Config
|
||||||
|
|
||||||
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
|
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
|
||||||
ALPHANUMERIC = string.ascii_lowercase + string.digits
|
ALPHANUMERIC = string.ascii_lowercase + string.digits
|
||||||
|
|||||||
@@ -25,16 +25,15 @@ The meaning and format of tokens is basically a matter of Delta-Chat Core and
|
|||||||
the `notification.delta.chat` service.
|
the `notification.delta.chat` service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
import math
|
||||||
|
import logging
|
||||||
|
from uuid import uuid4
|
||||||
|
from threading import Thread
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import PriorityQueue
|
from queue import PriorityQueue
|
||||||
from threading import Thread
|
from dataclasses import dataclass
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
|
||||||
import random
|
|
||||||
from email import policy
|
|
||||||
from email.parser import BytesParser
|
from email.parser import BytesParser
|
||||||
from pathlib import Path
|
from email import policy
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from chatmaild.config import read_config, write_initial_config
|
|
||||||
from chatmaild.database import Database
|
from chatmaild.database import Database
|
||||||
|
from chatmaild.config import read_config, write_initial_config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def test_read_config_testrun(make_config):
|
|||||||
assert config.postfix_reinject_port == 10025
|
assert config.postfix_reinject_port == 10025
|
||||||
assert config.max_user_send_per_minute == 60
|
assert config.max_user_send_per_minute == 60
|
||||||
assert config.max_mailbox_size == "100M"
|
assert config.max_mailbox_size == "100M"
|
||||||
assert config.delete_mails_after == "20"
|
assert config.delete_mails_after == "40"
|
||||||
assert config.username_min_length == 9
|
assert config.username_min_length == 9
|
||||||
assert config.username_max_length == 9
|
assert config.username_max_length == 9
|
||||||
assert config.password_min_length == 9
|
assert config.password_min_length == 9
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import pytest
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import chatmaild.doveauth
|
import chatmaild.doveauth
|
||||||
import pytest
|
|
||||||
from chatmaild.database import DBError
|
|
||||||
from chatmaild.doveauth import (
|
from chatmaild.doveauth import (
|
||||||
get_user_data,
|
get_user_data,
|
||||||
handle_dovecot_protocol,
|
|
||||||
handle_dovecot_request,
|
|
||||||
lookup_passdb,
|
lookup_passdb,
|
||||||
|
handle_dovecot_request,
|
||||||
|
handle_dovecot_protocol,
|
||||||
)
|
)
|
||||||
|
from chatmaild.database import DBError
|
||||||
|
|
||||||
|
|
||||||
def test_basic(db, example_config):
|
def test_basic(db, example_config):
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import pytest
|
|
||||||
from chatmaild.filtermail import (
|
from chatmaild.filtermail import (
|
||||||
|
check_encrypted,
|
||||||
BeforeQueueHandler,
|
BeforeQueueHandler,
|
||||||
SendRateLimiter,
|
SendRateLimiter,
|
||||||
check_encrypted,
|
|
||||||
check_mdn,
|
check_mdn,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def maildomain():
|
def maildomain():
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import io
|
import io
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
from chatmaild.metadata import (
|
from chatmaild.metadata import (
|
||||||
Metadata,
|
|
||||||
handle_dovecot_protocol,
|
|
||||||
handle_dovecot_request,
|
handle_dovecot_request,
|
||||||
|
handle_dovecot_protocol,
|
||||||
|
Metadata,
|
||||||
)
|
)
|
||||||
from chatmaild.notifier import (
|
from chatmaild.notifier import (
|
||||||
Notifier,
|
Notifier,
|
||||||
@@ -296,17 +296,3 @@ def test_persistent_queue_items(tmp_path, testaddr, token):
|
|||||||
item2.delete()
|
item2.delete()
|
||||||
assert not item2.path.exists()
|
assert not item2.path.exists()
|
||||||
assert not queue_item < item2 and not item2 < queue_item
|
assert not queue_item < item2 and not item2 < queue_item
|
||||||
|
|
||||||
|
|
||||||
def test_iroh_relay(metadata):
|
|
||||||
rfile = io.BytesIO(
|
|
||||||
b"\n".join(
|
|
||||||
[
|
|
||||||
b"H",
|
|
||||||
b"Lshared/0123/vendor/vendor.dovecot/pvt/server/vendor/deltachat/irohrelay\tuser@example.org",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
wfile = io.BytesIO()
|
|
||||||
handle_dovecot_protocol(rfile, wfile, notifier, metadata, "https://example.org/")
|
|
||||||
assert wfile.getvalue() == b"Ohttps://example.org/\n"
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ dependencies = [
|
|||||||
"build",
|
"build",
|
||||||
"tox",
|
"tox",
|
||||||
"ruff",
|
"ruff",
|
||||||
|
"black",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
@@ -30,13 +31,3 @@ cmdeploy = "cmdeploy.cmdeploy:main"
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-v -ra --strict-markers"
|
addopts = "-v -ra --strict-markers"
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
lint.select = [
|
|
||||||
"F", # Pyflakes
|
|
||||||
"I", # isort
|
|
||||||
|
|
||||||
"PLC", # Pylint Convention
|
|
||||||
"PLE", # Pylint Error
|
|
||||||
"PLW", # Pylint Warning
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -2,22 +2,21 @@
|
|||||||
Chat Mail pyinfra deploy.
|
Chat Mail pyinfra deploy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import importlib.resources
|
|
||||||
import io
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
|
import importlib.resources
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import io
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from chatmaild.config import Config, read_config
|
|
||||||
from pyinfra import host
|
from pyinfra import host
|
||||||
|
from pyinfra.operations import apt, files, server, systemd, pip
|
||||||
from pyinfra.facts.files import File
|
from pyinfra.facts.files import File
|
||||||
from pyinfra.facts.systemd import SystemdEnabled
|
from pyinfra.facts.systemd import SystemdEnabled
|
||||||
from pyinfra.operations import apt, files, pip, server, systemd
|
|
||||||
|
|
||||||
from .acmetool import deploy_acmetool
|
from .acmetool import deploy_acmetool
|
||||||
|
|
||||||
root_owned = dict(user="root", group="root", mode="644")
|
from chatmaild.config import read_config, Config
|
||||||
|
|
||||||
|
|
||||||
def _build_chatmaild(dist_dir) -> None:
|
def _build_chatmaild(dist_dir) -> None:
|
||||||
dist_dir = Path(dist_dir).resolve()
|
dist_dir = Path(dist_dir).resolve()
|
||||||
@@ -51,6 +50,7 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
|||||||
remote_dist_file = f"{remote_base_dir}/dist/{dist_file.name}"
|
remote_dist_file = f"{remote_base_dir}/dist/{dist_file.name}"
|
||||||
remote_venv_dir = f"{remote_base_dir}/venv"
|
remote_venv_dir = f"{remote_base_dir}/venv"
|
||||||
remote_chatmail_inipath = f"{remote_base_dir}/chatmail.ini"
|
remote_chatmail_inipath = f"{remote_base_dir}/chatmail.ini"
|
||||||
|
root_owned = dict(user="root", group="root", mode="644")
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="apt install python3-virtualenv",
|
name="apt install python3-virtualenv",
|
||||||
@@ -85,19 +85,9 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# create metrics every 5 minutes via systemd
|
|
||||||
|
|
||||||
files.put(
|
|
||||||
name="Upload metrics.timer",
|
|
||||||
src=importlib.resources.files(__package__).joinpath("service/metrics.timer"),
|
|
||||||
dest=f"/etc/systemd/system/metrics.timer",
|
|
||||||
**root_owned,
|
|
||||||
)
|
|
||||||
|
|
||||||
files.template(
|
files.template(
|
||||||
name="upload metrics.service",
|
src=importlib.resources.files(__package__).joinpath("metrics.cron.j2"),
|
||||||
src=importlib.resources.files(__package__).joinpath("service/metrics.service.j2"),
|
dest="/etc/cron.d/chatmail-metrics",
|
||||||
dest="/etc/systemd/system/metrics.service",
|
|
||||||
user="root",
|
user="root",
|
||||||
group="root",
|
group="root",
|
||||||
mode="644",
|
mode="644",
|
||||||
@@ -107,15 +97,6 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
systemd.service(
|
|
||||||
name=f"Setup metrics timer",
|
|
||||||
service="metrics.timer",
|
|
||||||
running=True,
|
|
||||||
enabled=True,
|
|
||||||
restarted=True,
|
|
||||||
daemon_reload=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# install systemd units
|
# install systemd units
|
||||||
for fn in (
|
for fn in (
|
||||||
"doveauth",
|
"doveauth",
|
||||||
@@ -371,23 +352,6 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
|
|||||||
commands=["/usr/bin/sievec /etc/dovecot/default.sieve"],
|
commands=["/usr/bin/sievec /etc/dovecot/default.sieve"],
|
||||||
)
|
)
|
||||||
|
|
||||||
files.template(
|
|
||||||
src=importlib.resources.files(__package__).joinpath("service/expunge.service.j2"),
|
|
||||||
dest="/etc/systemd/system/expunge.service",
|
|
||||||
config={
|
|
||||||
"mail_domain": config.mail_domain,
|
|
||||||
"delete_mails_after": config.delete_mails_after,
|
|
||||||
},
|
|
||||||
**root_owned,
|
|
||||||
)
|
|
||||||
|
|
||||||
files.put(
|
|
||||||
name="Upload expunge.timer",
|
|
||||||
src=importlib.resources.files(__package__).joinpath("service/expunge.timer"),
|
|
||||||
dest=f"/etc/systemd/system/expunge.timer",
|
|
||||||
**root_owned,
|
|
||||||
)
|
|
||||||
|
|
||||||
files.template(
|
files.template(
|
||||||
src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"),
|
src=importlib.resources.files(__package__).joinpath("dovecot/expunge.cron.j2"),
|
||||||
dest="/etc/cron.d/expunge",
|
dest="/etc/cron.d/expunge",
|
||||||
@@ -513,31 +477,12 @@ def deploy_chatmail(config_path: Path) -> None:
|
|||||||
groups=["opendkim"],
|
groups=["opendkim"],
|
||||||
system=True,
|
system=True,
|
||||||
)
|
)
|
||||||
server.user(name="Create echobot user", user="echobot", system=True)
|
|
||||||
|
|
||||||
server.shell(
|
server.shell(
|
||||||
name="Fix file owner in /home/vmail",
|
name="Fix file owner in /home/vmail",
|
||||||
commands=["test -d /home/vmail && chown -R vmail:vmail /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/ ./",
|
|
||||||
escape_regex_characters=True,
|
|
||||||
ensure_newline=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
apt.update(name="apt update", cache_time=24 * 3600)
|
apt.update(name="apt update", cache_time=24 * 3600)
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
@@ -559,7 +504,6 @@ def deploy_chatmail(config_path: Path) -> None:
|
|||||||
"systemctl reset-failed unbound.service",
|
"systemctl reset-failed unbound.service",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
systemd.service(
|
systemd.service(
|
||||||
name="Start and enable unbound",
|
name="Start and enable unbound",
|
||||||
service="unbound.service",
|
service="unbound.service",
|
||||||
@@ -569,15 +513,10 @@ def deploy_chatmail(config_path: Path) -> None:
|
|||||||
|
|
||||||
# Deploy acmetool to have TLS certificates.
|
# Deploy acmetool to have TLS certificates.
|
||||||
deploy_acmetool(
|
deploy_acmetool(
|
||||||
|
nginx_hook=True,
|
||||||
domains=[mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"],
|
domains=[mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"],
|
||||||
)
|
)
|
||||||
|
|
||||||
apt.packages(
|
|
||||||
# required for setfacl for echobot
|
|
||||||
name="Install acl",
|
|
||||||
packages="acl",
|
|
||||||
)
|
|
||||||
|
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install Postfix",
|
name="Install Postfix",
|
||||||
packages="postfix",
|
packages="postfix",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import importlib.resources
|
import importlib.resources
|
||||||
|
|
||||||
|
from pyinfra.operations import apt, files, systemd, server
|
||||||
from pyinfra import host
|
from pyinfra import host
|
||||||
from pyinfra.facts.systemd import SystemdStatus
|
from pyinfra.facts.systemd import SystemdStatus
|
||||||
from pyinfra.operations import apt, files, server, systemd
|
|
||||||
|
|
||||||
|
|
||||||
def deploy_acmetool(email="", domains=[]):
|
def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
||||||
"""Deploy acmetool."""
|
"""Deploy acmetool."""
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install acmetool",
|
name="Install acmetool",
|
||||||
@@ -20,13 +20,16 @@ def deploy_acmetool(email="", domains=[]):
|
|||||||
mode="644",
|
mode="644",
|
||||||
)
|
)
|
||||||
|
|
||||||
files.put(
|
if nginx_hook:
|
||||||
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
|
files.put(
|
||||||
dest="/usr/lib/acme/hooks/nginx",
|
src=importlib.resources.files(__package__)
|
||||||
user="root",
|
.joinpath("acmetool.hook")
|
||||||
group="root",
|
.open("rb"),
|
||||||
mode="744",
|
dest="/usr/lib/acme/hooks/nginx",
|
||||||
)
|
user="root",
|
||||||
|
group="root",
|
||||||
|
mode="744",
|
||||||
|
)
|
||||||
|
|
||||||
files.template(
|
files.template(
|
||||||
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
|
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
|
||||||
@@ -69,8 +72,7 @@ def deploy_acmetool(email="", domains=[]):
|
|||||||
restarted=service_file.changed,
|
restarted=service_file.changed,
|
||||||
)
|
)
|
||||||
|
|
||||||
if str(host) != "staging.testrun.org":
|
server.shell(
|
||||||
server.shell(
|
name=f"Request certificate for: { ', '.join(domains) }",
|
||||||
name=f"Request certificate for: { ', '.join(domains) }",
|
commands=[f"acmetool want { ' '.join(domains)}"],
|
||||||
commands=[f"acmetool want --xlog.severity=debug { ' '.join(domains)}"],
|
)
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,5 +3,3 @@ set -e
|
|||||||
EVENT_NAME="$1"
|
EVENT_NAME="$1"
|
||||||
[ "$EVENT_NAME" = "live-updated" ] || exit 42
|
[ "$EVENT_NAME" = "live-updated" ] || exit 42
|
||||||
systemctl restart nginx.service
|
systemctl restart nginx.service
|
||||||
systemctl reload dovecot.service
|
|
||||||
systemctl reload postfix.service
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
"acme-enter-email": "{{ email }}"
|
"acme-enter-email": "{{ email }}"
|
||||||
"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf": true
|
"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf": true
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ along with command line option and subcommand parsing.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from chatmaild.config import read_config, write_initial_config
|
|
||||||
from termcolor import colored
|
|
||||||
|
|
||||||
from cmdeploy.dns import check_necessary_dns, show_dns
|
from termcolor import colored
|
||||||
|
from chatmaild.config import read_config, write_initial_config
|
||||||
|
from cmdeploy.dns import show_dns, check_necessary_dns
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# cmdeploy sub commands and options
|
# cmdeploy sub commands and options
|
||||||
@@ -156,26 +157,26 @@ def fmt_cmd_options(parser):
|
|||||||
|
|
||||||
|
|
||||||
def fmt_cmd(args, out):
|
def fmt_cmd(args, out):
|
||||||
"""Run formattting fixes on all chatmail source code."""
|
"""Run formattting fixes (ruff and black) on all chatmail source code."""
|
||||||
|
|
||||||
sources = [str(importlib.resources.files(x)) for x in ("chatmaild", "cmdeploy")]
|
sources = [str(importlib.resources.files(x)) for x in ("chatmaild", "cmdeploy")]
|
||||||
format_args = [shutil.which("ruff"), "format"]
|
black_args = [shutil.which("black")]
|
||||||
check_args = [shutil.which("ruff"), "check"]
|
ruff_args = [shutil.which("ruff")]
|
||||||
|
|
||||||
if args.check:
|
if args.check:
|
||||||
format_args.append("--diff")
|
black_args.append("--check")
|
||||||
else:
|
else:
|
||||||
check_args.append("--fix")
|
ruff_args.append("--fix")
|
||||||
|
|
||||||
if not args.verbose:
|
if not args.verbose:
|
||||||
check_args.append("--quiet")
|
black_args.append("-q")
|
||||||
format_args.append("--quiet")
|
ruff_args.append("-q")
|
||||||
|
|
||||||
format_args.extend(sources)
|
black_args.extend(sources)
|
||||||
check_args.extend(sources)
|
ruff_args.extend(sources)
|
||||||
|
|
||||||
out.check_call(" ".join(format_args), quiet=not args.verbose)
|
out.check_call(" ".join(black_args), quiet=not args.verbose)
|
||||||
out.check_call(" ".join(check_args), quiet=not args.verbose)
|
out.check_call(" ".join(ruff_args), quiet=not args.verbose)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ class Out:
|
|||||||
if not quiet:
|
if not quiet:
|
||||||
cmdstring = " ".join(args)
|
cmdstring = " ".join(args)
|
||||||
self(f"[$ {cmdstring}]", file=sys.stderr)
|
self(f"[$ {cmdstring}]", file=sys.stderr)
|
||||||
proc = subprocess.run(args, env=env, check=False)
|
proc = subprocess.run(args, env=env)
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import importlib.resources
|
|
||||||
import os
|
import os
|
||||||
|
import importlib.resources
|
||||||
import pyinfra
|
import pyinfra
|
||||||
|
|
||||||
from cmdeploy import deploy_chatmail
|
from cmdeploy import deploy_chatmail
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import datetime
|
|
||||||
import importlib
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class DNS:
|
class DNS:
|
||||||
@@ -104,8 +104,8 @@ def show_dns(args, out) -> int:
|
|||||||
return 0
|
return 0
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
for raw_line in zonefile.splitlines():
|
for line in zonefile.splitlines():
|
||||||
line = raw_line.format(
|
line = line.format(
|
||||||
acme_account_url=acme_account_url,
|
acme_account_url=acme_account_url,
|
||||||
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
|
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
|
||||||
chatmail_domain=args.config.mail_domain,
|
chatmail_domain=args.config.mail_domain,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ mail_plugins = quota
|
|||||||
# these are the capabilities Delta Chat cares about actually
|
# these are the capabilities Delta Chat cares about actually
|
||||||
# so let's keep the network overhead per login small
|
# so let's keep the network overhead per login small
|
||||||
# https://github.com/deltachat/deltachat-core-rust/blob/master/src/imap/capabilities.rs
|
# https://github.com/deltachat/deltachat-core-rust/blob/master/src/imap/capabilities.rs
|
||||||
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY METADATA XDELTAPUSH XCHATMAIL
|
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY METADATA XDELTAPUSH
|
||||||
|
|
||||||
|
|
||||||
# Authentication for system users.
|
# Authentication for system users.
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import io
|
|
||||||
import os
|
|
||||||
|
|
||||||
import qrcode
|
import qrcode
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
import os
|
||||||
|
from PIL import ImageFont, ImageDraw, Image
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
def gen_qr_png_data(maildomain):
|
def gen_qr_png_data(maildomain):
|
||||||
|
|||||||
Binary file not shown.
@@ -2,7 +2,7 @@
|
|||||||
Description=Chatmail dict proxy for IMAP METADATA
|
Description=Chatmail dict proxy for IMAP METADATA
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart={execpath} /run/chatmail-metadata/metadata.socket /home/vmail/mail/{mail_domain} {config_path}
|
ExecStart={execpath} /run/chatmail-metadata/metadata.socket /home/vmail/mail/{mail_domain}
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
User=vmail
|
User=vmail
|
||||||
|
|||||||
@@ -7,20 +7,6 @@ Environment="PATH={remote_venv_dir}:$PATH"
|
|||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=30
|
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
|
# Apply security restrictions suggested by
|
||||||
# systemd-analyze security echobot.service
|
# systemd-analyze security echobot.service
|
||||||
CapabilityBoundingSet=
|
CapabilityBoundingSet=
|
||||||
@@ -30,10 +16,7 @@ NoNewPrivileges=true
|
|||||||
PrivateDevices=true
|
PrivateDevices=true
|
||||||
PrivateMounts=true
|
PrivateMounts=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
PrivateUsers=true
|
||||||
# We need to know about doveauth user to give it access to /run/echobot/password
|
|
||||||
PrivateUsers=false
|
|
||||||
|
|
||||||
ProtectClock=true
|
ProtectClock=true
|
||||||
ProtectControlGroups=true
|
ProtectControlGroups=true
|
||||||
ProtectHostname=true
|
ProtectHostname=true
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Expunge old mails after {{ config.delete_mails_after }} days
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
# delete all mails after {{ config.delete_mails_after }} days, in the Inbox
|
|
||||||
ExecStart=/home/vmail/mail/{{ config.mail_domain }} -path '*/cur/*' -mtime +{{ config.delete_mails_after }} -type f -delete
|
|
||||||
# or in any IMAP subfolder
|
|
||||||
ExecStart=vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/.*/cur/*' -mtime +{{ config.delete_mails_after }} -type f -delete
|
|
||||||
# even if they are unseen
|
|
||||||
ExecStart=vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/new/*' -mtime +{{ config.delete_mails_after }} -type f -delete
|
|
||||||
ExecStart=vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/new/*' -mtime +{{ config.delete_mails_after }} -type f -delete
|
|
||||||
# or only temporary (but then they shouldn't be around after {{ config.delete_mails_after }} days anyway).
|
|
||||||
ExecStart=vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/tmp/*' -mtime +{{ config.delete_mails_after }} -type f -delete
|
|
||||||
ExecStart=vmail find /home/vmail/mail/{{ config.mail_domain }} -path '*/.*/tmp/*' -mtime +{{ config.delete_mails_after }} -type f -delete
|
|
||||||
ExecStart=vmail find /home/vmail/mail/{{ config.mail_domain }} -name 'maildirsize' -type f -delete
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Run expunge.service daily
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=weekly
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Generate metrics in /var/www/html/metrics
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart={{ config.execpath }} /home/vmail/mail/{{ config.mail_domain }} > /var/www/html/metrics
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Run metrics.service every 5 minutes
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnBootSec=5min
|
|
||||||
OnUnitActiveSec=5min
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import pytest
|
||||||
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import socket
|
import socket
|
||||||
import threading
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
|
|
||||||
from cmdeploy.cmdeploy import main
|
from cmdeploy.cmdeploy import main
|
||||||
|
|
||||||
|
|
||||||
@@ -15,13 +14,6 @@ def test_init(tmp_path, maildomain):
|
|||||||
assert config.mail_domain == maildomain
|
assert config.mail_domain == maildomain
|
||||||
|
|
||||||
|
|
||||||
def test_capabilities(imap):
|
|
||||||
imap.connect()
|
|
||||||
capas = imap.conn.capabilities
|
|
||||||
assert "XCHATMAIL" in capas
|
|
||||||
assert "XDELTAPUSH" in capas
|
|
||||||
|
|
||||||
|
|
||||||
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
||||||
"""Test a) that an initial login creates a user automatically
|
"""Test a) that an initial login creates a user automatically
|
||||||
and b) verify we can also login a second time with the same password
|
and b) verify we can also login a second time with the same password
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import ipaddress
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
|
||||||
import imap_tools
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
import ipaddress
|
||||||
|
import imap_tools
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import imaplib
|
|
||||||
import io
|
|
||||||
import itertools
|
|
||||||
import os
|
import os
|
||||||
import random
|
import io
|
||||||
import smtplib
|
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import imaplib
|
||||||
|
import smtplib
|
||||||
|
import itertools
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from chatmaild.config import read_config
|
|
||||||
from chatmaild.database import Database
|
from chatmaild.database import Database
|
||||||
|
from chatmaild.config import read_config
|
||||||
|
|
||||||
|
|
||||||
conftestdir = Path(__file__).parent
|
conftestdir = Path(__file__).parent
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cmdeploy.cmdeploy import get_parser, main
|
from cmdeploy.cmdeploy import get_parser, main
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import hashlib
|
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
|
import webbrowser
|
||||||
|
import hashlib
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from chatmaild.config import read_config
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
|
||||||
from .genqr import gen_qr_png_data
|
from .genqr import gen_qr_png_data
|
||||||
|
from chatmaild.config import read_config
|
||||||
|
|
||||||
|
|
||||||
def snapshot_dir_stats(somedir):
|
def snapshot_dir_stats(somedir):
|
||||||
@@ -121,8 +120,7 @@ def main():
|
|||||||
print(f"watching {src_path} directory for changes")
|
print(f"watching {src_path} directory for changes")
|
||||||
|
|
||||||
changenum = 0
|
changenum = 0
|
||||||
count = 0
|
for count in range(0, 1000000):
|
||||||
while True:
|
|
||||||
newstats = snapshot_dir_stats(src_path)
|
newstats = snapshot_dir_stats(src_path)
|
||||||
if newstats == stats and count % 60 != 0:
|
if newstats == stats and count % 60 != 0:
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
Submodule scripts/dovecot/dovecot-build/dovecot deleted from 4b7f802ca1
Reference in New Issue
Block a user