mirror of
https://github.com/chatmail/relay.git
synced 2026-05-20 12:58:04 +00:00
Compare commits
35 Commits
hpk/cliff-
...
hpk/guard_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c9eaf3d63 | ||
|
|
7a91aa539d | ||
|
|
4c8ad95244 | ||
|
|
8d65770c28 | ||
|
|
0931da21b8 | ||
|
|
11a8f8cf9e | ||
|
|
0aa255e3f1 | ||
|
|
6c4764b452 | ||
|
|
c1f08a9afe | ||
|
|
5c8afb377e | ||
|
|
8225a9f398 | ||
|
|
eb221ca1af | ||
|
|
93421b317b | ||
|
|
777be107f3 | ||
|
|
8b81d5b5d6 | ||
|
|
e6a2906e82 | ||
|
|
67ba4ac99e | ||
|
|
8cadf51387 | ||
|
|
ce4bb97294 | ||
|
|
3a0c629f3b | ||
|
|
8df53c2655 | ||
|
|
3fd3ab1a68 | ||
|
|
d74f792787 | ||
|
|
1135372b81 | ||
|
|
c9f80bffd8 | ||
|
|
10e53d17e8 | ||
|
|
01ca2a8b91 | ||
|
|
fb01944f0d | ||
|
|
a90a651ba0 | ||
|
|
7d74b46502 | ||
|
|
6d3e690653 | ||
|
|
ed7a70ba31 | ||
|
|
023116bc91 | ||
|
|
b13929119b | ||
|
|
a4152140ca |
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,5 @@
|
|||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Mutual Help Chat Group
|
||||||
|
url: https://i.delta.chat/#6CBFF8FFD505C0FDEA20A66674F2916EA8FBEE99&a=invitebot%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=X69wTFfvCfs3d-JzqP0kVA3i&s=ibp-447dU-wUq-52QanwAtWc
|
||||||
|
about: If you have troubles setting up the relay server, feel free to ask here.
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
## Chatmail Relay
|
This diagram shows components of the chatmail server; this is a draft
|
||||||
|
|
||||||
This diagram shows components of the chatmail relay; this is a draft
|
|
||||||
overview as of mid-August 2025:
|
overview as of mid-August 2025:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@@ -50,66 +48,3 @@ graph LR;
|
|||||||
The edges in this graph should not be taken too literally; they
|
The edges in this graph should not be taken too literally; they
|
||||||
reflect some sort of communication path or dependency relationship
|
reflect some sort of communication path or dependency relationship
|
||||||
between components of the chatmail server.
|
between components of the chatmail server.
|
||||||
|
|
||||||
## cmdeploy
|
|
||||||
|
|
||||||
cmdeploy is a Python program that uses the pyinfra library to deploy
|
|
||||||
chatmail servers, with all the necessary software, configuration, and
|
|
||||||
services. The deployment process performs three primary types of operation:
|
|
||||||
|
|
||||||
1. Installation of software, universal across all deployments.
|
|
||||||
2. Configuration of software, with deploy-specific variations.
|
|
||||||
3. Activation of services.
|
|
||||||
|
|
||||||
The process is implemented through a family of "deployer" objects
|
|
||||||
which all derive from a common `Deployer` base class, defined in
|
|
||||||
[deployer.py](cmdeploy/src/cmdeploy/deployer.py). Each object
|
|
||||||
provides implementation methods for the three stages -- install,
|
|
||||||
configure, and activate. The top-level procedure in
|
|
||||||
`deploy_chatmail()` calls these methods for all the deployer objects,
|
|
||||||
first calling all the install methods, then the configure methods,
|
|
||||||
then the activate methods.
|
|
||||||
|
|
||||||
The base class also implements support for a CMDEPLOY_STAGES
|
|
||||||
environment variable, which allows limiting the process to specific
|
|
||||||
stages. Note that some deployers are stateful between the stages
|
|
||||||
(this is one reason why they are implemented as objects), and that
|
|
||||||
state will not get propagated between stages when run in separate
|
|
||||||
invocations of cmdeploy. This environment variable is intended for
|
|
||||||
use in future revisions to support building Docker images with
|
|
||||||
software pre-installed, and configuration of containers at run time
|
|
||||||
from environmnet variables.
|
|
||||||
|
|
||||||
The `install_impl()` method for the deployer classes is static, to
|
|
||||||
ensure that it does not rely on any object state, in particular, the
|
|
||||||
configuration details of the deployment. This helps ensure that all
|
|
||||||
install methods are suitable for running as part of a container image
|
|
||||||
build.
|
|
||||||
|
|
||||||
Operations that start services for systemd-based deployments should
|
|
||||||
only be called from the `activate_impl()` methods. These methods will
|
|
||||||
not be called in non-systemd container environments.
|
|
||||||
|
|
||||||
### Deployer objects
|
|
||||||
|
|
||||||
One might ask why the deployers are implemented as object classes, as
|
|
||||||
opposed to callable functions or the like. There are various reasons
|
|
||||||
why objects are a good fit for the deployment process.
|
|
||||||
|
|
||||||
1. Objects provide a way to organize the install, configure, and
|
|
||||||
deploy operations for each component that is installed, supporting a
|
|
||||||
"driver" type of pattern. This could be implemented in other ways
|
|
||||||
without objects, such as function jump tables, but objects provide a
|
|
||||||
clean and formalized way to do essentially the same thing.
|
|
||||||
|
|
||||||
2. Class inheritance provides a natural way to define
|
|
||||||
component-specific operations for the various stages of deployment, by
|
|
||||||
overriding the no-op stub methods in the base class. The base class
|
|
||||||
handles policy decisions about which stages are to be executed,
|
|
||||||
ensuring consistent handling of the stages in a central location.
|
|
||||||
|
|
||||||
3. Some of the components track state between stages, basing decisions
|
|
||||||
like whether to restart a service on whether the software or
|
|
||||||
configuration of that service was changed in an earlier stage.
|
|
||||||
Keeping track of state between method calls is an ideal use case for
|
|
||||||
objects.
|
|
||||||
|
|||||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -2,21 +2,6 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
- Organized cmdeploy into install, configure, and activate stages
|
|
||||||
([#695](https://github.com/chatmail/relay/pull/695))
|
|
||||||
|
|
||||||
- don't deploy the website if there are merge conflicts in the www folder
|
|
||||||
([#714](https://github.com/chatmail/relay/pull/714))
|
|
||||||
|
|
||||||
- acmetool: use ECDSA keys instead of RSA
|
|
||||||
([#689](https://github.com/chatmail/relay/pull/689))
|
|
||||||
|
|
||||||
- Require TLS 1.2 for outgoing SMTP connections
|
|
||||||
([#685](https://github.com/chatmail/relay/pull/685))
|
|
||||||
|
|
||||||
- require STARTTLS for incoming port 25 connections
|
|
||||||
([#684](https://github.com/chatmail/relay/pull/684))
|
|
||||||
|
|
||||||
- filtermail: run CPU-intensive handle_DATA in a thread pool executor
|
- filtermail: run CPU-intensive handle_DATA in a thread pool executor
|
||||||
([#676](https://github.com/chatmail/relay/pull/676))
|
([#676](https://github.com/chatmail/relay/pull/676))
|
||||||
|
|
||||||
@@ -36,7 +21,7 @@
|
|||||||
([#650](https://github.com/chatmail/relay/pull/650))
|
([#650](https://github.com/chatmail/relay/pull/650))
|
||||||
|
|
||||||
- filtermail: accept mails from Protonmail
|
- filtermail: accept mails from Protonmail
|
||||||
([#616](https://github.com/chatmail/relay/pull/616))
|
([#616](https://github.com/chatmail/relay/pull/655))
|
||||||
|
|
||||||
- Ignore all RCPT TO: parameters
|
- Ignore all RCPT TO: parameters
|
||||||
([#651](https://github.com/chatmail/relay/pull/651))
|
([#651](https://github.com/chatmail/relay/pull/651))
|
||||||
@@ -69,7 +54,7 @@
|
|||||||
to only do a single iteration over sometimes millions of messages
|
to only do a single iteration over sometimes millions of messages
|
||||||
instead of doing "find" commands that iterate 9 times over the messages.
|
instead of doing "find" commands that iterate 9 times over the messages.
|
||||||
Provide an "fsreport" CLI for more fine grained analysis of message files.
|
Provide an "fsreport" CLI for more fine grained analysis of message files.
|
||||||
([#637](https://github.com/chatmail/relay/pull/637))
|
([#637](https://github.com/chatmail/relay/pull/632))
|
||||||
|
|
||||||
|
|
||||||
## 1.7.0 2025-09-11
|
## 1.7.0 2025-09-11
|
||||||
|
|||||||
@@ -180,10 +180,6 @@ The components of chatmail are:
|
|||||||
- [Iroh relay](https://www.iroh.computer/docs/concepts/relay)
|
- [Iroh relay](https://www.iroh.computer/docs/concepts/relay)
|
||||||
which helps client devices to establish Peer-to-Peer connections
|
which helps client devices to establish Peer-to-Peer connections
|
||||||
|
|
||||||
- [TURN](https://github.com/chatmail/chatmail-turn)
|
|
||||||
to enable relay users to start webRTC calls
|
|
||||||
even if a p2p connection can't be established
|
|
||||||
|
|
||||||
- and the chatmaild services, explained in the next section:
|
- and the chatmaild services, explained in the next section:
|
||||||
|
|
||||||
### chatmaild
|
### chatmaild
|
||||||
@@ -308,8 +304,6 @@ Chatmail address creation will be denied while this file is present.
|
|||||||
[Nginx](https://www.nginx.com/) listens on port 8443 (HTTPS-ALT) and 443 (HTTPS).
|
[Nginx](https://www.nginx.com/) listens on port 8443 (HTTPS-ALT) and 443 (HTTPS).
|
||||||
Port 443 multiplexes HTTPS, IMAP and SMTP using ALPN to redirect connections to ports 8443, 465 or 993.
|
Port 443 multiplexes HTTPS, IMAP and SMTP using ALPN to redirect connections to ports 8443, 465 or 993.
|
||||||
[acmetool](https://hlandau.github.io/acmetool/) listens on port 80 (HTTP).
|
[acmetool](https://hlandau.github.io/acmetool/) listens on port 80 (HTTP).
|
||||||
[chatmail-turn](https://github.com/chatmail/chatmail-turn) listens on UDP port 3478 (STUN/TURN),
|
|
||||||
and temporarily opens UDP ports when users request them. UDP port range is not restricted, any free port may be allocated.
|
|
||||||
|
|
||||||
chatmail-core based apps will, however, discover all ports and configurations
|
chatmail-core based 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 relay server.
|
automatically by reading the [autoconfig XML file](https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html) from the chatmail relay server.
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ def turn_credentials() -> str:
|
|||||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
||||||
client_socket.connect("/run/chatmail-turn/turn.socket")
|
client_socket.connect("/run/chatmail-turn/turn.socket")
|
||||||
with client_socket.makefile("rb") as file:
|
with client_socket.makefile("rb") as file:
|
||||||
return file.readline().decode("utf-8").strip()
|
return file.readline().decode("utf-8")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,79 +2,66 @@ import importlib.resources
|
|||||||
|
|
||||||
from pyinfra.operations import apt, files, server, systemd
|
from pyinfra.operations import apt, files, server, systemd
|
||||||
|
|
||||||
from ..deployer import Deployer
|
|
||||||
|
|
||||||
|
def deploy_acmetool(email="", domains=[]):
|
||||||
|
"""Deploy acmetool."""
|
||||||
|
apt.packages(
|
||||||
|
name="Install acmetool",
|
||||||
|
packages=["acmetool"],
|
||||||
|
)
|
||||||
|
|
||||||
class AcmetoolDeployer(Deployer):
|
files.put(
|
||||||
def __init__(self, *, email, domains, **kwargs):
|
src=importlib.resources.files(__package__).joinpath("acmetool.cron").open("rb"),
|
||||||
super().__init__(**kwargs)
|
dest="/etc/cron.d/acmetool",
|
||||||
self.domains = domains
|
user="root",
|
||||||
self.email = email
|
group="root",
|
||||||
self.need_restart = False
|
mode="644",
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
files.put(
|
||||||
def install_impl():
|
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
|
||||||
apt.packages(
|
dest="/usr/lib/acme/hooks/nginx",
|
||||||
name="Install acmetool",
|
user="root",
|
||||||
packages=["acmetool"],
|
group="root",
|
||||||
)
|
mode="744",
|
||||||
|
)
|
||||||
|
|
||||||
def configure_impl(self):
|
files.template(
|
||||||
files.put(
|
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
|
||||||
src=importlib.resources.files(__package__).joinpath("acmetool.cron").open("rb"),
|
dest="/var/lib/acme/conf/responses",
|
||||||
dest="/etc/cron.d/acmetool",
|
user="root",
|
||||||
user="root",
|
group="root",
|
||||||
group="root",
|
mode="644",
|
||||||
mode="644",
|
email=email,
|
||||||
)
|
)
|
||||||
|
|
||||||
files.put(
|
files.template(
|
||||||
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
|
src=importlib.resources.files(__package__).joinpath("target.yaml.j2"),
|
||||||
dest="/usr/lib/acme/hooks/nginx",
|
dest="/var/lib/acme/conf/target",
|
||||||
user="root",
|
user="root",
|
||||||
group="root",
|
group="root",
|
||||||
mode="744",
|
mode="644",
|
||||||
)
|
)
|
||||||
|
|
||||||
files.template(
|
service_file = files.put(
|
||||||
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
|
src=importlib.resources.files(__package__).joinpath(
|
||||||
dest="/var/lib/acme/conf/responses",
|
"acmetool-redirector.service"
|
||||||
user="root",
|
),
|
||||||
group="root",
|
dest="/etc/systemd/system/acmetool-redirector.service",
|
||||||
mode="644",
|
user="root",
|
||||||
email=self.email,
|
group="root",
|
||||||
)
|
mode="644",
|
||||||
|
)
|
||||||
|
|
||||||
files.template(
|
systemd.service(
|
||||||
src=importlib.resources.files(__package__).joinpath("target.yaml.j2"),
|
name="Setup acmetool-redirector service",
|
||||||
dest="/var/lib/acme/conf/target",
|
service="acmetool-redirector.service",
|
||||||
user="root",
|
running=True,
|
||||||
group="root",
|
enabled=True,
|
||||||
mode="644",
|
restarted=service_file.changed,
|
||||||
)
|
)
|
||||||
|
|
||||||
service_file = files.put(
|
server.shell(
|
||||||
src=importlib.resources.files(__package__).joinpath(
|
name=f"Request certificate for: {', '.join(domains)}",
|
||||||
"acmetool-redirector.service"
|
commands=[f"acmetool want --xlog.severity=debug {' '.join(domains)}"],
|
||||||
),
|
)
|
||||||
dest="/etc/systemd/system/acmetool-redirector.service",
|
|
||||||
user="root",
|
|
||||||
group="root",
|
|
||||||
mode="644",
|
|
||||||
)
|
|
||||||
self.need_restart = service_file.changed
|
|
||||||
|
|
||||||
def activate_impl(self):
|
|
||||||
systemd.service(
|
|
||||||
name="Setup acmetool-redirector service",
|
|
||||||
service="acmetool-redirector.service",
|
|
||||||
running=True,
|
|
||||||
enabled=True,
|
|
||||||
restarted=self.need_restart,
|
|
||||||
)
|
|
||||||
self.need_restart = False
|
|
||||||
|
|
||||||
server.shell(
|
|
||||||
name=f"Request certificate for: {', '.join(self.domains)}",
|
|
||||||
commands=[f"acmetool want --xlog.severity=debug {' '.join(self.domains)}"],
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
request:
|
request:
|
||||||
provider: https://acme-v02.api.letsencrypt.org/directory
|
provider: https://acme-v02.api.letsencrypt.org/directory
|
||||||
key:
|
key:
|
||||||
type: ecdsa
|
type: rsa
|
||||||
ecdsa-curve: nistp256
|
|
||||||
challenge:
|
challenge:
|
||||||
webroot-paths:
|
webroot-paths:
|
||||||
- /var/www/html/.well-known/acme-challenge
|
- /var/www/html/.well-known/acme-challenge
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from pyinfra.operations import server
|
|
||||||
|
|
||||||
|
|
||||||
class Deployment:
|
|
||||||
def install(self, deployer):
|
|
||||||
# optional 'required_users' contains a list of (user, group, secondary-group-list) tuples.
|
|
||||||
# If the group is None, no group is created corresponding to that user.
|
|
||||||
# If the secondary group list is not None, all listed groups are created as well.
|
|
||||||
required_users = getattr(deployer, "required_users", [])
|
|
||||||
for user, group, groups in required_users:
|
|
||||||
if group is not None:
|
|
||||||
server.group(
|
|
||||||
name="Create {} group".format(group), group=group, system=True
|
|
||||||
)
|
|
||||||
if groups is not None:
|
|
||||||
for group2 in groups:
|
|
||||||
server.group(
|
|
||||||
name="Create {} group".format(group2), group=group2, system=True
|
|
||||||
)
|
|
||||||
server.user(
|
|
||||||
name="Create {} user".format(user),
|
|
||||||
user=user,
|
|
||||||
group=group,
|
|
||||||
groups=groups,
|
|
||||||
system=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
ret = bool(deployer.install())
|
|
||||||
if ret:
|
|
||||||
deployer.need_restart = True
|
|
||||||
|
|
||||||
def configure(self, deployer):
|
|
||||||
deployer.configure()
|
|
||||||
|
|
||||||
def activate(self, deployer):
|
|
||||||
deployer.activate()
|
|
||||||
|
|
||||||
def perform_stages(self, deployers):
|
|
||||||
default_stages = "install,configure,activate"
|
|
||||||
stages = os.getenv("CMDEPLOY_STAGES", default_stages).split(",")
|
|
||||||
|
|
||||||
for stage in stages:
|
|
||||||
for deployer in deployers:
|
|
||||||
getattr(self, stage)(deployer)
|
|
||||||
|
|
||||||
|
|
||||||
class Deployer:
|
|
||||||
need_restart = False
|
|
||||||
|
|
||||||
def install(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def configure(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def activate(self):
|
|
||||||
pass
|
|
||||||
@@ -70,12 +70,6 @@ userdb {
|
|||||||
# Mailboxes are stored in the "mail" directory of the vmail user home.
|
# Mailboxes are stored in the "mail" directory of the vmail user home.
|
||||||
mail_location = maildir:{{ config.mailboxes_dir }}/%u
|
mail_location = maildir:{{ config.mailboxes_dir }}/%u
|
||||||
|
|
||||||
# index/cache files are not very useful for chatmail relay operations
|
|
||||||
# but it's not clear how to disable them completely.
|
|
||||||
# According to https://doc.dovecot.org/2.3/settings/advanced/#core_setting-mail_cache_max_size
|
|
||||||
# if the cache file becomes larger than the specified size, it is truncated by dovecot
|
|
||||||
mail_cache_max_size = 500K
|
|
||||||
|
|
||||||
namespace inbox {
|
namespace inbox {
|
||||||
inbox = yes
|
inbox = yes
|
||||||
|
|
||||||
|
|||||||
BIN
cmdeploy/src/cmdeploy/obs-home-deltachat.gpg
Normal file
BIN
cmdeploy/src/cmdeploy/obs-home-deltachat.gpg
Normal file
Binary file not shown.
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
echo "All runlevel operations denied by policy" >&2
|
|
||||||
exit 101
|
|
||||||
@@ -26,7 +26,6 @@ smtp_tls_security_level=verify
|
|||||||
smtp_tls_servername = hostname
|
smtp_tls_servername = hostname
|
||||||
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
||||||
smtp_tls_policy_maps = inline:{nauta.cu=may}
|
smtp_tls_policy_maps = inline:{nauta.cu=may}
|
||||||
smtp_tls_protocols = >=TLSv1.2
|
|
||||||
smtpd_tls_protocols = >=TLSv1.2
|
smtpd_tls_protocols = >=TLSv1.2
|
||||||
|
|
||||||
# Disable anonymous cipher suites
|
# Disable anonymous cipher suites
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ smtp inet n - y - - smtpd -v
|
|||||||
{%- else %}
|
{%- else %}
|
||||||
smtp inet n - y - - smtpd
|
smtp inet n - y - - smtpd
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
-o smtpd_tls_security_level=encrypt
|
|
||||||
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port_incoming }}
|
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port_incoming }}
|
||||||
submission inet n - y - 5000 smtpd
|
submission inet n - y - 5000 smtpd
|
||||||
-o syslog_name=postfix/submission
|
-o syslog_name=postfix/submission
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ def query_dns(typ, domain):
|
|||||||
|
|
||||||
# Query authoritative nameserver directly to bypass DNS cache.
|
# Query authoritative nameserver directly to bypass DNS cache.
|
||||||
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
|
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
|
||||||
return next((line for line in res.split("\n") if not line.startswith(';')), '')
|
if res:
|
||||||
|
return res.split("\n")[0]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_zonefile(zonefile, verbose=True):
|
def check_zonefile(zonefile, verbose=True):
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class TestDC:
|
|||||||
|
|
||||||
def test_ping_pong(self, benchmark, cmfactory):
|
def test_ping_pong(self, benchmark, cmfactory):
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_protected_chat(ac1, ac2)
|
||||||
|
|
||||||
def dc_ping_pong():
|
def dc_ping_pong():
|
||||||
chat.send_text("ping")
|
chat.send_text("ping")
|
||||||
@@ -49,7 +49,7 @@ class TestDC:
|
|||||||
|
|
||||||
def test_send_10_receive_10(self, benchmark, cmfactory, lp):
|
def test_send_10_receive_10(self, benchmark, cmfactory, lp):
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_protected_chat(ac1, ac2)
|
||||||
|
|
||||||
def dc_send_10_receive_10():
|
def dc_send_10_receive_10():
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import queue
|
import queue
|
||||||
import smtplib
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -91,23 +91,25 @@ def test_concurrent_logins_same_account(
|
|||||||
|
|
||||||
def test_no_vrfy(chatmail_config):
|
def test_no_vrfy(chatmail_config):
|
||||||
domain = chatmail_config.mail_domain
|
domain = chatmail_config.mail_domain
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
s = smtplib.SMTP(domain)
|
sock.settimeout(10)
|
||||||
s.starttls()
|
try:
|
||||||
|
sock.connect((domain, 25))
|
||||||
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
|
except socket.timeout:
|
||||||
result = s.getreply()
|
pytest.skip(f"port 25 not reachable for {domain}")
|
||||||
|
banner = sock.recv(1024)
|
||||||
|
print(banner)
|
||||||
|
sock.send(b"VRFY wrongaddress@%s\r\n" % (chatmail_config.mail_domain.encode(),))
|
||||||
|
result = sock.recv(1024)
|
||||||
print(result)
|
print(result)
|
||||||
s.putcmd("vrfy", f"echo@{chatmail_config.mail_domain}")
|
sock.send(b"VRFY echo@%s\r\n" % (chatmail_config.mail_domain.encode(),))
|
||||||
result2 = s.getreply()
|
result2 = sock.recv(1024)
|
||||||
print(result2)
|
print(result2)
|
||||||
assert result[0] == result2[0] == 252
|
assert result[0:10] == result2[0:10]
|
||||||
assert result[1][0:6] == result2[1][0:6] == b"2.0.0 "
|
sock.send(b"VRFY wrongaddress\r\n")
|
||||||
s.putcmd("vrfy", "wrongaddress")
|
result = sock.recv(1024)
|
||||||
result = s.getreply()
|
|
||||||
print(result)
|
print(result)
|
||||||
s.putcmd("vrfy", "echo")
|
sock.send(b"VRFY echo\r\n")
|
||||||
result2 = s.getreply()
|
result2 = sock.recv(1024)
|
||||||
print(result2)
|
print(result2)
|
||||||
assert result[0] == result2[0] == 252
|
assert result[0:10] == result2[0:10] == b"252 2.0.0 "
|
||||||
assert result[1][0:6] == result2[1][0:6] == b"2.0.0 "
|
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ def test_reject_missing_dkim(cmsetup, maildata, from_addr):
|
|||||||
"encrypted.eml", from_addr=from_addr, to_addr=recipient.addr
|
"encrypted.eml", from_addr=from_addr, to_addr=recipient.addr
|
||||||
).as_string()
|
).as_string()
|
||||||
conn = smtplib.SMTP(cmsetup.maildomain, 25, timeout=10)
|
conn = smtplib.SMTP(cmsetup.maildomain, 25, timeout=10)
|
||||||
conn.starttls()
|
|
||||||
|
|
||||||
with conn as s:
|
with conn as s:
|
||||||
with pytest.raises(smtplib.SMTPDataError, match="No valid DKIM signature"):
|
with pytest.raises(smtplib.SMTPDataError, match="No valid DKIM signature"):
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class TestEndToEndDeltaChat:
|
|||||||
"""Test that a DC account can send a message to a second DC account
|
"""Test that a DC account can send a message to a second DC account
|
||||||
on the same chat-mail instance."""
|
on the same chat-mail instance."""
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_protected_chat(ac1, ac2)
|
||||||
chat.send_text("message0")
|
chat.send_text("message0")
|
||||||
|
|
||||||
lp.sec("wait for ac2 to receive message")
|
lp.sec("wait for ac2 to receive message")
|
||||||
@@ -70,7 +70,7 @@ class TestEndToEndDeltaChat:
|
|||||||
before quota is exceeded, and thus depends on the speed of the upload.
|
before quota is exceeded, and thus depends on the speed of the upload.
|
||||||
"""
|
"""
|
||||||
ac1, ac2 = cmfactory.get_online_accounts(2)
|
ac1, ac2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(ac1, ac2)
|
chat = cmfactory.get_protected_chat(ac1, ac2)
|
||||||
|
|
||||||
user = ac2.get_config("configured_addr")
|
user = ac2.get_config("configured_addr")
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ def test_hide_senders_ip_address(cmfactory):
|
|||||||
assert ipaddress.ip_address(public_ip)
|
assert ipaddress.ip_address(public_ip)
|
||||||
|
|
||||||
user1, user2 = cmfactory.get_online_accounts(2)
|
user1, user2 = cmfactory.get_online_accounts(2)
|
||||||
chat = cmfactory.get_accepted_chat(user1, user2)
|
chat = cmfactory.get_protected_chat(user1, user2)
|
||||||
|
|
||||||
chat.send_text("testing submission header cleanup")
|
chat.send_text("testing submission header cleanup")
|
||||||
user2._evtracker.wait_next_incoming_message()
|
user2._evtracker.wait_next_incoming_message()
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cmdeploy import remote
|
from cmdeploy import remote
|
||||||
@@ -10,63 +8,38 @@ from cmdeploy.dns import check_full_zone, check_initial_remote_data
|
|||||||
def mockdns_base(monkeypatch):
|
def mockdns_base(monkeypatch):
|
||||||
qdict = {}
|
qdict = {}
|
||||||
|
|
||||||
def shell(command, fail_ok=False, print=print):
|
def query_dns(typ, domain):
|
||||||
if command.startswith("dig"):
|
try:
|
||||||
if command == "dig":
|
return qdict[typ][domain]
|
||||||
return "."
|
except KeyError:
|
||||||
if "SOA" in command:
|
return ""
|
||||||
return (
|
|
||||||
"delta.chat. 21600 IN SOA ns1.first-ns.de. dns.hetzner.com."
|
|
||||||
" 2025102800 14400 1800 604800 3600"
|
|
||||||
)
|
|
||||||
command_chunks = command.split()
|
|
||||||
domain, typ = command_chunks[4], command_chunks[6]
|
|
||||||
try:
|
|
||||||
return qdict[typ][domain]
|
|
||||||
except KeyError:
|
|
||||||
return ""
|
|
||||||
return remote.rshell.shell(command=command, fail_ok=fail_ok, print=print)
|
|
||||||
|
|
||||||
monkeypatch.setattr(remote.rdns, shell.__name__, shell)
|
monkeypatch.setattr(remote.rdns, query_dns.__name__, query_dns)
|
||||||
return qdict
|
return qdict
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mockdns_expected():
|
def mockdns(mockdns_base):
|
||||||
return {
|
mockdns_base.update(
|
||||||
"A": {"some.domain": "1.1.1.1"},
|
{
|
||||||
"AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"},
|
"A": {"some.domain": "1.1.1.1"},
|
||||||
"CNAME": {
|
"AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"},
|
||||||
"mta-sts.some.domain": "some.domain.",
|
"CNAME": {
|
||||||
"www.some.domain": "some.domain.",
|
"mta-sts.some.domain": "some.domain.",
|
||||||
},
|
"www.some.domain": "some.domain.",
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
@pytest.fixture(params=["plain", "with-dns-comments"])
|
|
||||||
def mockdns(request, mockdns_base, mockdns_expected):
|
|
||||||
mockdns_base.update(deepcopy(mockdns_expected))
|
|
||||||
match request.param:
|
|
||||||
case "plain":
|
|
||||||
pass
|
|
||||||
case "with-dns-comments":
|
|
||||||
for typ, data in mockdns_base.items():
|
|
||||||
for host, result in data.items():
|
|
||||||
mockdns_base[typ][host] = (
|
|
||||||
";; some unsuccessful attempt result\n"
|
|
||||||
"; and another with a single semicolon\n"
|
|
||||||
f"{result}"
|
|
||||||
)
|
|
||||||
return mockdns_base
|
return mockdns_base
|
||||||
|
|
||||||
|
|
||||||
class TestPerformInitialChecks:
|
class TestPerformInitialChecks:
|
||||||
def test_perform_initial_checks_ok1(self, mockdns, mockdns_expected):
|
def test_perform_initial_checks_ok1(self, mockdns):
|
||||||
remote_data = remote.rdns.perform_initial_checks("some.domain")
|
remote_data = remote.rdns.perform_initial_checks("some.domain")
|
||||||
assert remote_data["A"] == mockdns_expected["A"]["some.domain"]
|
assert remote_data["A"] == mockdns["A"]["some.domain"]
|
||||||
assert remote_data["AAAA"] == mockdns_expected["AAAA"]["some.domain"]
|
assert remote_data["AAAA"] == mockdns["AAAA"]["some.domain"]
|
||||||
assert remote_data["MTA_STS"] == mockdns_expected["CNAME"]["mta-sts.some.domain"]
|
assert remote_data["MTA_STS"] == mockdns["CNAME"]["mta-sts.some.domain"]
|
||||||
assert remote_data["WWW"] == mockdns_expected["CNAME"]["www.some.domain"]
|
assert remote_data["WWW"] == mockdns["CNAME"]["www.some.domain"]
|
||||||
|
|
||||||
@pytest.mark.parametrize("drop", ["A", "AAAA"])
|
@pytest.mark.parametrize("drop", ["A", "AAAA"])
|
||||||
def test_perform_initial_checks_with_one_of_A_AAAA(self, mockdns, drop):
|
def test_perform_initial_checks_with_one_of_A_AAAA(self, mockdns, drop):
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
from chatmaild.config import read_config
|
from chatmaild.config import read_config
|
||||||
@@ -13,9 +12,6 @@ from jinja2 import Template
|
|||||||
from .genqr import gen_qr_png_data
|
from .genqr import gen_qr_png_data
|
||||||
|
|
||||||
|
|
||||||
_MERGE_CONFLICT_RE = re.compile(r"^<<<<<<<.+^=======.+^>>>>>>>", re.DOTALL | re.MULTILINE)
|
|
||||||
|
|
||||||
|
|
||||||
def snapshot_dir_stats(somedir):
|
def snapshot_dir_stats(somedir):
|
||||||
d = {}
|
d = {}
|
||||||
for path in somedir.iterdir():
|
for path in somedir.iterdir():
|
||||||
@@ -120,17 +116,6 @@ def _build_webpages(src_dir, build_dir, config):
|
|||||||
return build_dir
|
return build_dir
|
||||||
|
|
||||||
|
|
||||||
def find_merge_conflict(src_dir) -> Path:
|
|
||||||
assert src_dir.exists(), src_dir
|
|
||||||
result = None
|
|
||||||
for path in src_dir.iterdir():
|
|
||||||
if path.suffix in [".css", ".html", ".md"]:
|
|
||||||
if _MERGE_CONFLICT_RE.search(path.read_text()):
|
|
||||||
result = path
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
path = importlib.resources.files(__package__)
|
path = importlib.resources.files(__package__)
|
||||||
reporoot = path.joinpath("../../../").resolve()
|
reporoot = path.joinpath("../../../").resolve()
|
||||||
|
|||||||
Reference in New Issue
Block a user