Skip to content
Merged
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
39 changes: 24 additions & 15 deletions chatmaild/src/chatmaild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ def read_config(inipath):
class Config:
def __init__(self, inipath, params):
self._inipath = inipath
self.mail_domain = params["mail_domain"]
self.mail_domain_hostname = format_arpa_address(params["mail_domain"])
self.mail_domain_deliverable = format_deliverable_domain(params["mail_domain"])
raw_domain = params["mail_domain"]
self.mail_domain_bare = raw_domain

if is_valid_ipv4(raw_domain):
self.ipv4_relay = raw_domain
self.mail_domain = f"[{raw_domain}]"
self.postfix_myhostname = ipaddress.IPv4Address(raw_domain).reverse_pointer
else:
DomainValidator().validate_domain_re(raw_domain)
self.ipv4_relay = None
self.mail_domain = raw_domain
self.postfix_myhostname = raw_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"]
Expand Down Expand Up @@ -57,7 +67,7 @@ def __init__(self, inipath, params):
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
if "iroh_relay" not in params:
self.iroh_relay = "https://" + params["mail_domain"]
self.iroh_relay = "https://" + raw_domain
self.enable_iroh_relay = True
else:
self.iroh_relay = params["iroh_relay"].strip()
Expand All @@ -83,19 +93,17 @@ def __init__(self, inipath, params):
)
self.tls_cert_mode = "external"
self.tls_cert_path, self.tls_key_path = parts
elif self.mail_domain.startswith("_") or is_valid_ipv4(params["mail_domain"]):
elif raw_domain.startswith("_") or self.ipv4_relay:
self.tls_cert_mode = "self"
self.tls_cert_path = "/etc/ssl/certs/mailserver.pem"
self.tls_key_path = "/etc/ssl/private/mailserver.key"
else:
self.tls_cert_mode = "acme"
self.tls_cert_path = f"/var/lib/acme/live/{self.mail_domain}/fullchain"
self.tls_key_path = f"/var/lib/acme/live/{self.mail_domain}/privkey"
self.tls_cert_path = f"/var/lib/acme/live/{raw_domain}/fullchain"
self.tls_key_path = f"/var/lib/acme/live/{raw_domain}/privkey"

# deprecated option
mbdir = params.get(
"mailboxes_dir", f"/home/vmail/mail/{self.mail_domain_deliverable}"
)
mbdir = params.get("mailboxes_dir", f"/home/vmail/mail/{raw_domain}")
self.mailboxes_dir = Path(mbdir.strip())

# old unused option (except for first migration from sqlite to maildir store)
Expand Down Expand Up @@ -192,15 +200,16 @@ def is_valid_ipv4(address: str) -> bool:
return False



def format_arpa_address(address: str) -> str:
if is_valid_ipv4(address):
return ipaddress.IPv4Address(address).reverse_pointer
DomainValidator().validate_domain_re(address)
return address


def format_deliverable_domain(mail_domain: str) -> str:
if is_valid_ipv4(mail_domain):
return f"[{mail_domain}]"
DomainValidator().validate_domain_re(mail_domain)
return mail_domain
def format_mail_domain(raw_domain: str) -> str:
if is_valid_ipv4(raw_domain):
return f"[{raw_domain}]"
DomainValidator().validate_domain_re(raw_domain)
return raw_domain
4 changes: 2 additions & 2 deletions chatmaild/src/chatmaild/doveauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ def handle_lookup(self, parts):
if namespace == "shared":
if type == "userdb":
user = args[0]
if user.endswith(f"@{config.mail_domain_deliverable}"):
if user.endswith(f"@{config.mail_domain}"):
res = self.lookup_userdb(user)
if res:
reply_command = "O"
else:
reply_command = "N"
elif type == "passdb":
user = args[1]
if user.endswith(f"@{config.mail_domain_deliverable}"):
if user.endswith(f"@{config.mail_domain}"):
res = self.lookup_passdb(user, cleartext_password=args[0])
if res:
reply_command = "O"
Expand Down
10 changes: 4 additions & 6 deletions chatmaild/src/chatmaild/newemail.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ def create_newemail_dict(config: Config):
secrets.choice(ALPHANUMERIC_PUNCT)
for _ in range(config.password_min_length + 3)
)
return dict(
email=f"{user}@{config.mail_domain_deliverable}", password=f"{password}"
)
return dict(email=f"{user}@{config.mail_domain}", password=f"{password}")


