Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
2e616a7b5c ci: delete old files when backing up acmetool state and DKIM keys 2025-12-17 11:57:58 +01:00
20 changed files with 128 additions and 312 deletions

View File

@@ -19,8 +19,13 @@ jobs:
environment: environment:
name: staging-ipv4.testrun.org name: staging-ipv4.testrun.org
url: https://staging-ipv4.testrun.org/ url: https://staging-ipv4.testrun.org/
concurrency: staging-ipv4.testrun.org concurrency:
group: ci-ipv4-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ !contains(github.ref, '$GITHUB_REF') }}
steps: steps:
- uses: jsok/serialize-workflow-action@515cd04c46d7ea7435c4a22a3b4419127afdefe9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: prepare SSH - name: prepare SSH
@@ -30,11 +35,11 @@ jobs:
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan staging-ipv4.testrun.org > ~/.ssh/known_hosts ssh-keyscan staging-ipv4.testrun.org > ~/.ssh/known_hosts
# save previous acme & dkim state # save previous acme & dkim state
rsync -avz root@staging-ipv4.testrun.org:/var/lib/acme acme-ipv4 || true rsync -avz --delete root@staging-ipv4.testrun.org:/var/lib/acme acme-ipv4
rsync -avz root@staging-ipv4.testrun.org:/etc/dkimkeys dkimkeys-ipv4 || true rsync -avz --delete root@staging-ipv4.testrun.org:/etc/dkimkeys dkimkeys-ipv4
# store previous acme & dkim state on ns.testrun.org, if it contains useful certs # store previous acme & dkim state on ns.testrun.org, if it contains useful certs
if [ -f dkimkeys-ipv4/dkimkeys/opendkim.private ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys-ipv4 root@ns.testrun.org:/tmp/ || true; fi test -f dkimkeys-ipv4/dkimkeys/opendkim.private && rsync -avz --delete -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys-ipv4 root@ns.testrun.org:/tmp/
if [ "$(ls -A acme-ipv4/acme/certs)" ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" acme-ipv4 root@ns.testrun.org:/tmp/ || true; fi test -n "$(ls -A acme-ipv4/acme/certs)" && rsync -avz --delete -e "ssh -o StrictHostKeyChecking=accept-new" acme-ipv4 root@ns.testrun.org:/tmp/
# make sure CAA record isn't set # make sure CAA record isn't set
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone scp -o StrictHostKeyChecking=accept-new .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone
ssh root@ns.testrun.org sed -i '/CAA/d' /etc/nsd/staging-ipv4.testrun.org.zone ssh root@ns.testrun.org sed -i '/CAA/d' /etc/nsd/staging-ipv4.testrun.org.zone
@@ -88,7 +93,7 @@ jobs:
ssh root@ns.testrun.org systemctl reload nsd ssh root@ns.testrun.org systemctl reload nsd
- name: cmdeploy test - name: cmdeploy test
run: CHATMAIL_DOMAIN2=ci-chatmail.testrun.org cmdeploy test --slow run: CHATMAIL_DOMAIN2=nine.testrun.org cmdeploy test --slow
- name: cmdeploy dns - name: cmdeploy dns
run: cmdeploy dns -v run: cmdeploy dns -v

View File

@@ -19,8 +19,13 @@ jobs:
environment: environment:
name: staging2.testrun.org name: staging2.testrun.org
url: https://staging2.testrun.org/ url: https://staging2.testrun.org/
concurrency: staging2.testrun.org concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ !contains(github.ref, '$GITHUB_REF') }}
steps: steps:
- uses: jsok/serialize-workflow-action@515cd04c46d7ea7435c4a22a3b4419127afdefe9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: prepare SSH - name: prepare SSH
@@ -30,11 +35,11 @@ jobs:
chmod 600 ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519
ssh-keyscan staging2.testrun.org > ~/.ssh/known_hosts ssh-keyscan staging2.testrun.org > ~/.ssh/known_hosts
# save previous acme & dkim state # save previous acme & dkim state
rsync -avz root@staging2.testrun.org:/var/lib/acme . || true rsync -avz --delete root@staging2.testrun.org:/var/lib/acme .
rsync -avz root@staging2.testrun.org:/etc/dkimkeys . || true rsync -avz --delete root@staging2.testrun.org:/etc/dkimkeys .
# store previous acme & dkim state on ns.testrun.org, if it contains useful certs # store previous acme & dkim state on ns.testrun.org, if it contains useful certs
if [ -f dkimkeys/opendkim.private ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys root@ns.testrun.org:/tmp/ || true; fi test -f dkimkeys/opendkim.private && rsync -avz --delete -e "ssh -o StrictHostKeyChecking=accept-new" dkimkeys root@ns.testrun.org:/tmp/
if [ "$(ls -A acme/certs)" ]; then rsync -avz -e "ssh -o StrictHostKeyChecking=accept-new" acme root@ns.testrun.org:/tmp/ || true; fi test -n "$(ls -A acme/certs)" && rsync -avz --delete -e "ssh -o StrictHostKeyChecking=accept-new" acme root@ns.testrun.org:/tmp/
# make sure CAA record isn't set # make sure CAA record isn't set
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging2.testrun.org.zone scp -o StrictHostKeyChecking=accept-new .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging2.testrun.org.zone
ssh root@ns.testrun.org sed -i '/CAA/d' /etc/nsd/staging2.testrun.org.zone ssh root@ns.testrun.org sed -i '/CAA/d' /etc/nsd/staging2.testrun.org.zone
@@ -89,7 +94,7 @@ jobs:
ssh root@ns.testrun.org systemctl reload nsd ssh root@ns.testrun.org systemctl reload nsd
- name: cmdeploy test - name: cmdeploy test
run: CHATMAIL_DOMAIN2=ci-chatmail.testrun.org cmdeploy test --slow run: CHATMAIL_DOMAIN2=nine.testrun.org cmdeploy test --slow
- name: cmdeploy dns - name: cmdeploy dns
run: cmdeploy dns -v run: cmdeploy dns -v

View File

@@ -1,32 +1,6 @@
# Changelog for chatmail deployment # Changelog for chatmail deployment
## 1.9.0 2025-12-18 ## untagged
### Documentation
- Add RELEASE.md and CONTRIBUTING.md
- README update, mention Chatmail Cookbook project
### Bug Fixes
- Expire messages also from IMAP subfolders
- Use absolute path instead of relative path in message expiration script
- Restart Postfix and Dovecot automatically on failure
- acmetool: Use a fixed name and `reconcile` instead of `want`
### Features
- Report DKIM error code in SMTP response
- Remove development notice from the web pages
### Miscellaneous Tasks
- Update the heading in the CHANGELOG.md
- Setup git-cliff
- Run tests against ci-chatmail.testrun.org instead of nine.testrun.org
- Cleanup remaining echobot code, remove echobot user from deployment and passthrough recipients
## 1.8.0 2025-12-12
- Add imap_compress option to chatmail.ini - Add imap_compress option to chatmail.ini
([#760](https://github.com/chatmail/relay/pull/760)) ([#760](https://github.com/chatmail/relay/pull/760))

View File

@@ -1,7 +0,0 @@
# Contributing to the chatmail relay
Commit messages follow the [Conventional Commits] notation.
We use [git-cliff] to generate the changelog from commit messages before the release.
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/

View File

@@ -1,15 +0,0 @@
# Releasing a new version of chatmail relay
For example, to release version 1.9.0 of chatmail relay, do the following steps.
1. Update the changelog: `git cliff --unreleased --tag 1.9.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.9.0 -p CHANGELOG.md`.
2. Open the changelog in the editor, edit it if required.
3. Commit the changes to the changelog with a commit message `chore(release): prepare for 1.9.0`.
3. Tag the release: `git tag --annotate 1.9.0`.
4. Push the release tag: `git push origin 1.9.0`.
5. Create a GitHub release: `gh release create 1.9.0`.

View File

@@ -14,7 +14,7 @@ from stat import S_ISREG
from chatmaild.config import read_config from chatmaild.config import read_config
FileEntry = namedtuple("FileEntry", ("path", "mtime", "size")) FileEntry = namedtuple("FileEntry", ("relpath", "mtime", "size"))
def iter_mailboxes(basedir, maxnum): def iter_mailboxes(basedir, maxnum):
@@ -51,27 +51,33 @@ class MailboxStat:
def __init__(self, basedir): def __init__(self, basedir):
self.basedir = str(basedir) self.basedir = str(basedir)
# all detected messages in cur/new/tmp folders
self.messages = [] self.messages = []
self.extrafiles = []
self.scandir(self.basedir)
def scandir(self, folderdir): # all detected files in mailbox top dir
for name in os_listdir_if_exists(folderdir): self.extrafiles = []
path = f"{folderdir}/{name}"
# scan all relevant files (without recursion)
old_cwd = os.getcwd()
try:
os.chdir(self.basedir)
except FileNotFoundError:
return
for name in os_listdir_if_exists("."):
if name in ("cur", "new", "tmp"): if name in ("cur", "new", "tmp"):
for msg_name in os_listdir_if_exists(path): for msg_name in os_listdir_if_exists(name):
entry = get_file_entry(f"{path}/{msg_name}") entry = get_file_entry(f"{name}/{msg_name}")
if entry is not None: if entry is not None:
self.messages.append(entry) self.messages.append(entry)
elif os.path.isdir(path):
self.scandir(path)
else: else:
entry = get_file_entry(path) entry = get_file_entry(name)
if entry is not None: if entry is not None:
self.extrafiles.append(entry) self.extrafiles.append(entry)
if name == "password": if name == "password":
self.last_login = entry.mtime self.last_login = entry.mtime
self.extrafiles.sort(key=lambda x: -x.size) self.extrafiles.sort(key=lambda x: -x.size)
os.chdir(old_cwd)
def print_info(msg): def print_info(msg):
@@ -124,6 +130,13 @@ class Expiry:
self.remove_mailbox(mbox.basedir) self.remove_mailbox(mbox.basedir)
return return
# all to-be-removed files are relative to the mailbox basedir
try:
os.chdir(mbox.basedir)
except FileNotFoundError:
print_info(f"mailbox not found/vanished {mbox.basedir}")
return
mboxname = os.path.basename(mbox.basedir) mboxname = os.path.basename(mbox.basedir)
if self.verbose: if self.verbose:
date = datetime.fromtimestamp(mbox.last_login) if mbox.last_login else None date = datetime.fromtimestamp(mbox.last_login) if mbox.last_login else None
@@ -134,12 +147,11 @@ class Expiry:
self.all_files += len(mbox.messages) self.all_files += len(mbox.messages)
for message in mbox.messages: for message in mbox.messages:
if message.mtime < cutoff_mails: if message.mtime < cutoff_mails:
self.remove_file(message.path, mtime=message.mtime) self.remove_file(message.relpath, mtime=message.mtime)
elif message.size > 200000 and message.mtime < cutoff_large_mails: elif message.size > 200000 and message.mtime < cutoff_large_mails:
# we only remove noticed large files (not unnoticed ones in new/) # we only remove noticed large files (not unnoticed ones in new/)
parts = message.path.split("/") if message.relpath.startswith("cur/"):
if len(parts) >= 2 and parts[-2] == "cur": self.remove_file(message.relpath, mtime=message.mtime)
self.remove_file(message.path, mtime=message.mtime)
else: else:
continue continue
changed = True changed = True

View File

@@ -307,9 +307,12 @@ class IncomingBeforeQueueHandler:
return error return error
log_info("re-injecting the mail that passed checks") log_info("re-injecting the mail that passed checks")
# the smtp daemon on reinject_port_incoming gives it to dkim milter
# which looks at source address to determine whether to verify or sign
client = SMTPClient( client = SMTPClient(
"localhost", "localhost",
self.config.postfix_reinject_port_incoming, self.config.postfix_reinject_port_incoming,
source_address=("127.0.0.2", 0),
) )
client.sendmail( client.sendmail(
envelope.mail_from, envelope.rcpt_tos, envelope.original_content envelope.mail_from, envelope.rcpt_tos, envelope.original_content

View File

@@ -15,7 +15,7 @@ mail_domain = {mail_domain}
max_user_send_per_minute = 60 max_user_send_per_minute = 60
# maximum mailbox size of a chatmail address # maximum mailbox size of a chatmail address
max_mailbox_size = 500M max_mailbox_size = 100M
# maximum message size for an e-mail in bytes # maximum message size for an e-mail in bytes
max_message_size = 31457280 max_message_size = 31457280

View File

@@ -33,7 +33,7 @@ def test_read_config_testrun(make_config):
assert config.filtermail_smtp_port == 10080 assert config.filtermail_smtp_port == 10080
assert config.postfix_reinject_port == 10025 assert config.postfix_reinject_port == 10025
assert config.max_user_send_per_minute == 60 assert config.max_user_send_per_minute == 60
assert config.max_mailbox_size == "500M" assert config.max_mailbox_size == "100M"
assert config.delete_mails_after == "20" assert config.delete_mails_after == "20"
assert config.delete_large_after == "7" assert config.delete_large_after == "7"
assert config.username_min_length == 9 assert config.username_min_length == 9

View File

@@ -17,17 +17,19 @@ from chatmaild.expire import main as expiry_main
from chatmaild.fsreport import main as report_main from chatmaild.fsreport import main as report_main
def fill_mbox(folderdir): def fill_mbox(basedir):
password = folderdir.joinpath("password") basedir1 = basedir.joinpath("mailbox1@example.org")
basedir1.mkdir()
password = basedir1.joinpath("password")
password.write_text("xxx") password.write_text("xxx")
folderdir.joinpath("maildirsize").write_text("xxx") basedir1.joinpath("maildirsize").write_text("xxx")
garbagedir = folderdir.joinpath("garbagedir") garbagedir = basedir1.joinpath("garbagedir")
garbagedir.mkdir() garbagedir.mkdir()
garbagedir.joinpath("bimbum").write_text("hello")
create_new_messages(folderdir, ["cur/msg1"], size=500) create_new_messages(basedir1, ["cur/msg1"], size=500)
create_new_messages(folderdir, ["new/msg2"], size=600) create_new_messages(basedir1, ["new/msg2"], size=600)
return basedir1
def create_new_messages(basedir, relpaths, size=1000, days=0): def create_new_messages(basedir, relpaths, size=1000, days=0):
@@ -43,21 +45,8 @@ def create_new_messages(basedir, relpaths, size=1000, days=0):
@pytest.fixture @pytest.fixture
def mbox1(example_config): def mbox1(example_config):
mboxdir = example_config.mailboxes_dir.joinpath("mailbox1@example.org") basedir1 = fill_mbox(example_config.mailboxes_dir)
mboxdir.mkdir() return MailboxStat(basedir1)
fill_mbox(mboxdir)
return MailboxStat(mboxdir)
def test_deltachat_folder(example_config):
"""Test old setups that might have a .DeltaChat folder where messages also need to get removed."""
mboxdir = example_config.mailboxes_dir.joinpath("mailbox1@example.org")
mboxdir.mkdir()
mbox2dir = mboxdir.joinpath(".DeltaChat")
mbox2dir.mkdir()
fill_mbox(mbox2dir)
mb = MailboxStat(mboxdir)
assert len(mb.messages) == 2
def test_filentry_ordering(tmp_path): def test_filentry_ordering(tmp_path):
@@ -87,7 +76,7 @@ def test_stats_mailbox(mbox1):
create_new_messages(mbox1.basedir, ["large-extra"], size=1000) create_new_messages(mbox1.basedir, ["large-extra"], size=1000)
create_new_messages(mbox1.basedir, ["index-something"], size=3) create_new_messages(mbox1.basedir, ["index-something"], size=3)
mbox2 = MailboxStat(mbox1.basedir) mbox2 = MailboxStat(mbox1.basedir)
assert len(mbox2.extrafiles) == 5 assert len(mbox2.extrafiles) == 4
assert mbox2.extrafiles[0].size == 1000 assert mbox2.extrafiles[0].size == 1000
# cope well with mailbox dirs that have no password (for whatever reason) # cope well with mailbox dirs that have no password (for whatever reason)

View File

@@ -1,94 +0,0 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[changelog]
# A Tera template to be rendered for each release in the changelog.
# See https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}
"""
# Remove leading and trailing whitespaces from the changelog's body.
trim = true
# Render body even when there are no releases to process.
render_always = true
# An array of regex based postprocessors to modify the changelog.
postprocessors = [
# Replace the placeholder <REPO> with a URL.
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# Parse commits according to the conventional commits specification.
# See https://www.conventionalcommits.org
conventional_commits = true
# Exclude commits that do not match the conventional commits specification.
filter_unconventional = true
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
# An array of regex based parsers to modify commit messages prior to further processing.
commit_preprocessors = [
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit message using https://github.com/crate-ci/typos.
# If the spelling is incorrect, it will be fixed automatically.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# Prevent commits that are breaking from being excluded by commit parsers.
protect_breaking_commits = false
# An array of regex based parsers for extracting data from the commit message.
# Assigns commits to groups.
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^docs", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^style", group = "Styling" },
{ message = "^test", group = "Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "Miscellaneous Tasks" },
{ body = ".*security", group = "Security" },
{ message = "^revert", group = "Revert" },
{ message = ".*", group = "Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
# Fail on a commit that is not matched by any commit parser.
fail_on_unmatched_commit = false
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
use_branch_tags = false
# Order releases topologically instead of chronologically.
topo_order = false
# Order commits topologically instead of chronologically.
topo_order_commits = true
# Order of commits in each group/release within the changelog.
# Allowed values: newest, oldest
sort_commits = "oldest"
# Process submodules commits
recurse_submodules = false

View File

@@ -13,8 +13,6 @@ from cmdeploy.basedeploy import (
class DovecotDeployer(Deployer): class DovecotDeployer(Deployer):
daemon_reload = False
def __init__(self, config, disable_mail): def __init__(self, config, disable_mail):
self.config = config self.config = config
self.disable_mail = disable_mail self.disable_mail = disable_mail
@@ -29,7 +27,7 @@ class DovecotDeployer(Deployer):
def configure(self): def configure(self):
configure_remote_units(self.config.mail_domain, self.units) configure_remote_units(self.config.mail_domain, self.units)
self.need_restart, self.daemon_reload = _configure_dovecot(self.config) self.need_restart = _configure_dovecot(self.config)
def activate(self): def activate(self):
activate_remote_units(self.units) activate_remote_units(self.units)
@@ -44,7 +42,6 @@ class DovecotDeployer(Deployer):
running=False if self.disable_mail else True, running=False if self.disable_mail else True,
enabled=False if self.disable_mail else True, enabled=False if self.disable_mail else True,
restarted=restart, restarted=restart,
daemon_reload=self.daemon_reload,
) )
self.need_restart = False self.need_restart = False
@@ -83,10 +80,9 @@ def _install_dovecot_package(package: str, arch: str):
apt.deb(name=f"Install dovecot-{package}", src=deb_filename) apt.deb(name=f"Install dovecot-{package}", src=deb_filename)
def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool): def _configure_dovecot(config: Config, debug: bool = False) -> bool:
"""Configures Dovecot IMAP server.""" """Configures Dovecot IMAP server."""
need_restart = False need_restart = False
daemon_reload = False
main_config = files.template( main_config = files.template(
src=get_resource("dovecot/dovecot.conf.j2"), src=get_resource("dovecot/dovecot.conf.j2"),
@@ -138,11 +134,4 @@ def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):
) )
need_restart |= timezone_env.changed need_restart |= timezone_env.changed
restart_conf = files.put( return need_restart
name="dovecot: restart automatically on failure",
src=get_resource("service/10_restart.conf"),
dest="/etc/systemd/system/dovecot.service.d/10_restart.conf",
)
daemon_reload |= restart_conf.changed
return need_restart, daemon_reload

View File

@@ -29,7 +29,6 @@ stream {
default 127.0.0.1:8443; default 127.0.0.1:8443;
~\bsmtp\b 127.0.0.1:465; ~\bsmtp\b 127.0.0.1:465;
~\bimap\b 127.0.0.1:993; ~\bimap\b 127.0.0.1:993;
~\bssh\b 127.0.0.1:22;
} }
server { server {

View File

@@ -1,5 +1,4 @@
mtaname = odkim.get_mtasymbol(ctx, "{daemon_name}") if odkim.internal_ip(ctx) == 1 then
if mtaname == "ORIGINATING" then
-- Outgoing message will be signed, -- Outgoing message will be signed,
-- no need to look for signatures. -- no need to look for signatures.
return nil return nil

View File

@@ -65,9 +65,3 @@ PidFile /run/opendkim/opendkim.pid
# The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided # The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided
# by the package dns-root-data. # by the package dns-root-data.
TrustAnchorFile /usr/share/dns/root.key TrustAnchorFile /usr/share/dns/root.key
# Sign messages when `-o milter_macro_daemon_name=ORIGINATING` is set.
MTA ORIGINATING
# No hosts are treated as internal, ORIGINATING daemon name should be set explicitly.
InternalHosts -

View File

@@ -5,7 +5,6 @@ from cmdeploy.basedeploy import Deployer, get_resource
class PostfixDeployer(Deployer): class PostfixDeployer(Deployer):
required_users = [("postfix", None, ["opendkim"])] required_users = [("postfix", None, ["opendkim"])]
daemon_reload = False
def __init__(self, config, disable_mail): def __init__(self, config, disable_mail):
self.config = config self.config = config
@@ -61,13 +60,6 @@ class PostfixDeployer(Deployer):
mode="644", mode="644",
) )
need_restart |= login_map.changed need_restart |= login_map.changed
restart_conf = files.put(
name="postfix: restart automatically on failure",
src=get_resource("service/10_restart.conf"),
dest="/etc/systemd/system/dovecot.service.d/10_restart.conf",
)
self.daemon_reload = restart_conf.changed
self.need_restart = need_restart self.need_restart = need_restart
def activate(self): def activate(self):
@@ -81,6 +73,5 @@ class PostfixDeployer(Deployer):
running=False if self.disable_mail else True, running=False if self.disable_mail else True,
enabled=False if self.disable_mail else True, enabled=False if self.disable_mail else True,
restarted=restart, restarted=restart,
daemon_reload=self.daemon_reload,
) )
self.need_restart = False self.need_restart = False

View File

@@ -31,6 +31,7 @@ submission inet n - y - 5000 smtpd
-o smtpd_sender_restrictions=$mua_sender_restrictions -o smtpd_sender_restrictions=$mua_sender_restrictions
-o smtpd_recipient_restrictions= -o smtpd_recipient_restrictions=
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_client_connection_count_limit=1000 -o smtpd_client_connection_count_limit=1000
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }} -o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
smtps inet n - y - 5000 smtpd smtps inet n - y - 5000 smtpd
@@ -48,6 +49,7 @@ smtps inet n - y - 5000 smtpd
-o smtpd_recipient_restrictions= -o smtpd_recipient_restrictions=
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_client_connection_count_limit=1000 -o smtpd_client_connection_count_limit=1000
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }} -o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
#628 inet n - y - - qmqpd #628 inet n - y - - qmqpd
pickup unix n - y 60 1 pickup pickup unix n - y 60 1 pickup
@@ -79,7 +81,6 @@ filter unix - n n - - lmtp
# Local SMTP server for reinjecting outgoing filtered mail. # Local SMTP server for reinjecting outgoing filtered mail.
127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd 127.0.0.1:{{ config.postfix_reinject_port }} inet n - n - 100 smtpd
-o syslog_name=postfix/reinject -o syslog_name=postfix/reinject
-o milter_macro_daemon_name=ORIGINATING
-o smtpd_milters=unix:opendkim/opendkim.sock -o smtpd_milters=unix:opendkim/opendkim.sock
-o cleanup_service_name=authclean -o cleanup_service_name=authclean

View File

@@ -1,3 +0,0 @@
[Service]
Restart=always
RestartSec=30

View File

@@ -40,10 +40,10 @@ steps. Please substitute it with your own domain.
:: ::
chat.example.org. 3600 IN A 198.51.100.5 chat.example.com. 3600 IN A 198.51.100.5
chat.example.org. 3600 IN AAAA 2001:db8::5 chat.example.com. 3600 IN AAAA 2001:db8::5
www.chat.example.org. 3600 IN CNAME chat.example.org. www.chat.example.com. 3600 IN CNAME chat.example.com.
mta-sts.chat.example.org. 3600 IN CNAME chat.example.org. mta-sts.chat.example.com. 3600 IN CNAME chat.example.com.
2. On your local PC, clone the repository and bootstrap the Python 2. On your local PC, clone the repository and bootstrap the Python
virtualenv. virtualenv.

View File

@@ -1,98 +1,72 @@
Migrating to a new machine Migrating to a new host
=========================== -----------------------
This migration tutorial provides a step-wise approach If you want to migrate chatmail relay from an old machine to a new
to safely migrate a chatmail relay from one remote machine to another. machine, you can use these steps. They were tested with a Linux laptop;
you might need to adjust some of the steps to your environment.
Preliminary notes and assumptions Lets assume that your ``mail_domain`` is ``mail.example.org``, all
--------------------------------- involved machines run Debian 12, your old sites IP address is
``13.37.13.37``, and your new sites IP address is ``13.12.23.42``.
- If the migration is a planned move, Note, you should lower the TTLs of your DNS records to a value such as
it's recommended to lower the Time To Live (TTL) of your DNS records to a value such as 300 (5 minutes), 300 (5 minutes) so the migration happens as smoothly as possible.
at best much earlier than the actual planned migration.
This speeds up propagation of DNS changes in the Internet after the migration is complete.
- The migration steps were tested with a Linux laptop; you might need to adjust some of the steps to your local environment. During the guide you might get a warning about changed SSH Host keys; in
this case, just run ``ssh-keygen -R "mail.example.org"`` as recommended.
- Your ``mail_domain`` is ``mail.example.org``. 1. First, disable mail services on the old site.
- All remote machines run Debian 12.
- The old sites IP version 4 address is ``$OLD_IP4``.
- The new sites IP addresses are ``$NEW_IP4`` and ``$NEW_IPV6``.
The six steps to migrate
------------------------
Note that during some of the following steps you might get a warning about changed SSH Host keys;
in this case, just run ``ssh-keygen -R "mail.example.org"`` as recommended.
1. **Initially transfer mailboxes from old to new site.**
Login to old site, forwarding your ssh-agent with ``ssh -A``
to allow using ssh to directly copy files from old to new site.
::
ssh -A root@$OLD_IP4
tar c /home/vmail/mail | ssh root@$NEW_IP4 "tar x -C /"
2. **Pre-configure the new site but keep it inactive until step 6**
::
CMDEPLOY_STAGES=install,configure scripts/cmdeploy run --ssh-host $NEW_IP4
3. **It's getting serious: disable mail services on the old site.**
Users will not be able to send or receive messages until all steps are completed.
Other relays and mail servers will retry delivering messages from time to time,
so nothing is lost for users.
:: ::
scripts/cmdeploy run --disable-mail --ssh-host $OLD_IP4 cmdeploy run --disable-mail --ssh-host 13.37.13.37
Now your users will notice the migration and will not be able to send
or receive messages until the migration is completed.
4. **Final synchronization of TLS/DKIM secrets, mail queues and mailboxes.** 2. Now we want to copy ``/home/vmail``, ``/var/lib/acme``,
Again we use ssh-agent forwarding (``-A``) to allow transfering all important data directly ``/etc/dkimkeys``, and ``/var/spool/postfix`` to
from the old to the new site. the new site. Login to the old site while forwarding your SSH agent
:: so you can copy directly from the old to the new site with your SSH
key:
ssh -A root@$OLD_IP4
tar c /var/lib/acme /etc/dkimkeys /var/spool/postfix | ssh root@$NEW_IP4 "tar x -C /" ::
rsync -azH /home/vmail/mail root@$NEW_IP4:/home/vmail/
ssh -A root@13.37.13.37
Login to the new site and ensure file ownerships are correctly set: tar c - /home/vmail/mail /var/lib/acme /etc/dkimkeys /var/spool/postfix | ssh root@13.12.23.42 "tar x -C /"
This transfers all addresses, the TLS certificate,
and DKIM keys (so DKIM DNS record remains valid).
It also preserves the Postfix mail spool so any messages
pending delivery will still be delivered.
3. Install chatmail on the new machine:
::
cmdeploy run --disable-mail --ssh-host 13.12.23.42
Postfix and Dovecot are disabled for now; we will enable them later.
We first need to make the new site fully operational.
4. On the new site, run the following to ensure the ownership is correct
in case UIDs/GIDs changed:
:: ::
ssh root@$NEW_IP4
chown root: -R /var/lib/acme chown root: -R /var/lib/acme
chown opendkim: -R /etc/dkimkeys chown opendkim: -R /etc/dkimkeys
chown vmail: -R /home/vmail/mail chown vmail: -R /home/vmail/mail
5. Now, update DNS entries.
5. **Update the DNS entries to point to the new site.** If other MTAs try to deliver messages to your chatmail domain they
You only need to change the ``A`` and ``AAAA`` records, for example: may fail intermittently, as DNS catches up with the new site settings
but normally will retry delivering messages for at least a week, so
messages will not be lost.
:: 6. Finally, you can execute ``cmdeploy run --ssh-host 13.12.23.42`` to
turn on chatmail on the new relay. Your users will be able to use the
mail.example.org. IN A $NEW_IP4 chatmail relay as soon as the DNS changes have propagated. Voilà!
mail.example.org. IN AAAA $NEW_IP6
6. **Activate chatmail relay on new site.**
::
CMDEPLOY_STAGES=activate scripts/cmdeploy run --ssh-host $NEW_IP4
Voilà!
Users will be able to use the relay as soon as the DNS changes have propagated.
If you have lowered the Time-to-Live for DNS records in step 1,
better use a higher value again (between 14400 and 86400 seconds) once you are sure everything works.