Compare commits

..

1 Commits

Author SHA1 Message Date
missytake
d5c3fb1eca nginx: multiplex SSH over port 443 in case port 22 is blocked 2026-01-02 10:13:57 +01:00
16 changed files with 33 additions and 241 deletions

View File

@@ -22,7 +22,7 @@ class DictProxy:
wfile.flush()
def handle_dovecot_request(self, msg, transactions):
# see https://doc.dovecot.org/2.3/developer_manual/design/dict_protocol/#dovecot-dict-protocol
# see https://doc.dovecot.org/developer_manual/design/dict_protocol/#dovecot-dict-protocol
short_command = msg[0]
parts = msg[1:].split("\t")

View File

@@ -16,7 +16,7 @@ NOCREATE_FILE = "/etc/chatmail-nocreate"
def encrypt_password(password: str):
# https://doc.dovecot.org/2.3/configuration_manual/authentication/password_schemes/
# https://doc.dovecot.org/configuration_manual/authentication/password_schemes/
passhash = crypt_r.crypt(password, crypt_r.METHOD_SHA512)
return "{SHA512-CRYPT}" + passhash

View File

@@ -144,7 +144,7 @@ class Expiry:
continue
changed = True
if changed:
self.remove_file(f"{mbox.basedir}/maildirsize")
self.remove_file("maildirsize")
def get_summary(self):
return (

View File

@@ -71,11 +71,6 @@ def run_cmd_options(parser):
action="store_true",
help="install/upgrade the server, but disable postfix & dovecot for now",
)
parser.add_argument(
"--website-only",
action="store_true",
help="only update/deploy the website, skipping full server upgrade/deployment, useful when you only changed/updated the web pages and don't need to re-run a full server upgrade",
)
parser.add_argument(
"--skip-dns-check",
dest="dns_check_disabled",
@@ -98,7 +93,6 @@ def run_cmd(args, out):
env = os.environ.copy()
env["CHATMAIL_INI"] = args.inipath
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 ""
deploy_path = importlib.resources.files(__package__).joinpath("run.py").resolve()
@@ -114,12 +108,7 @@ def run_cmd(args, out):
try:
retcode = out.check_call(cmd, env=env)
if args.website_only:
if retcode == 0:
out.green("Website deployment completed.")
else:
out.red("Website deployment failed.")
elif retcode == 0:
if retcode == 0:
out.green("Deploy completed, call `cmdeploy dns` next.")
elif not remote_data["acme_account_url"]:
out.red("Deploy completed but letsencrypt not configured")

View File

@@ -502,28 +502,23 @@ class GithashDeployer(Deployer):
except Exception:
git_diff = ""
files.put(
name="Upload chatmail relay git commit hash",
name="Upload chatmail relay git commiit hash",
src=StringIO(git_hash + git_diff),
dest="/etc/chatmail-version",
mode="700",
)
def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -> None:
def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
"""Deploy a chat-mail instance.
:param config_path: path to chatmail.ini
:param disable_mail: whether to disable postfix & dovecot
:param website_only: if True, only deploy the website
"""
config = read_config(config_path)
check_config(config)
mail_domain = config.mail_domain
if website_only:
Deployment().perform_stages([WebsiteDeployer(config)])
return
if host.get_fact(Port, port=53) != "unbound":
files.line(
name="Add 9.9.9.9 to resolv.conf",
@@ -541,8 +536,6 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -
(["master", "smtpd"], 587),
(["imap-login", "dovecot"], 993),
("iroh-relay", 3340),
("mtail", 3903),
("dovecot-stats", 3904),
("nginx", 8443),
(["master", "smtpd"], config.postfix_reinject_port),
(["master", "smtpd"], config.postfix_reinject_port_incoming),

View File

@@ -4,7 +4,7 @@ iterate_prefix = userdb/
default_pass_scheme = plain
# %E escapes characters " (double quote), ' (single quote) and \ (backslash) with \ (backslash).
# See <https://doc.dovecot.org/2.3/configuration_manual/config_file/config_variables/#modifiers>
# See <https://doc.dovecot.org/configuration_manual/config_file/config_variables/#modifiers>
# for documentation.
#
# We escape user-provided input and use double quote as a separator.

View File

@@ -116,7 +116,7 @@ def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):
)
need_restart |= lua_push_notification_script.changed
# as per https://doc.dovecot.org/2.3/configuration_manual/os/
# as per https://doc.dovecot.org/configuration_manual/os/
# it is recommended to set the following inotify limits
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"