def create_dclogin_url(config, email, password):
Expand All @@ -33,9 +31,9 @@ def create_dclogin_url(config, email, password):
Uses ic=3 (AcceptInvalidCertificates) so chatmail clients
can connect to servers with self-signed TLS certificates.
"""
if config.mail_domain != config.mail_domain_deliverable:
imap_host = "&ih=" + config.mail_domain
smtp_host = "&sh=" + config.mail_domain
if config.ipv4_relay:
imap_host = "&ih=" + config.ipv4_relay
smtp_host = "&sh=" + config.ipv4_relay
else:
imap_host = ""
smtp_host = ""
Expand Down
14 changes: 7 additions & 7 deletions chatmaild/src/chatmaild/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from chatmaild.config import (
format_arpa_address,
format_deliverable_domain,
format_mail_domain,
is_valid_ipv4,
parse_size_mb,
read_config,
Expand All @@ -21,12 +21,12 @@ def test_read_config_basic(example_config):
example_config = read_config(inipath)
assert example_config.max_user_send_per_minute == 37
assert example_config.mail_domain == "chat.example.org"
assert example_config.mail_domain_deliverable == "chat.example.org"
assert example_config.ipv4_relay is None


def test_read_config_deliverable(ipv4_config):
assert ipv4_config.mail_domain == "1.3.3.7"
assert ipv4_config.mail_domain_deliverable == "[1.3.3.7]"
def test_read_config_ipv4(ipv4_config):
assert ipv4_config.ipv4_relay == "1.3.3.7"
assert ipv4_config.mail_domain == "[1.3.3.7]"


def test_read_config_basic_using_defaults(tmp_path, maildomain):
Expand Down Expand Up @@ -188,6 +188,6 @@ def test_format_arpa_address(input, result, exception):
("12394142", None, pytest.raises(ValueError)),
],
)
def test_format_deliverable_domain(input, result, exception):
def test_format_mail_domain(input, result, exception):
with exception:
assert result == format_deliverable_domain(input)
assert result == format_mail_domain(input)
20 changes: 6 additions & 14 deletions chatmaild/src/chatmaild/tests/test_doveauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,12 @@ def test_invalid_username_length(example_config):
config.username_min_length = 6
config.username_max_length = 10
password = create_newemail_dict(config)["password"]
assert not is_allowed_to_create(config, f"a1234@{config.mail_domain}", password)
assert is_allowed_to_create(config, f"012345@{config.mail_domain}", password)
assert is_allowed_to_create(config, f"0123456@{config.mail_domain}", password)
assert is_allowed_to_create(config, f"0123456789@{config.mail_domain}", password)
assert not is_allowed_to_create(
config, f"a1234@{config.mail_domain_deliverable}", password
)
assert is_allowed_to_create(
config, f"012345@{config.mail_domain_deliverable}", password
)
assert is_allowed_to_create(
config, f"0123456@{config.mail_domain_deliverable}", password
)
assert is_allowed_to_create(
config, f"0123456789@{config.mail_domain_deliverable}", password
)
assert not is_allowed_to_create(
config, f"0123456789x@{config.mail_domain_deliverable}", password
config, f"0123456789x@{config.mail_domain}", password
)


Expand Down Expand Up @@ -132,7 +124,7 @@ def test_invalid_localpart_characters(make_config):
"""Test that is_allowed_to_create rejects localparts with invalid characters."""
config = make_config("chat.example.org", {"username_min_length": "3"})
password = "zequ0Aimuchoodaechik"
domain = config.mail_domain_deliverable
domain = config.mail_domain

# valid localparts
assert is_allowed_to_create(config, f"abc123@{domain}", password)
Expand Down
2 changes: 1 addition & 1 deletion chatmaild/src/chatmaild/tests/test_filtermail_blackbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def test_one_mail(
print(line.decode("ascii"), file=sys.stderr)
pytest.fail("starting filtermail failed")

addr = f"user1@{config.mail_domain_deliverable}"
addr = f"user1@{config.mail_domain}"
config.get_user(addr).set_password("l1k2j3l1k2j3l")

# send encrypted mail
Expand Down
2 changes: 1 addition & 1 deletion chatmaild/src/chatmaild/tests/test_newmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir, example_conf
assert lines[0] == "Content-Type: application/json"
assert not lines[1]
dic = json.loads(lines[2])
assert dic["email"].endswith(f"@{example_config.mail_domain_deliverable}")
assert dic["email"].endswith(f"@{example_config.mail_domain}")
assert len(dic["password"]) >= 10
# default tls_cert=acme should not include dclogin_url
assert "dclogin_url" not in dic
Expand Down
15 changes: 8 additions & 7 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, is_valid_ipv4
from chatmaild.config import read_config, write_initial_config
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_bare
sshexec = get_sshexec(ssh_host)
require_iroh = args.config.enable_iroh_relay
strict_tls = args.config.tls_cert_mode == "acme"
if is_valid_ipv4(args.config.mail_domain):
if args.config.ipv4_relay:
args.dns_check_disabled = True
if not args.dns_check_disabled:
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
Expand Down Expand Up @@ -121,7 +121,7 @@ def run_cmd(args, out):
elif not args.dns_check_disabled and strict_tls and not remote_data["acme_account_url"]:
out.red("Deploy completed but letsencrypt not configured")
out.red("Run 'cmdeploy run' again")
elif is_valid_ipv4(args.config.mail_domain):
elif args.config.ipv4_relay:
out.green("Deploy completed.")
else:
out.green("Deploy completed, call `cmdeploy dns` next.")
Expand All @@ -144,8 +144,9 @@ def dns_cmd_options(parser):

def dns_cmd(args, out):
"""Check DNS entries and optionally generate dns zone file."""
if is_valid_ipv4(args.config.mail_domain):
print(f"[WARNING] {args.config.mail_domain} is not a domain, skipping DNS checks.")
if args.config.ipv4_relay:
ipv4 = args.config.ipv4_relay
print(f"[WARNING] {ipv4} is not a domain, skipping DNS checks.")
return 0
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
Expand Down Expand Up @@ -184,7 +185,7 @@ def status_cmd_options(parser):
def status_cmd(args, out):
"""Display status for online chatmail instance."""

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_bare
sshexec = get_sshexec(ssh_host, verbose=args.verbose)

out.green(f"chatmail domain: {args.config.mail_domain}")
Expand Down
15 changes: 7 additions & 8 deletions cmdeploy/src/cmdeploy/deployers.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def install(self):

def configure(self):
_configure_remote_venv_with_chatmaild(self.config)
configure_remote_units(self.config.mail_domain, self.units)
configure_remote_units(self.config.mail_domain_bare, self.units)

def activate(self):
activate_remote_units(self.units)
Expand All @@ -482,7 +482,7 @@ class ChatmailDeployer(Deployer):

def __init__(self, config):
self.config = config
self.mail_domain_deliverable = config.mail_domain_deliverable
self.mail_domain = config.mail_domain

def install(self):
files.put(
Expand Down Expand Up @@ -522,7 +522,7 @@ def configure(self):
server.shell(
name="Setup /etc/mailname",
commands=[
f"echo {self.mail_domain_deliverable} >/etc/mailname; chmod 644 /etc/mailname"
f"echo {self.mail_domain} >/etc/mailname; chmod 644 /etc/mailname"
],
)

Expand Down Expand Up @@ -584,8 +584,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
"""
config = read_config(config_path)
check_config(config)
mail_domain = config.mail_domain
mail_domain_deliverable = config.mail_domain_deliverable
bare_host = config.mail_domain_bare

