Compare commits

..

7 Commits

Author SHA1 Message Date
missytake ccd58da8e9 fix: show privacy email or invite as link
fix #635
2026-06-17 11:39:07 +02:00
Jagoda Ślązak 8d7870db55 chore(deps): Upgrade filtermail to v0.7.1
## 0.7.1 - 2026-06-09

### Bug Fixes

- Switch ratelimiter to MonotonicClock
- *(smtp-server)* Correct error when EOF while reading DATA
- *(resolver)* Disable negative caching
- Ignore CNAME records when resolving TXT records

### Features

- Switch to aws-lc-rs cryptography provider

### Miscellaneous Tasks

- Add filtermail.mtail so filtermail failures can be monitored

Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
2026-06-17 10:16:35 +02:00
Jagoda Estera Ślązak 5a31ff475f docs(overview): Remove mermaid styles from 'Accepting and delivering mail' (#1009)
Previously used colors caused
diagrams to be unreadable in
light mode.
Removing styling completely,
as this was redundant anyway.

Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
2026-06-10 22:31:26 +02:00
missytake 8c18aea18e fix: pass kwargs to files.put() 2026-06-10 16:49:34 +02:00
missytake ebf5a51964 fix: still overwrite /etc/resolv.conf if it is a symbolic link 2026-06-10 16:49:34 +02:00
feld f596d4b56d Merge pull request #1003 from chatmail/feld/crypt-r
fix: crypt-r dependency was declared for wrong Python version
2026-06-09 12:38:45 -07:00
Mark Felder 8e3c18019b fix: crypt-r dependency was declared for wrong Python version
The original crypt library was last supported in Python 3.12, so it's
not needed until Python 3.13 is default (Trixie)

> The crypt_r module is a renamed copy of the crypt module as it was present in Python 3.12 before it was removed.
2026-06-09 12:38:07 -07:00
12 changed files with 28 additions and 77 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false persist-credentials: false
- name: download filtermail - name: download filtermail
run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.7.0/filtermail-x86_64 -o /usr/local/bin/filtermail && chmod +x /usr/local/bin/filtermail run: curl -L https://github.com/chatmail/filtermail/releases/download/v0.7.1/filtermail-x86_64 -o /usr/local/bin/filtermail && chmod +x /usr/local/bin/filtermail
- name: run chatmaild tests - name: run chatmaild tests
working-directory: chatmaild working-directory: chatmaild
run: pipx run tox run: pipx run tox
+1 -1
View File
@@ -9,7 +9,7 @@ dependencies = [
"iniconfig", "iniconfig",
"filelock", "filelock",
"requests", "requests",
"crypt-r >= 3.13.1 ; python_version >= '3.11'", "crypt-r >= 3.13.1 ; python_version >= '3.13'",
] ]
[tool.setuptools] [tool.setuptools]
+3 -16
View File
@@ -1,6 +1,5 @@
import ipaddress import ipaddress
from pathlib import Path from pathlib import Path
from random import randint
import iniconfig import iniconfig
@@ -42,11 +41,6 @@ class Config:
self.username_max_length = int(params.pop("username_max_length", 9)) self.username_max_length = int(params.pop("username_max_length", 9))
self.password_min_length = int(params.pop("password_min_length", 9)) self.password_min_length = int(params.pop("password_min_length", 9))
self.www_folder = params.pop("www_folder", "") self.www_folder = params.pop("www_folder", "")
self.imap_port = int(params.pop("imap_port", 143))
self.imaps_port = int(params.pop("imaps_port", 993))
self.smtp_port = int(params.pop("smtp_port", 587))
self.smtps_port = int(params.pop("smtps_port", 465))
self.filtermail_smtp_port = int(params.pop("filtermail_smtp_port", "10080")) self.filtermail_smtp_port = int(params.pop("filtermail_smtp_port", "10080"))
self.filtermail_smtp_port_incoming = int( self.filtermail_smtp_port_incoming = int(
params.pop("filtermail_smtp_port_incoming", "10081") params.pop("filtermail_smtp_port_incoming", "10081")
@@ -77,7 +71,7 @@ class Config:
self.iroh_relay = iroh_relay.strip() self.iroh_relay = iroh_relay.strip()
self.enable_iroh_relay = False self.enable_iroh_relay = False
self.privacy_postal = params.pop("privacy_postal", None) self.privacy_postal = params.pop("privacy_postal", None)
self.privacy_mail = params.pop("privacy_mail", None) self.privacy_mail = params.pop("admin_contact", params.pop("privacy_mail", None))
self.privacy_pdo = params.pop("privacy_pdo", None) self.privacy_pdo = params.pop("privacy_pdo", None)
self.privacy_supervisor = params.pop("privacy_supervisor", None) self.privacy_supervisor = params.pop("privacy_supervisor", None)
@@ -144,15 +138,8 @@ def parse_size_mb(limit):
def write_initial_config(inipath, mail_domain, overrides): def write_initial_config(inipath, mail_domain, overrides):
"""Write out default config file, using the specified config value overrides.""" """Write out default config file, using the specified config value overrides."""
content = get_default_config_content(mail_domain, **overrides).splitlines() content = get_default_config_content(mail_domain, **overrides)
used_ports = [25, 53, 80, 143, 402, 443, 465, 587, 993, 3340, 3903, 3904, 8443, 10080, 10081, 10082, 10083, 10025, 10026] inipath.write_text(content)
for config_key in ["smtp_port", "imap_port", "smtps_port", "imaps_port"]:
value = randint(1, 65536)
while value in used_ports:
value = randint(65535)
used_ports.append(value)
content.append(f"{config_key} = {value}")
inipath.write_text("\n".join(content))
def get_default_config_content(mail_domain, **overrides): def get_default_config_content(mail_domain, **overrides):
+2 -2
View File
@@ -109,8 +109,8 @@ mail_domain = {mail_domain}
# postal address of privacy contact # postal address of privacy contact
privacy_postal = privacy_postal =
# email address of privacy contact # email address or invite link of admin contact
privacy_mail = admin_contact =
# postal address of the privacy data officer # postal address of the privacy data officer
privacy_pdo = privacy_pdo =
+2 -1
View File
@@ -166,7 +166,7 @@ class Deployer:
return self.put_template(src, dest, **kwargs) return self.put_template(src, dest, **kwargs)
return self.put_file(src, dest) return self.put_file(src, dest)
def put_file(self, src, dest, mode="644"): def put_file(self, src, dest, mode="644", **kwargs):
if isinstance(src, str): if isinstance(src, str):
src = get_resource(src) src = get_resource(src)
res = files.put( res = files.put(
@@ -176,6 +176,7 @@ class Deployer:
user="root", user="root",
group="root", group="root",
mode=mode, mode=mode,
**kwargs,
) )
return self._update_restart_signals(dest, res) return self._update_restart_signals(dest, res)
+5 -4
View File
@@ -164,6 +164,7 @@ class UnboundDeployer(Deployer):
self.put_file( self.put_file(
src=BytesIO(b"nameserver 127.0.0.1\nnameserver 9.9.9.9\n"), src=BytesIO(b"nameserver 127.0.0.1\nnameserver 9.9.9.9\n"),
dest="/etc/resolv.conf", dest="/etc/resolv.conf",
force=True,
) )
server.shell( server.shell(
name="Generate root keys for validating DNSSEC", name="Generate root keys for validating DNSSEC",
@@ -495,15 +496,15 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
if config.tls_cert_mode == "acme": if config.tls_cert_mode == "acme":
port_services.append(("acmetool", 402)) port_services.append(("acmetool", 402))
port_services += [ port_services += [
(["imap-login", "dovecot", "nginx"], config.imap_port), (["imap-login", "dovecot"], 143),
# acmetool previously listened on port 80, # acmetool previously listened on port 80,
# so don't complain during upgrade that moved it to port 402 # so don't complain during upgrade that moved it to port 402
# and gave the port to nginx. # and gave the port to nginx.
(["acmetool", "nginx"], 80), (["acmetool", "nginx"], 80),
("nginx", 443), ("nginx", 443),
(["master", "smtpd", "nginx"], config.smtp_port), (["master", "smtpd"], 465),
(["master", "smtpd", "nginx"], config.smtps_port), (["master", "smtpd"], 587),
(["imap-login", "dovecot", "nginx"], config.imaps_port), (["imap-login", "dovecot"], 993),
("iroh-relay", 3340), ("iroh-relay", 3340),
("mtail", 3903), ("mtail", 3903),
("stats", 3904), ("stats", 3904),
+3 -3
View File
@@ -20,10 +20,10 @@ class FiltermailDeployer(Deployer):
return return
arch = host.get_fact(facts.server.Arch) arch = host.get_fact(facts.server.Arch)
url = f"https://github.com/chatmail/filtermail/releases/download/v0.7.0/filtermail-{arch}" url = f"https://github.com/chatmail/filtermail/releases/download/v0.7.1/filtermail-{arch}"
sha256sum = { sha256sum = {
"x86_64": "451f295a85b3b12dbb0f89e18ec319f742ee46dec218f20f7923bfb017a248bd", "x86_64": "fc2d8141166f8561b9711fb68c5327fc9421f814c46dc69671a4605a95b175c0",
"aarch64": "6833061b2a2028264fdeb32f0a6123e1ff73de57dace125364016300b748452e", "aarch64": "37e52c5ddb373ef29b5ead89658407c53f48d10ce055a2dbd9c606fa1ebd5f7f",
}[arch] }[arch]
self.download_executable(url, self.bin_path, sha256sum) self.download_executable(url, self.bin_path, sha256sum)
@@ -7,14 +7,14 @@
<displayShortName>{{ config.mail_domain }}</displayShortName> <displayShortName>{{ config.mail_domain }}</displayShortName>
<incomingServer type="imap"> <incomingServer type="imap">
<hostname>{{ config.mail_domain }}</hostname> <hostname>{{ config.mail_domain }}</hostname>
<port>{{ config.imaps_port }}</port> <port>993</port>
<socketType>SSL</socketType> <socketType>SSL</socketType>
<authentication>password-cleartext</authentication> <authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
</incomingServer> </incomingServer>
<incomingServer type="imap"> <incomingServer type="imap">
<hostname>{{ config.mail_domain }}</hostname> <hostname>{{ config.mail_domain }}</hostname>
<port>{{ config.imap_port }}</port> <port>143</port>
<socketType>STARTTLS</socketType> <socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication> <authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
@@ -28,14 +28,14 @@
</incomingServer> </incomingServer>
<outgoingServer type="smtp"> <outgoingServer type="smtp">
<hostname>{{ config.mail_domain }}</hostname> <hostname>{{ config.mail_domain }}</hostname>
<port>{{ config.smtps_port }}</port> <port>465</port>
<socketType>SSL</socketType> <socketType>SSL</socketType>
<authentication>password-cleartext</authentication> <authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
</outgoingServer> </outgoingServer>
<outgoingServer type="smtp"> <outgoingServer type="smtp">
<hostname>{{ config.mail_domain }}</hostname> <hostname>{{ config.mail_domain }}</hostname>
<port>{{ config.smtp_port }}</port> <port>587</port>
<socketType>STARTTLS</socketType> <socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication> <authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username> <username>%EMAILADDRESS%</username>
-20
View File
@@ -31,26 +31,6 @@ stream {
~\bimap\b 127.0.0.1:993; ~\bimap\b 127.0.0.1:993;
} }
server {
listen {{ config.smtp_port }};
proxy_pass 127.0.0.1:587;
}
server {
listen {{ config.imap_port }};
proxy_pass 127.0.0.1:143;
}
server {
listen {{ config.smtps_port }};
proxy_pass 127.0.0.1:465;
}
server {
listen {{ config.imaps_port }};
proxy_pass 127.0.0.1:993;
}
server { server {
listen 443; listen 443;
{% if not disable_ipv6 %} {% if not disable_ipv6 %}
+6 -9
View File
@@ -94,15 +94,12 @@ def _build_webpages(src_dir, build_dir, config):
for path in src_dir.iterdir(): for path in src_dir.iterdir():
if path.suffix == ".md": if path.suffix == ".md":
render_vars, content = prepare_template(path) render_vars, content = prepare_template(path)
render_vars["username_min_length"] = int_to_english( if config.privacy_mail.startswith("https://"):
config.username_min_length render_vars["admin_contact"] = f"<a href='{config.privacy_mail}'>{config.privacy_mail}</a>"
) elif "@" in config.privacy_mail:
render_vars["username_max_length"] = int_to_english( render_vars["admin_contact"] = f"<a href='mailto:{config.privacy_mail}'>{config.privacy_mail}</a>"
config.username_max_length else:
) render_vars["admin_contact"] = config.privacy_mail
render_vars["password_min_length"] = int_to_english(
config.password_min_length
)
target = build_dir.joinpath(path.stem + ".html") target = build_dir.joinpath(path.stem + ".html")
# recursive jinja2 rendering # recursive jinja2 rendering
-15
View File
@@ -225,21 +225,6 @@ Accepting and delivering mail
nginx -.SMTP inet:465.-> smtpd-smtps nginx -.SMTP inet:465.-> smtpd-smtps
mta2[Remote relay] -.SMTP inet:25.-> smtpd-smtp mta2[Remote relay] -.SMTP inet:25.-> smtpd-smtp
mta2 -.HTTPS /mxdeliv.-> nginx mta2 -.HTTPS /mxdeliv.-> nginx
style postfix fill:#363
style qmgr fill:#252
style authclean fill:#252
style cleanup fill:#252
style lmtp-filtermail fill:#252
style lmtp fill:#252
style bounce fill:#252
style smtpd-submission fill:#252
style smtpd-smtps fill:#252
style smtpd-reinject-outgoing fill:#252
style smtpd-reinject-incoming fill:#252
style smtpd-smtp fill:#252
style filtermail-outgoing fill:#225
style filtermail-incoming fill:#225
style filtermail-transport fill:#225
Operational details of a chatmail relay Operational details of a chatmail relay
---------------------------------------- ----------------------------------------
+1 -1
View File
@@ -48,7 +48,7 @@ Responsible for the processing of your personal data is:
{{ config.privacy_postal }} {{ config.privacy_postal }}
``` ```
E-mail: {{ config.privacy_mail }} E-mail: {{ admin_contact }}
We have appointed a data protection officer: We have appointed a data protection officer: