mirror of
https://github.com/chatmail/relay.git
synced 2026-05-12 09:04:36 +00:00
Compare commits
22 Commits
link2xt/do
...
hagi/insta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345d3f9b35 | ||
|
|
2b5b06316d | ||
|
|
76b56d7b78 | ||
|
|
c1163228f6 | ||
|
|
8af825d7ea | ||
|
|
0a968aae93 | ||
|
|
879cffc056 | ||
|
|
462e92cca0 | ||
|
|
e1b1a945b1 | ||
|
|
0493e27312 | ||
|
|
e4f8c78efe | ||
|
|
e2cbf4e3e4 | ||
|
|
f35d98bb40 | ||
|
|
7ce1a5e841 | ||
|
|
0a72c2fba7 | ||
|
|
824f70f463 | ||
|
|
39f5f64998 | ||
|
|
1752803199 | ||
|
|
e372599ce7 | ||
|
|
ce9fb02a75 | ||
|
|
4526f5e772 | ||
|
|
616a42c8f3 |
2
.github/workflows/test-and-deploy.yaml
vendored
2
.github/workflows/test-and-deploy.yaml
vendored
@@ -56,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
|
ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org chown root:root -R /var/lib/acme || true
|
||||||
|
|
||||||
- name: run formatting checks
|
- name: run formatting checks
|
||||||
run: cmdeploy fmt -v
|
run: cmdeploy fmt -v
|
||||||
|
|||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -159,4 +159,27 @@ While this file is present, account creation will be blocked.
|
|||||||
Delta Chat apps will, however, discover all ports and configurations
|
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.
|
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.
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ 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]
|
||||||
@@ -47,10 +57,9 @@ skipdist = True
|
|||||||
skip_install = True
|
skip_install = True
|
||||||
deps =
|
deps =
|
||||||
ruff
|
ruff
|
||||||
black
|
|
||||||
commands =
|
commands =
|
||||||
black --quiet --check --diff src/
|
ruff format --quiet --diff src/
|
||||||
ruff src/
|
ruff check src/
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps = pytest
|
deps = pytest
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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 sqlite3
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
|
import crypt
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
import json
|
import time
|
||||||
import crypt
|
from pathlib import Path
|
||||||
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"
|
||||||
|
|
||||||
@@ -45,23 +46,32 @@ 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
|
||||||
):
|
):
|
||||||
if localpart != "echo":
|
logging.warning(
|
||||||
logging.warning(
|
"localpart %s has to be between %s and %s chars long",
|
||||||
"localpart %s has to be between %s and %s chars long",
|
localpart,
|
||||||
localpart,
|
config.username_min_length,
|
||||||
config.username_min_length,
|
config.username_max_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:
|
||||||
@@ -76,6 +86,21 @@ 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,14 +3,17 @@
|
|||||||
|
|
||||||
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.newemail import create_newemail_dict
|
|
||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
|
from chatmaild.newemail import create_newemail_dict
|
||||||
|
|
||||||
hooks = events.HookCollection()
|
hooks = events.HookCollection()
|
||||||
|
|
||||||
@@ -75,9 +78,23 @@ 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,10 @@
|
|||||||
import os
|
|
||||||
import logging
|
|
||||||
import json
|
import json
|
||||||
import filelock
|
import logging
|
||||||
|
import os
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import filelock
|
||||||
|
|
||||||
|
|
||||||
class FileDict:
|
class FileDict:
|
||||||
"""Concurrency-safe multi-reader/single-writer persistent dict."""
|
"""Concurrency-safe multi-reader/single-writer persistent dict."""
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
from email.parser import BytesParser
|
import time
|
||||||
from email import policy
|
from email import policy
|
||||||
|
from email.parser import BytesParser
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from pathlib import Path
|
|
||||||
from socketserver import (
|
|
||||||
UnixStreamServer,
|
|
||||||
StreamRequestHandler,
|
|
||||||
ThreadingMixIn,
|
|
||||||
)
|
|
||||||
import sys
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from socketserver import (
|
||||||
|
StreamRequestHandler,
|
||||||
|
ThreadingMixIn,
|
||||||
|
UnixStreamServer,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,32 +49,40 @@ class Metadata:
|
|||||||
return mdict.get(self.DEVICETOKEN_KEY, [])
|
return mdict.get(self.DEVICETOKEN_KEY, [])
|
||||||
|
|
||||||
|
|
||||||
def handle_dovecot_protocol(rfile, wfile, notifier, metadata):
|
def handle_dovecot_protocol(rfile, wfile, notifier, metadata, iroh_relay=None):
|
||||||
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)
|
res = handle_dovecot_request(msg, transactions, notifier, metadata, iroh_relay)
|
||||||
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):
|
def handle_dovecot_request(msg, transactions, notifier, metadata, iroh_relay=None):
|
||||||
# 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("/")
|
keyparts = parts[0].split("/", 2)
|
||||||
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:
|
||||||
@@ -120,7 +128,10 @@ class ThreadedUnixStreamServer(ThreadingMixIn, UnixStreamServer):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
socket, vmail_dir = sys.argv[1:]
|
socket, vmail_dir, config_path = 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():
|
||||||
@@ -136,7 +147,9 @@ def main():
|
|||||||
class Handler(StreamRequestHandler):
|
class Handler(StreamRequestHandler):
|
||||||
def handle(self):
|
def handle(self):
|
||||||
try:
|
try:
|
||||||
handle_dovecot_protocol(self.rfile, self.wfile, notifier, metadata)
|
handle_dovecot_protocol(
|
||||||
|
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
|
||||||
from pathlib import Path
|
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
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 read_config, Config
|
from chatmaild.config import Config, read_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,15 +25,16 @@ 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
|
||||||
import math
|
from dataclasses import dataclass
|
||||||
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 dataclasses import dataclass
|
from threading import Thread
|
||||||
|
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
|
||||||
from email.parser import BytesParser
|
import os
|
||||||
|
import random
|
||||||
from email import policy
|
from email import policy
|
||||||
import pytest
|
from email.parser import BytesParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from chatmaild.database import Database
|
import pytest
|
||||||
from chatmaild.config import read_config, write_initial_config
|
from chatmaild.config import read_config, write_initial_config
|
||||||
|
from chatmaild.database import Database
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -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,
|
||||||
lookup_passdb,
|
|
||||||
handle_dovecot_request,
|
|
||||||
handle_dovecot_protocol,
|
handle_dovecot_protocol,
|
||||||
|
handle_dovecot_request,
|
||||||
|
lookup_passdb,
|
||||||
)
|
)
|
||||||
from chatmaild.database import DBError
|
|
||||||
|
|
||||||
|
|
||||||
def test_basic(db, example_config):
|
def test_basic(db, example_config):
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
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 pytest
|
|
||||||
import requests
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
from chatmaild.metadata import (
|
from chatmaild.metadata import (
|
||||||
handle_dovecot_request,
|
|
||||||
handle_dovecot_protocol,
|
|
||||||
Metadata,
|
Metadata,
|
||||||
|
handle_dovecot_protocol,
|
||||||
|
handle_dovecot_request,
|
||||||
)
|
)
|
||||||
from chatmaild.notifier import (
|
from chatmaild.notifier import (
|
||||||
Notifier,
|
Notifier,
|
||||||
@@ -296,3 +296,17 @@ 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,7 +16,6 @@ dependencies = [
|
|||||||
"build",
|
"build",
|
||||||
"tox",
|
"tox",
|
||||||
"ruff",
|
"ruff",
|
||||||
"black",
|
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
@@ -31,3 +30,13 @@ 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,20 +2,20 @@
|
|||||||
Chat Mail pyinfra deploy.
|
Chat Mail pyinfra deploy.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
import subprocess
|
|
||||||
import shutil
|
|
||||||
import io
|
import io
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
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 .acmetool import deploy_acmetool
|
from pyinfra.operations import apt, files, pip, server, systemd
|
||||||
|
|
||||||
from chatmaild.config import read_config, Config
|
from .acmetool import deploy_acmetool
|
||||||
|
|
||||||
|
|
||||||
def _build_chatmaild(dist_dir) -> None:
|
def _build_chatmaild(dist_dir) -> None:
|
||||||
@@ -477,12 +477,31 @@ 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(
|
||||||
@@ -513,10 +532,15 @@ 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(nginx_hook=False, email="", domains=[]):
|
def deploy_acmetool(email="", domains=[]):
|
||||||
"""Deploy acmetool."""
|
"""Deploy acmetool."""
|
||||||
apt.packages(
|
apt.packages(
|
||||||
name="Install acmetool",
|
name="Install acmetool",
|
||||||
@@ -20,16 +20,13 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
|||||||
mode="644",
|
mode="644",
|
||||||
)
|
)
|
||||||
|
|
||||||
if nginx_hook:
|
files.put(
|
||||||
files.put(
|
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
|
||||||
src=importlib.resources.files(__package__)
|
dest="/usr/lib/acme/hooks/nginx",
|
||||||
.joinpath("acmetool.hook")
|
user="root",
|
||||||
.open("rb"),
|
group="root",
|
||||||
dest="/usr/lib/acme/hooks/nginx",
|
mode="744",
|
||||||
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"),
|
||||||
@@ -74,5 +71,5 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
|||||||
|
|
||||||
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,3 +3,5 @@ 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.3-September-21-2022.pdf": true
|
"acme-agreement:https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf": true
|
||||||
|
|||||||
@@ -4,19 +4,18 @@ 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 termcolor import colored
|
|
||||||
from chatmaild.config import read_config, write_initial_config
|
from chatmaild.config import read_config, write_initial_config
|
||||||
from cmdeploy.dns import show_dns, check_necessary_dns
|
from termcolor import colored
|
||||||
|
|
||||||
|
from cmdeploy.dns import check_necessary_dns, show_dns
|
||||||
|
|
||||||
#
|
#
|
||||||
# cmdeploy sub commands and options
|
# cmdeploy sub commands and options
|
||||||
@@ -157,26 +156,26 @@ def fmt_cmd_options(parser):
|
|||||||
|
|
||||||
|
|
||||||
def fmt_cmd(args, out):
|
def fmt_cmd(args, out):
|
||||||
"""Run formattting fixes (ruff and black) on all chatmail source code."""
|
"""Run formattting fixes 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")]
|
||||||
black_args = [shutil.which("black")]
|
format_args = [shutil.which("ruff"), "format"]
|
||||||
ruff_args = [shutil.which("ruff")]
|
check_args = [shutil.which("ruff"), "check"]
|
||||||
|
|
||||||
if args.check:
|
if args.check:
|
||||||
black_args.append("--check")
|
format_args.append("--diff")
|
||||||
else:
|
else:
|
||||||
ruff_args.append("--fix")
|
check_args.append("--fix")
|
||||||
|
|
||||||
if not args.verbose:
|
if not args.verbose:
|
||||||
black_args.append("-q")
|
check_args.append("--quiet")
|
||||||
ruff_args.append("-q")
|
format_args.append("--quiet")
|
||||||
|
|
||||||
black_args.extend(sources)
|
format_args.extend(sources)
|
||||||
ruff_args.extend(sources)
|
check_args.extend(sources)
|
||||||
|
|
||||||
out.check_call(" ".join(black_args), quiet=not args.verbose)
|
out.check_call(" ".join(format_args), quiet=not args.verbose)
|
||||||
out.check_call(" ".join(ruff_args), quiet=not args.verbose)
|
out.check_call(" ".join(check_args), quiet=not args.verbose)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -232,7 +231,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)
|
proc = subprocess.run(args, env=env, check=False)
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import os
|
|
||||||
import importlib.resources
|
import importlib.resources
|
||||||
|
import os
|
||||||
|
|
||||||
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 line in zonefile.splitlines():
|
for raw_line in zonefile.splitlines():
|
||||||
line = line.format(
|
line = raw_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
|
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY METADATA XDELTAPUSH XCHATMAIL
|
||||||
|
|
||||||
|
|
||||||
# Authentication for system users.
|
# Authentication for system users.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import qrcode
|
|
||||||
import os
|
|
||||||
from PIL import ImageFont, ImageDraw, Image
|
|
||||||
import io
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
import qrcode
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
|
||||||
def gen_qr_png_data(maildomain):
|
def gen_qr_png_data(maildomain):
|
||||||
|
|||||||
BIN
cmdeploy/src/cmdeploy/obs-home-deltachat.gpg
Normal file
BIN
cmdeploy/src/cmdeploy/obs-home-deltachat.gpg
Normal file
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}
|
ExecStart={execpath} /run/chatmail-metadata/metadata.socket /home/vmail/mail/{mail_domain} {config_path}
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
User=vmail
|
User=vmail
|
||||||
|
|||||||
@@ -7,6 +7,20 @@ 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=
|
||||||
@@ -16,7 +30,10 @@ 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,9 +1,10 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +15,13 @@ 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,4 +1,5 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import time
|
import ipaddress
|
||||||
import re
|
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
import imap_tools
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
import ipaddress
|
|
||||||
import imap_tools
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import os
|
|
||||||
import io
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import subprocess
|
|
||||||
import imaplib
|
import imaplib
|
||||||
import smtplib
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import smtplib
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from chatmaild.database import Database
|
|
||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
|
from chatmaild.database import Database
|
||||||
|
|
||||||
conftestdir = Path(__file__).parent
|
conftestdir = Path(__file__).parent
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cmdeploy.cmdeploy import get_parser, main
|
from cmdeploy.cmdeploy import get_parser, main
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import importlib.resources
|
|
||||||
import webbrowser
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import importlib.resources
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from jinja2 import Template
|
|
||||||
from .genqr import gen_qr_png_data
|
|
||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
from .genqr import gen_qr_png_data
|
||||||
|
|
||||||
|
|
||||||
def snapshot_dir_stats(somedir):
|
def snapshot_dir_stats(somedir):
|
||||||
@@ -120,7 +121,8 @@ def main():
|
|||||||
print(f"watching {src_path} directory for changes")
|
print(f"watching {src_path} directory for changes")
|
||||||
|
|
||||||
changenum = 0
|
changenum = 0
|
||||||
for count in range(0, 1000000):
|
count = 0
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user