if website_only:
Deployment().perform_stages([WebsiteDeployer(config)])
Expand Down Expand Up @@ -636,21 +635,21 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
)
exit(1)

tls_deployer = get_tls_deployer(config, mail_domain)
tls_deployer = get_tls_deployer(config, bare_host)

all_deployers = [
ChatmailDeployer(config),
LegacyRemoveDeployer(),
FiltermailDeployer(),
JournaldDeployer(),
UnboundDeployer(config),
TurnDeployer(mail_domain),
TurnDeployer(bare_host),
IrohDeployer(config.enable_iroh_relay),
tls_deployer,
WebsiteDeployer(config),
ChatmailVenvDeployer(config),
MtastsDeployer(),
OpendkimDeployer(mail_domain_deliverable),
OpendkimDeployer(config.mail_domain),
# Dovecot should be started before Postfix
# because it creates authentication socket
# required by Postfix.
Expand Down
2 changes: 1 addition & 1 deletion cmdeploy/src/cmdeploy/dovecot/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def install(self):
)

def configure(self):
configure_remote_units(self.config.mail_domain, self.units)
configure_remote_units(self.config.mail_domain_bare, self.units)
config_restart, self.daemon_reload = _configure_dovecot(self.config)
self.need_restart |= config_restart

Expand Down
2 changes: 1 addition & 1 deletion cmdeploy/src/cmdeploy/dovecot/dovecot.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ service imap {
process_limit = 50000
}

