mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
Compare commits
8 Commits
link2xt/an
...
ipv4-only
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1651917a57 | ||
|
|
2ba19fd888 | ||
|
|
26c5e1e847 | ||
|
|
8ae2c475fb | ||
|
|
85223245e4 | ||
|
|
f152b0fca9 | ||
|
|
808eb3e53e | ||
|
|
bbacd74c9f |
@@ -1,3 +1,4 @@
|
||||
import ipaddress
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -20,7 +21,10 @@ def read_config(inipath):
|
||||
class Config:
|
||||
def __init__(self, inipath, params):
|
||||
self._inipath = inipath
|
||||
self.mail_domain = params["mail_domain"]
|
||||
if is_valid_ipv4(params["mail_domain"]):
|
||||
self.mail_domain = f"[{params.get('mail_domain')}]"
|
||||
else:
|
||||
self.mail_domain = params["mail_domain"]
|
||||
self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60))
|
||||
self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10))
|
||||
self.max_mailbox_size = params["max_mailbox_size"]
|
||||
@@ -76,7 +80,7 @@ class Config:
|
||||
)
|
||||
self.tls_cert_mode = "external"
|
||||
self.tls_cert_path, self.tls_key_path = parts
|
||||
elif self.mail_domain.startswith("_"):
|
||||
elif self.mail_domain.startswith("_") or is_valid_ipv4(params["mail_domain"]):
|
||||
self.tls_cert_mode = "self"
|
||||
self.tls_cert_path = "/etc/ssl/certs/mailserver.pem"
|
||||
self.tls_key_path = "/etc/ssl/private/mailserver.key"
|
||||
@@ -157,3 +161,12 @@ def get_default_config_content(mail_domain, **overrides):
|
||||
lines.append(line)
|
||||
content = "\n".join(lines)
|
||||
return content
|
||||
|
||||
|
||||
def is_valid_ipv4(address: str) -> bool:
|
||||
"""Check if a mail_domain is an IPv4 address."""
|
||||
try:
|
||||
ipaddress.IPv4Address(address)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@@ -7,7 +7,7 @@ import secrets
|
||||
import string
|
||||
from urllib.parse import quote
|
||||
|
||||
from chatmaild.config import Config, read_config
|
||||
from chatmaild.config import Config, is_valid_ipv4, read_config
|
||||
|
||||
CONFIG_PATH = "/usr/local/lib/chatmaild/chatmail.ini"
|
||||
ALPHANUMERIC = string.ascii_lowercase + string.digits
|
||||
@@ -31,7 +31,15 @@ def create_dclogin_url(email, password):
|
||||
Uses ic=3 (AcceptInvalidCertificates) so chatmail clients
|
||||
can connect to servers with self-signed TLS certificates.
|
||||
"""
|
||||
return f"dclogin:{quote(email, safe='@')}?p={quote(password, safe='')}&v=1&ic=3"
|
||||
domain = email.split("@")[-1]
|
||||
domain_without_brackets = domain.strip("[").strip("]")
|
||||
if is_valid_ipv4(domain_without_brackets):
|
||||
imap_host = "&ih=" + domain_without_brackets
|
||||
smtp_host = "&sh=" + domain_without_brackets
|
||||
else:
|
||||
imap_host = ""
|
||||
smtp_host = ""
|
||||
return f"dclogin:{quote(email, safe='@[]')}?p={quote(password, safe='')}&v=1{imap_host}{smtp_host}&ic=3"
|
||||
|
||||
|
||||
def print_new_account():
|
||||
|
||||
@@ -13,7 +13,7 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pyinfra
|
||||
from chatmaild.config import read_config, write_initial_config
|
||||
from chatmaild.config import read_config, write_initial_config, is_valid_ipv4
|
||||
from packaging import version
|
||||
from termcolor import colored
|
||||
|
||||
@@ -87,11 +87,11 @@ def run_cmd_options(parser):
|
||||
def run_cmd(args, out):
|
||||
"""Deploy chatmail services on the remote server."""
|
||||
|
||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain.strip("[").strip("]")
|
||||
sshexec = get_sshexec(ssh_host)
|
||||
require_iroh = args.config.enable_iroh_relay
|
||||
strict_tls = args.config.tls_cert_mode == "acme"
|
||||
if not args.dns_check_disabled:
|
||||
if not args.dns_check_disabled and not is_valid_ipv4(args.config.mail_domain.strip("[").strip("]")):
|
||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||
if not dns.check_initial_remote_data(remote_data, strict_tls=strict_tls, print=out.red):
|
||||
return 1
|
||||
@@ -101,7 +101,7 @@ def run_cmd(args, out):
|
||||
env["CHATMAIL_WEBSITE_ONLY"] = "True" if args.website_only else ""
|
||||
env["CHATMAIL_DISABLE_MAIL"] = "True" if args.disable_mail else ""
|
||||
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
||||
if not args.dns_check_disabled:
|
||||
if not args.dns_check_disabled and not is_valid_ipv4(args.config.mail_domain.strip("[").strip("]")):
|
||||
env["CHATMAIL_ADDR_V4"] = remote_data.get("A") or ""
|
||||
env["CHATMAIL_ADDR_V6"] = remote_data.get("AAAA") or ""
|
||||
deploy_path = importlib.resources.files(__package__).joinpath("run.py").resolve()
|
||||
|
||||
@@ -7,6 +7,7 @@ listen = 0.0.0.0
|
||||
protocols = imap lmtp
|
||||
|
||||
auth_mechanisms = plain
|
||||
auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@[]
|
||||
|
||||
{% if debug == true %}
|
||||
auth_verbose = yes
|
||||
|
||||
@@ -54,14 +54,15 @@ smtpd_tls_exclude_ciphers = aNULL, RC4, MD5, DES
|
||||
tls_preempt_cipherlist = yes
|
||||
|
||||
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
|
||||
myhostname = {{ config.mail_domain }}
|
||||
alias_maps = hash:/etc/aliases
|
||||
alias_database = hash:/etc/aliases
|
||||
|
||||
# Postfix does not deliver mail for any domain by itself.
|
||||
# Primary domain is listed in `virtual_mailbox_domains` instead
|
||||
# and handed over to Dovecot.
|
||||
mydestination =
|
||||
mydestination = {{ config.mail_domain }}
|
||||
local_transport = lmtp:unix:private/dovecot-lmtp
|
||||
local_recipient_maps =
|
||||
|
||||
relayhost =
|
||||
{% if disable_ipv6 %}
|
||||
@@ -88,8 +89,6 @@ inet_protocols = ipv4
|
||||
inet_protocols = all
|
||||
{% endif %}
|
||||
|
||||
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||
virtual_mailbox_domains = {{ config.mail_domain }}
|
||||
lmtp_header_checks = regexp:/etc/postfix/lmtp_header_cleanup
|
||||
|
||||
mua_client_restrictions = permit_sasl_authenticated, reject
|
||||
|
||||
@@ -80,7 +80,9 @@ filter unix - n n - - lmtp
|
||||
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
|
||||
-o syslog_name=postfix/reinject
|
||||
-o milter_macro_daemon_name=ORIGINATING
|
||||
{% if "[" not in config.mail_domain %}
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
{% endif %}
|
||||
-o cleanup_service_name=authclean
|
||||
|
||||
# Local SMTP server for reinjecting incoming filtered mail
|
||||
|
||||
@@ -8,11 +8,11 @@ from chatmaild.config import read_config
|
||||
from cmdeploy.cmdeploy import main
|
||||
|
||||
|
||||
def test_init(tmp_path, maildomain):
|
||||
def test_init(tmp_path, maildomain_sanitized):
|
||||
inipath = tmp_path.joinpath("chatmail.ini")
|
||||
main(["init", "--config", str(inipath), maildomain])
|
||||
main(["init", "--config", str(inipath), maildomain_sanitized])
|
||||
config = read_config(inipath)
|
||||
assert config.mail_domain == maildomain
|
||||
assert config.mail_domain.strip("[").strip("]") == maildomain_sanitized
|
||||
|
||||
|
||||
def test_capabilities(imap):
|
||||
@@ -92,7 +92,7 @@ def test_concurrent_logins_same_account(
|
||||
def test_no_vrfy(chatmail_config):
|
||||
domain = chatmail_config.mail_domain
|
||||
|
||||
s = smtplib.SMTP(domain)
|
||||
s = smtplib.SMTP(domain.strip("[").strip("]"))
|
||||
s.starttls()
|
||||
|
||||
s.putcmd("vrfy", f"wrongaddress@{chatmail_config.mail_domain}")
|
||||
|
||||
@@ -10,31 +10,31 @@ def test_gen_qr_png_data(maildomain):
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning")
|
||||
def test_fastcgi_working(maildomain, chatmail_config):
|
||||
url = f"https://{maildomain}/new"
|
||||
def test_fastcgi_working(maildomain_sanitized, chatmail_config):
|
||||
url = f"https://{maildomain_sanitized}/new"
|
||||
print(url)
|
||||
verify = chatmail_config.tls_cert_mode == "acme"
|
||||
res = requests.post(url, verify=verify)
|
||||
assert maildomain in res.json().get("email")
|
||||
assert maildomain_sanitized in res.json().get("email")
|
||||
assert len(res.json().get("password")) > chatmail_config.password_min_length
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::urllib3.exceptions.InsecureRequestWarning")
|
||||
def test_newemail_configure(maildomain, rpc, chatmail_config):
|
||||
def test_newemail_configure(maildomain_sanitized, rpc, chatmail_config):
|
||||
"""Test configuring accounts by scanning a QR code works."""
|
||||
url = f"DCACCOUNT:https://{maildomain}/new"
|
||||
url = f"DCACCOUNT:https://{maildomain_sanitized}/new"
|
||||
for i in range(3):
|
||||
account_id = rpc.add_account()
|
||||
if chatmail_config.tls_cert_mode == "self":
|
||||
# deltachat core's rustls rejects self-signed HTTPS certs during
|
||||
# set_config_from_qr, so fetch credentials via requests instead
|
||||
res = requests.post(f"https://{maildomain}/new", verify=False)
|
||||
res = requests.post(f"https://{maildomain_sanitized}/new", verify=False)
|
||||
data = res.json()
|
||||
rpc.add_or_update_transport(account_id, {
|
||||
"addr": data["email"],
|
||||
"password": data["password"],
|
||||
"imapServer": maildomain,
|
||||
"smtpServer": maildomain,
|
||||
"imapServer": maildomain_sanitized,
|
||||
"smtpServer": maildomain_sanitized,
|
||||
"certificateChecks": "acceptInvalidCertificates",
|
||||
})
|
||||
else:
|
||||
|
||||
@@ -21,6 +21,8 @@ class TestSSHExecutor:
|
||||
assert out == out2
|
||||
|
||||
def test_perform_initial(self, sshexec, maildomain):
|
||||
if "[" in maildomain:
|
||||
pytest.skip("Relay doesn't have a domain")
|
||||
res = sshexec(
|
||||
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
||||
)
|
||||
@@ -131,7 +133,7 @@ def test_authenticated_from(cmsetup, maildata):
|
||||
|
||||
@pytest.mark.parametrize("from_addr", ["fake@example.org", "fake@testrun.org"])
|
||||
def test_reject_missing_dkim(cmsetup, maildata, from_addr):
|
||||
domain = cmsetup.maildomain
|
||||
domain = cmsetup.maildomain.strip("[").strip("]")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
try:
|
||||
@@ -143,7 +145,7 @@ def test_reject_missing_dkim(cmsetup, maildata, from_addr):
|
||||
msg = maildata(
|
||||
"encrypted.eml", from_addr=from_addr, to_addr=recipient.addr
|
||||
).as_string()
|
||||
conn = smtplib.SMTP(cmsetup.maildomain, 25, timeout=10)
|
||||
conn = smtplib.SMTP(cmsetup.maildomain.strip("[").strip("]"), 25, timeout=10)
|
||||
conn.starttls()
|
||||
|
||||
with conn as s:
|
||||
|
||||
@@ -15,7 +15,7 @@ def imap_mailbox(cmfactory, ssl_context):
|
||||
(ac1,) = cmfactory.get_online_accounts(1)
|
||||
user = ac1.get_config("addr")
|
||||
password = ac1.get_config("mail_pw")
|
||||
host = user.split("@")[1]
|
||||
host = user.split("@")[1].strip("[").strip("]")
|
||||
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
|
||||
mailbox.login(user, password)
|
||||
mailbox.dc_ac = ac1
|
||||
@@ -178,7 +178,7 @@ def test_hide_senders_ip_address(cmfactory, ssl_context):
|
||||
chat.send_text("testing submission header cleanup")
|
||||
user2.wait_for_incoming_msg()
|
||||
addr = user2.get_config("addr")
|
||||
host = addr.split("@")[1]
|
||||
host = addr.split("@")[1].strip("[").strip("]")
|
||||
pw = user2.get_config("mail_pw")
|
||||
mailbox = imap_tools.MailBox(host, ssl_context=ssl_context)
|
||||
mailbox.login(addr, pw)
|
||||
|
||||
@@ -61,8 +61,13 @@ def maildomain(chatmail_config):
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sshdomain(maildomain):
|
||||
return os.environ.get("CHATMAIL_SSH", maildomain)
|
||||
def maildomain_sanitized(maildomain):
|
||||
return maildomain.strip("[").strip("]")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sshdomain(maildomain_sanitized):
|
||||
return os.environ.get("CHATMAIL_SSH", maildomain_sanitized)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -75,7 +80,7 @@ def maildomain2():
|
||||
|
||||
@pytest.fixture
|
||||
def sshdomain2(maildomain2):
|
||||
return os.environ.get("CHATMAIL_SSH2", maildomain2)
|
||||
return os.environ.get("CHATMAIL_SSH2", maildomain2.strip("[").strip("]"))
|
||||
|
||||
|
||||
def pytest_report_header():
|
||||
@@ -176,14 +181,14 @@ def ssl_context(chatmail_config):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def imap(maildomain, ssl_context):
|
||||
return ImapConn(maildomain, ssl_context=ssl_context)
|
||||
def imap(maildomain_sanitized, ssl_context):
|
||||
return ImapConn(maildomain_sanitized, ssl_context=ssl_context)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_imap_connection(maildomain, ssl_context):
|
||||
def make_imap_connection(maildomain_sanitized, ssl_context):
|
||||
def make_imap_connection():
|
||||
conn = ImapConn(maildomain, ssl_context=ssl_context)
|
||||
conn = ImapConn(maildomain_sanitized, ssl_context=ssl_context)
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
@@ -227,14 +232,14 @@ class ImapConn:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def smtp(maildomain, ssl_context):
|
||||
return SmtpConn(maildomain, ssl_context=ssl_context)
|
||||
def smtp(maildomain_sanitized, ssl_context):
|
||||
return SmtpConn(maildomain_sanitized, ssl_context=ssl_context)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_smtp_connection(maildomain, ssl_context):
|
||||
def make_smtp_connection(maildomain_sanitized, ssl_context):
|
||||
def make_smtp_connection():
|
||||
conn = SmtpConn(maildomain, ssl_context=ssl_context)
|
||||
conn = SmtpConn(maildomain_sanitized, ssl_context=ssl_context)
|
||||
conn.connect()
|
||||
return conn
|
||||
|
||||
@@ -321,8 +326,8 @@ class ChatmailACFactory:
|
||||
"password": password,
|
||||
# Setting server explicitly skips requesting autoconfig XML,
|
||||
# see https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/
|
||||
"imapServer": domain,
|
||||
"smtpServer": domain,
|
||||
"imapServer": domain.strip("[").strip("]"),
|
||||
"smtpServer": domain.strip("[").strip("]"),
|
||||
}
|
||||
if self.chatmail_config.tls_cert_mode == "self":
|
||||
transport["certificateChecks"] = "acceptInvalidCertificates"
|
||||
@@ -454,7 +459,7 @@ class CMSetup:
|
||||
|
||||
class CMUser:
|
||||
def __init__(self, maildomain, addr, password, ssl_context=None):
|
||||
self.maildomain = maildomain
|
||||
self.maildomain = maildomain.strip("[").strip("]")
|
||||
self.addr = addr
|
||||
self.password = password
|
||||
self.ssl_context = ssl_context
|
||||
|
||||
Reference in New Issue
Block a user