Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions chatmaild/src/chatmaild/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ipaddress
import os
from pathlib import Path

Expand All @@ -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"]
Comment thread
missytake marked this conversation as resolved.
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"]
Expand Down Expand Up @@ -76,7 +80,7 @@ def __init__(self, inipath, params):
)
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"
Expand Down Expand Up @@ -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
Comment on lines +166 to +172
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would introduce something like format_maildomain that would check if it is either a domain, IPv4 or IPv6 and then either don't change it in case of a domain, or format it as a domain-literal. It would be nice to accept IPv6, as the only change needed would be adding IPv6: prefix inside the square brackets.

12 changes: 10 additions & 2 deletions chatmaild/src/chatmaild/newemail.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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
Expand All @@ -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():
Expand Down
8 changes: 4 additions & 4 deletions cmdeploy/src/cmdeploy/cmdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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

Expand Down Expand Up @@ -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("]")
Comment thread
missytake marked this conversation as resolved.
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
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions cmdeploy/src/cmdeploy/dovecot/dovecot.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions cmdeploy/src/cmdeploy/postfix/main.cf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 =

Comment thread
missytake marked this conversation as resolved.
relayhost =
{% if disable_ipv6 %}
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmdeploy/src/cmdeploy/postfix/master.cf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions cmdeploy/src/cmdeploy/tests/online/test_0_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
from cmdeploy.cmdeploy import main


def test_init(tmp_path, maildomain):
def test_init(tmp_path, maildomain_sanitized):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the tests i would make maildomain fixture return the "[]"-stripped ip-address so you don't need to change tests much. "config.mail_domain" would still have the "[]" spelling where needed. sshdomain/imap/smtp fixtures can use maildomain directly then.

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):
Expand Down Expand Up @@ -92,7 +92,7 @@ def login_smtp_imap(smtp, imap):
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}")
Expand Down
16 changes: 8 additions & 8 deletions cmdeploy/src/cmdeploy/tests/online/test_0_qr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions cmdeploy/src/cmdeploy/tests/online/test_1_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def test_ls(self, sshexec):
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)
)
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions cmdeploy/src/cmdeploy/tests/online/test_2_deltachat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 19 additions & 14 deletions cmdeploy/src/cmdeploy/tests/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("]")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is good, should be used everywhere.



@pytest.fixture(scope="session")
def sshdomain(maildomain_sanitized):
return os.environ.get("CHATMAIL_SSH", maildomain_sanitized)


@pytest.fixture
Expand All @@ -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():
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -227,14 +232,14 @@ def fetch_all_messages(self):


@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

Expand Down Expand Up @@ -321,8 +326,8 @@ def _make_transport(self, domain):
"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"
Expand Down Expand Up @@ -454,7 +459,7 @@ def gen_users(self, num):

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
Expand Down
Loading