mail_server_admin = mailto:root@{{ config.mail_domain_deliverable }}
mail_server_admin = mailto:root@{{ config.mail_domain }}
mail_server_comment = Chatmail server

# `zlib` enables compressing messages stored in the maildir.
Expand Down
6 changes: 3 additions & 3 deletions cmdeploy/src/cmdeploy/opendkim/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
class OpendkimDeployer(Deployer):
required_users = [("opendkim", None, ["opendkim"])]

def __init__(self, mail_domain_deliverable):
self.mail_domain_deliverable = mail_domain_deliverable
def __init__(self, mail_domain):
self.mail_domain = mail_domain

def install(self):
apt.packages(
Expand All @@ -22,7 +22,7 @@ def install(self):
)

def configure(self):
domain = self.mail_domain_deliverable
domain = self.mail_domain
dkim_selector = "opendkim"
"""Configures OpenDKIM"""
need_restart = False
Expand Down
6 changes: 3 additions & 3 deletions cmdeploy/src/cmdeploy/postfix/main.cf.j2
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
myorigin = {{ config.mail_domain_deliverable }}
myorigin = {{ config.mail_domain }}

smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no
Expand Down Expand Up @@ -54,13 +54,13 @@ 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_hostname }}
myhostname = {{ config.postfix_myhostname }}
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases

# When postfix receives mail for $mydestination,
# it hands it over to dovecot via $local_transport.
mydestination = {{ config.mail_domain_deliverable }}
mydestination = {{ config.mail_domain }}
local_transport = lmtp:unix:private/dovecot-lmtp
# postfix doesn't check whether local users exist or not:
local_recipient_maps =
Expand Down
2 changes: 1 addition & 1 deletion cmdeploy/src/cmdeploy/postfix/master.cf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ filter unix - n n - - lmtp
-o syslog_name=postfix/reinject
-o milter_macro_daemon_name=ORIGINATING
-o cleanup_service_name=authclean
{% if config.mail_domain == config.mail_domain_deliverable %} -o smtpd_milters=unix:opendkim/opendkim.sock
{% if not config.ipv4_relay %} -o smtpd_milters=unix:opendkim/opendkim.sock
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.

yeah, that's much prettier.

{% endif %}

# Local SMTP server for reinjecting incoming filtered mail
Expand Down
Loading