View File

@@ -26,7 +26,7 @@ default_client_limit = 20000
# Increase number of logged in IMAP connections.
# Each connection is handled by a separate `imap` process.
# `imap` process should have `client_limit=1` as described in
# <https://doc.dovecot.org/2.3/configuration_manual/service_configuration/#service-limits>
# <https://doc.dovecot.org/configuration_manual/service_configuration/#service-limits>
# so each logged in IMAP session will need its own `imap` process.
#
# If this limit is reached,
@@ -44,11 +44,11 @@ mail_server_comment = Chatmail server
# `zlib` enables compressing messages stored in the maildir.
# See
# <https://doc.dovecot.org/2.3/configuration_manual/zlib_plugin/>
# <https://doc.dovecot.org/configuration_manual/zlib_plugin/>
# for documentation.
#
# quota plugin documentation:
# <https://doc.dovecot.org/2.3/configuration_manual/quota_plugin/>
# <https://doc.dovecot.org/configuration_manual/quota_plugin/>
mail_plugins = zlib quota
imap_capability = +XDELTAPUSH XCHATMAIL
@@ -125,13 +125,13 @@ plugin {
protocol lmtp {
# notify plugin is a dependency of push_notification plugin:
# <https://doc.dovecot.org/2.3/settings/plugin/notify-plugin/>
# <https://doc.dovecot.org/settings/plugin/notify-plugin/>
#
# push_notification plugin documentation:
# <https://doc.dovecot.org/2.3/configuration_manual/push_notification/>
# <https://doc.dovecot.org/configuration_manual/push_notification/>
#
# mail_lua and push_notification_lua are needed for Lua push notification handler.
# <https://doc.dovecot.org/2.3/configuration_manual/push_notification/#configuration>
# <https://doc.dovecot.org/configuration_manual/push_notification/#configuration>
mail_plugins = $mail_plugins mail_lua notify push_notification push_notification_lua
}
@@ -154,7 +154,7 @@ plugin {
# push_notification configuration
plugin {
# <https://doc.dovecot.org/2.3/configuration_manual/push_notification/#lua-lua>
# <https://doc.dovecot.org/configuration_manual/push_notification/#lua-lua>
push_notification_driver = lua:file=/etc/dovecot/push_notification.lua
}
@@ -277,134 +277,3 @@ service imap-hibernate {
}
}
{% endif %}
{% if config.mtail_address %}
#
# Dovecot Statistics
#
# OpenMetrics endpoint at http://{{- config.mtail_address}}:3904/metrics
service stats {
inet_listener http {
port = 3904
address = {{- config.mtail_address}}
}
}
# IMAP Command Metrics
# - Bytes in/out for compression efficiency analysis
# - Lock wait time for contention debugging
# - Grouped by command name and reply state
metric imap_command {
filter = event=imap_command_finished
fields = bytes_in bytes_out lock_wait_usecs running_usecs
group_by = cmd_name tagged_reply_state
}
# Duration buckets for latency histograms (base 10: 10us, 100us, 1ms, 10ms, 100ms, 1s, 10s, 100s)
metric imap_command_duration {
filter = event=imap_command_finished
group_by = cmd_name duration:exponential:1:8:10
}
# Slow command outliers (>1 second = 1000000 usecs)
# Useful for alerting without high cardinality
metric imap_command_slow {
filter = event=imap_command_finished AND duration>1000000 AND NOT cmd_name=IDLE
group_by = cmd_name
}
# IDLE-specific metrics
metric imap_idle {
filter = event=imap_command_finished AND cmd_name=IDLE
fields = bytes_in bytes_out running_usecs
group_by = tagged_reply_state
}
metric imap_idle_duration {
filter = event=imap_command_finished AND cmd_name=IDLE
# Base 10: 100ms to 27h (covers short wakeups to long idle sessions)
group_by = duration:exponential:5:11:10
}
# Hibernation Metrics (requires imap_hibernate_timeout to be set)
metric imap_hibernated {
filter = event=imap_client_hibernated
# error field present = failure
group_by = mailbox
}
metric imap_hibernated_failed {
filter = event=imap_client_hibernated AND error=*
}
metric imap_unhibernated {
filter = event=imap_client_unhibernated
fields = hibernation_usecs
group_by = reason
}
metric imap_unhibernated_failed {
filter = event=imap_client_unhibernated AND error=*
}
# Hibernation duration buckets (how long clients actually stayed hibernated)
# Base 10: 100ms to 27h
metric imap_hibernation_duration {
filter = event=imap_client_unhibernated
group_by = reason duration:exponential:5:11:10
}
# Authentication / Login Metrics
metric auth_request {
filter = event=auth_request_finished
group_by = success
}
metric auth_request_duration {
filter = event=auth_request_finished
group_by = success duration:exponential:2:6:10
}
metric auth_failed {
filter = event=auth_request_finished AND success=no
}
# Passdb cache effectiveness
metric auth_passdb {
filter = event=auth_passdb_request_finished
group_by = result cache
}
# Master login (post-auth userdb lookup)
metric auth_master_login {
filter = event=auth_master_client_login_finished
}
metric auth_master_login_failed {
filter = event=auth_master_client_login_finished AND error=*
}
# Mail Delivery (LMTP) - affects IDLE wakeup latency
metric mail_delivery {
filter = event=mail_delivery_finished
}
metric mail_delivery_duration {
filter = event=mail_delivery_finished
group_by = duration:exponential:3:7:10
}
metric mail_delivery_failed {
filter = event=mail_delivery_finished AND error=*
}
# Connection Events
metric client_connected {
filter = event=client_connection_connected AND category=service:imap
}
metric client_disconnected {
filter = event=client_connection_disconnected AND category=service:imap
fields = bytes_in bytes_out
}
{% endif %}

View File

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

View File

@@ -52,15 +52,6 @@ class PostfixDeployer(Deployer):
)
need_restart |= header_cleanup.changed
lmtp_header_cleanup = files.put(
src=get_resource("postfix/lmtp_header_cleanup"),
dest="/etc/postfix/lmtp_header_cleanup",
user="root",
group="root",
mode="644",
)
need_restart |= lmtp_header_cleanup.changed
# Login map that 1:1 maps email address to login.
login_map = files.put(
src=get_resource("postfix/login_map"),
@@ -74,7 +65,7 @@ class PostfixDeployer(Deployer):
restart_conf = files.put(
name="postfix: restart automatically on failure",
src=get_resource("service/10_restart.conf"),
dest="/etc/systemd/system/postfix@.service.d/10_restart.conf",
dest="/etc/systemd/system/dovecot.service.d/10_restart.conf",
)
self.daemon_reload = restart_conf.changed
self.need_restart = need_restart

View File

@@ -1,4 +0,0 @@
/^From:/ DUNNO
/^Message-Id:/ DUNNO
/^Chat-Is-Post-Message:/ DUNNO
/.*/ IGNORE

View File

@@ -77,7 +77,6 @@ inet_protocols = all
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
mua_sender_restrictions = reject_sender_login_mismatch, permit_sasl_authenticated, reject

View File

@@ -14,9 +14,8 @@ def main():
importlib.resources.files("cmdeploy").joinpath("../../../chatmail.ini"),
)
disable_mail = bool(os.environ.get("CHATMAIL_DISABLE_MAIL"))
website_only = bool(os.environ.get("CHATMAIL_WEBSITE_ONLY"))
deploy_chatmail(config_path, disable_mail, website_only)
deploy_chatmail(config_path, disable_mail)
if pyinfra.is_cli:

View File

@@ -17,7 +17,6 @@ def imap_mailbox(cmfactory):
password = ac1.get_config("mail_pw")
mailbox = imap_tools.MailBox(user.split("@")[1])
mailbox.login(user, password)
mailbox.dc_ac = ac1
return mailbox
@@ -122,28 +121,6 @@ class TestEndToEndDeltaChat:
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_dkim_header_stripped(self, cmfactory, maildomain2, lp, imap_mailbox):
"""Test that if a DC address receives a message, it has no
DKIM-Signature and Authentication-Results headers."""
ac1 = cmfactory.new_online_configuring_account(cache=False)
cmfactory.switch_maildomain(maildomain2)
ac2 = cmfactory.new_online_configuring_account(cache=False)
cmfactory.bring_accounts_online()
chat = cmfactory.get_accepted_chat(ac1, imap_mailbox.dc_ac)
chat.send_text("message0")
chat2 = cmfactory.get_accepted_chat(ac2, imap_mailbox.dc_ac)
chat2.send_text("message1")
lp.sec("receive message with ac1...")
received = 0
while received < 2:
msgs = imap_mailbox.fetch()
for msg in msgs:
lp.sec(f"ac1 received msg from {msg.from_}")
received += 1
assert "authentication-results" not in msg.headers
assert "dkim-signature" not in msg.headers
def test_read_receipts_between_instances(self, cmfactory, lp, maildomain2):
ac1 = cmfactory.new_online_configuring_account(cache=False)
cmfactory.switch_maildomain(maildomain2)

View File

@@ -16,16 +16,15 @@ You will need the following:
- Control over a domain through a DNS provider of your choice.
- A Debian 12 **deployment server** with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
- A Debian 12 server with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
IPv6 is encouraged if available. Chatmail relay servers only require
1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active
chatmail addresses.
- A Linux or Unix **build machine** with key-based SSH access to the root
user of the deployment server.
You must add a passphrase-protected private key to your local ssh-agent because you
cant type in your passphrase during deployment.
(An ed25519 private key is required due to an `upstream bug in
- Key-based SSH authentication to the root user. You must add a
passphrase-protected private key to your local ssh-agent because you
cant type in your passphrase during deployment. (An ed25519 private
key is required due to an `upstream bug in
paramiko <https://github.com/paramiko/paramiko/issues/2191>`_)
@@ -35,8 +34,7 @@ Setup with ``scripts/cmdeploy``
We use ``chat.example.org`` as the chatmail domain in the following
steps. Please substitute it with your own domain.
1. Setup the initial DNS records for your deployment server.
The following is an example in the
1. Setup the initial DNS records. The following is an example in the
familiar BIND zone file format with a TTL of 1 hour (3600 seconds).
Please substitute your domain and IP addresses.
@@ -56,20 +54,20 @@ steps. Please substitute it with your own domain.
cd relay
scripts/initenv.sh
3. On your local build machine (PC), create a chatmail configuration file
3. On your local PC, create chatmail configuration file
``chatmail.ini``:
::
scripts/cmdeploy init chat.example.org # <-- use your domain
4. Verify that SSH root login to the deployment server server works:
4. Verify that SSH root login to your remote server works:
::
ssh root@chat.example.org # <-- use your domain
5. From your local build machine, setup and configure the remote deployment server:
5. From your local PC, deploy the remote chatmail relay server:
::
@@ -83,7 +81,7 @@ steps. Please substitute it with your own domain.
Other helpful commands
----------------------
To check the status of your deployment server running the chatmail service:
To check the status of your remotely running chatmail service:
::
@@ -160,7 +158,7 @@ Disable automatic address creation
--------------------------------------------------------
If you need to stop address creation, e.g. because some script is wildly
creating addresses, login with ssh to the deployment machine and run:
creating addresses, login with ssh and run:
::
@@ -169,23 +167,3 @@ creating addresses, login with ssh to the deployment machine and run:
Chatmail address creation will be denied while this file is present.
Migrating to a new build machine
----------------------------------
To move or add a build machine,
clone the relay repository on the new build machine, and copy the ``chatmail.ini`` file from the old build machine.
Make sure ``rsync`` is installed, then initialize the environment:
::
./scripts/initenv.sh
Run safety checks before a new deployment:
::
./scripts/cmdeploy dns
./scripts/cmdeploy status
If you keep multiple build machines (ie laptop and desktop), keep ``chatmail.ini`` in sync between
them.

View File

@@ -14,10 +14,10 @@ We know of three work-in-progress alternative implementation efforts:
it to support all of the features and configuration settings required
to operate as a chatmail relay.
- `Madmail <https://github.com/omidz4t/madmail>`_: an
experimental fork of Maddy Mail Server <https://maddy.email/>`_ optimized
for chatmail deployments. It provides a single binary solution
for running a chatmail relay.
- `Maddy-Chatmail <https://github.com/sadraiiali/maddy_chatmail>`_: a
plugin for the `Maddy email server <https://maddy.email/>`_ which
aims to implement the chatmail relay features and configuration
options.
- `Chatmail Cookbook <https://github.com/feld/chatmail-cookbook>`_:
A Chef Cookbook implementing a relay server. The project follows the