mirror of
https://github.com/chatmail/relay.git
synced 2026-05-14 01:44:38 +00:00
Compare commits
4 Commits
lmtp_heade
...
docs-ssh-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c1f226b4 | ||
|
|
85eddb671e | ||
|
|
cf215f971d | ||
|
|
ab3b9d156a |
@@ -26,6 +26,7 @@ chatmail-expire = "chatmaild.expire:daily_expire_main"
|
|||||||
chatmail-quota-expire = "chatmaild.expire:quota_expire_main"
|
chatmail-quota-expire = "chatmaild.expire:quota_expire_main"
|
||||||
chatmail-fsreport = "chatmaild.fsreport:main"
|
chatmail-fsreport = "chatmaild.fsreport:main"
|
||||||
lastlogin = "chatmaild.lastlogin:main"
|
lastlogin = "chatmaild.lastlogin:main"
|
||||||
|
turnserver = "chatmaild.turnserver:main"
|
||||||
|
|
||||||
[project.entry-points.pytest11]
|
[project.entry-points.pytest11]
|
||||||
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
"chatmaild.testplugin" = "chatmaild.tests.plugin"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Config:
|
|||||||
self._inipath = inipath
|
self._inipath = inipath
|
||||||
raw_domain = params["mail_domain"]
|
raw_domain = params["mail_domain"]
|
||||||
self.mail_domain_bare = raw_domain
|
self.mail_domain_bare = raw_domain
|
||||||
|
self.ssh_host = params.get("ssh_host", raw_domain)
|
||||||
|
|
||||||
if is_valid_ipv4(raw_domain):
|
if is_valid_ipv4(raw_domain):
|
||||||
self.ipv4_relay = raw_domain
|
self.ipv4_relay = raw_domain
|
||||||
@@ -63,9 +64,6 @@ class Config:
|
|||||||
self.acme_email = params.get("acme_email", "")
|
self.acme_email = params.get("acme_email", "")
|
||||||
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
|
||||||
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
|
self.imap_compress = params.get("imap_compress", "false").lower() == "true"
|
||||||
self.turn_socket_path = params.get(
|
|
||||||
"turn_socket_path", "/run/chatmail-turn/turn.socket"
|
|
||||||
)
|
|
||||||
if "iroh_relay" not in params:
|
if "iroh_relay" not in params:
|
||||||
self.iroh_relay = "https://" + raw_domain
|
self.iroh_relay = "https://" + raw_domain
|
||||||
self.enable_iroh_relay = True
|
self.enable_iroh_relay = True
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
# mail domain (MUST be set to fully qualified chat mail domain)
|
# mail domain (MUST be set to fully qualified chat mail domain)
|
||||||
mail_domain = {mail_domain}
|
mail_domain = {mail_domain}
|
||||||
|
|
||||||
|
# Where to deploy the relay - if unspecified, mail_domain will be used.
|
||||||
|
ssh_host = localhost
|
||||||
|
|
||||||
#
|
#
|
||||||
# If you only do private test deploys, you don't need to modify any settings below
|
# If you only do private test deploys, you don't need to modify any settings below
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
@@ -8,14 +7,7 @@ from .config import read_config
|
|||||||
from .dictproxy import DictProxy
|
from .dictproxy import DictProxy
|
||||||
from .filedict import FileDict
|
from .filedict import FileDict
|
||||||
from .notifier import Notifier
|
from .notifier import Notifier
|
||||||
|
from .turnserver import turn_credentials
|
||||||
|
|
||||||
def turn_credentials(turn_socket_path):
|
|
||||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
|
||||||
client_socket.settimeout(5)
|
|
||||||
client_socket.connect(turn_socket_path)
|
|
||||||
with client_socket.makefile("rb") as file:
|
|
||||||
return file.readline().decode("utf-8").strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_token_timestamp(timestamp, now):
|
def _is_valid_token_timestamp(timestamp, now):
|
||||||
@@ -87,20 +79,12 @@ class Metadata:
|
|||||||
|
|
||||||
|
|
||||||
class MetadataDictProxy(DictProxy):
|
class MetadataDictProxy(DictProxy):
|
||||||
def __init__(
|
def __init__(self, notifier, metadata, iroh_relay=None, turn_hostname=None):
|
||||||
self,
|
|
||||||
notifier,
|
|
||||||
metadata,
|
|
||||||
iroh_relay=None,
|
|
||||||
turn_hostname=None,
|
|
||||||
turn_socket_path=None,
|
|
||||||
):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.notifier = notifier
|
self.notifier = notifier
|
||||||
self.metadata = metadata
|
self.metadata = metadata
|
||||||
self.iroh_relay = iroh_relay
|
self.iroh_relay = iroh_relay
|
||||||
self.turn_hostname = turn_hostname
|
self.turn_hostname = turn_hostname
|
||||||
self.turn_socket_path = turn_socket_path
|
|
||||||
|
|
||||||
def handle_lookup(self, parts):
|
def handle_lookup(self, parts):
|
||||||
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
# Lpriv/43f5f508a7ea0366dff30200c15250e3/devicetoken\tlkj123poi@c2.testrun.org
|
||||||
@@ -117,7 +101,7 @@ class MetadataDictProxy(DictProxy):
|
|||||||
return f"O{self.iroh_relay}\n"
|
return f"O{self.iroh_relay}\n"
|
||||||
case "turn":
|
case "turn":
|
||||||
try:
|
try:
|
||||||
res = turn_credentials(self.turn_socket_path)
|
res = turn_credentials()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("failed to get TURN credentials")
|
logging.exception("failed to get TURN credentials")
|
||||||
return "N\n"
|
return "N\n"
|
||||||
@@ -151,7 +135,6 @@ def main():
|
|||||||
config = read_config(config_path)
|
config = read_config(config_path)
|
||||||
iroh_relay = config.iroh_relay
|
iroh_relay = config.iroh_relay
|
||||||
mail_domain = config.mail_domain
|
mail_domain = config.mail_domain
|
||||||
socket_path = config.turn_socket_path
|
|
||||||
|
|
||||||
vmail_dir = config.mailboxes_dir
|
vmail_dir = config.mailboxes_dir
|
||||||
if not vmail_dir.exists():
|
if not vmail_dir.exists():
|
||||||
@@ -169,7 +152,6 @@ def main():
|
|||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
iroh_relay=iroh_relay,
|
iroh_relay=iroh_relay,
|
||||||
turn_hostname=mail_domain,
|
turn_hostname=mail_domain,
|
||||||
turn_socket_path=socket_path,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
dictproxy.serve_forever_from_socket(socket)
|
dictproxy.serve_forever_from_socket(socket)
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ def test_turn_credentials_exception_returns_N(notifier, metadata, monkeypatch):
|
|||||||
turn_hostname="turn.example.org",
|
turn_hostname="turn.example.org",
|
||||||
)
|
)
|
||||||
|
|
||||||
def mock_turn_credentials(turn_socket_path):
|
def mock_turn_credentials():
|
||||||
raise ConnectionRefusedError("socket not available")
|
raise ConnectionRefusedError("socket not available")
|
||||||
|
|
||||||
monkeypatch.setattr(chatmaild.metadata, "turn_credentials", mock_turn_credentials)
|
monkeypatch.setattr(chatmaild.metadata, "turn_credentials", mock_turn_credentials)
|
||||||
@@ -348,9 +348,7 @@ def test_turn_credentials_success(notifier, metadata, monkeypatch):
|
|||||||
turn_hostname="turn.example.org",
|
turn_hostname="turn.example.org",
|
||||||
)
|
)
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(chatmaild.metadata, "turn_credentials", lambda: "user:pass")
|
||||||
chatmaild.metadata, "turn_credentials", lambda path: "user:pass"
|
|
||||||
)
|
|
||||||
|
|
||||||
transactions = {}
|
transactions = {}
|
||||||
res = dictproxy.handle_dovecot_request(
|
res = dictproxy.handle_dovecot_request(
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import socket
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from chatmaild.metadata import turn_credentials
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def turn_socket(tmp_path):
|
|
||||||
sock_path = str(tmp_path / "turn.socket")
|
|
||||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
||||||
server.bind(sock_path)
|
|
||||||
server.listen(1)
|
|
||||||
yield sock_path, server
|
|
||||||
server.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_credentials_timeout(turn_socket):
|
|
||||||
sock_path, server = turn_socket
|
|
||||||
with pytest.raises(socket.timeout):
|
|
||||||
# Inside turn_credentials the kernel listen backlog (1)
|
|
||||||
# completes connect() without accept()
|
|
||||||
# so the client blocks on readline() until the 5s timeout fires.
|
|
||||||
turn_credentials(sock_path)
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_credentials_connection_refused_on_not_existing_socket(tmp_path):
|
|
||||||
missing = str(tmp_path / "nonexistent.socket")
|
|
||||||
with pytest.raises((ConnectionRefusedError, FileNotFoundError)):
|
|
||||||
turn_credentials(missing)
|
|
||||||
|
|
||||||
|
|
||||||
def test_turn_credentials_socket_success(turn_socket):
|
|
||||||
sock_path, server = turn_socket
|
|
||||||
|
|
||||||
def respond():
|
|
||||||
conn, _ = server.accept()
|
|
||||||
conn.sendall(b"testuser:testpass\n")
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
t = threading.Thread(target=respond, daemon=True)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
result = turn_credentials(sock_path)
|
|
||||||
assert result == "testuser:testpass"
|
|
||||||
73
chatmaild/src/chatmaild/tests/test_turnserver.py
Normal file
73
chatmaild/src/chatmaild/tests/test_turnserver.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from chatmaild.turnserver import turn_credentials
|
||||||
|
|
||||||
|
SOCKET_PATH = "/run/chatmail-turn/turn.socket"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def turn_socket(tmp_path):
|
||||||
|
"""Create a real Unix socket server at a temp path."""
|
||||||
|
sock_path = str(tmp_path / "turn.socket")
|
||||||
|
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
server.bind(sock_path)
|
||||||
|
server.listen(1)
|
||||||
|
yield sock_path, server
|
||||||
|
server.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _call_turn_credentials(sock_path):
|
||||||
|
"""Call turn_credentials but connect to sock_path instead of hardcoded path."""
|
||||||
|
original_connect = socket.socket.connect
|
||||||
|
|
||||||
|
def patched_connect(self, address):
|
||||||
|
if address == SOCKET_PATH:
|
||||||
|
address = sock_path
|
||||||
|
return original_connect(self, address)
|
||||||
|
|
||||||
|
with patch.object(socket.socket, "connect", patched_connect):
|
||||||
|
return turn_credentials()
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_credentials_timeout(turn_socket):
|
||||||
|
"""Server accepts but never responds — must raise socket.timeout."""
|
||||||
|
sock_path, server = turn_socket
|
||||||
|
|
||||||
|
def accept_and_hang():
|
||||||
|
conn, _ = server.accept()
|
||||||
|
time.sleep(30)
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
t = threading.Thread(target=accept_and_hang, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
with pytest.raises(socket.timeout):
|
||||||
|
_call_turn_credentials(sock_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_credentials_connection_refused(tmp_path):
|
||||||
|
"""Socket file doesn't exist — must raise ConnectionRefusedError or FileNotFoundError."""
|
||||||
|
missing = str(tmp_path / "nonexistent.socket")
|
||||||
|
with pytest.raises((ConnectionRefusedError, FileNotFoundError)):
|
||||||
|
_call_turn_credentials(missing)
|
||||||
|
|
||||||
|
|
||||||
|
def test_turn_credentials_success(turn_socket):
|
||||||
|
"""Server responds with credentials — must return stripped string."""
|
||||||
|
sock_path, server = turn_socket
|
||||||
|
|
||||||
|
def respond():
|
||||||
|
conn, _ = server.accept()
|
||||||
|
conn.sendall(b"testuser:testpass\n")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
t = threading.Thread(target=respond, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
result = _call_turn_credentials(sock_path)
|
||||||
|
assert result == "testuser:testpass"
|
||||||
10
chatmaild/src/chatmaild/turnserver.py
Normal file
10
chatmaild/src/chatmaild/turnserver.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
def turn_credentials() -> str:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client_socket:
|
||||||
|
client_socket.settimeout(5)
|
||||||
|
client_socket.connect("/run/chatmail-turn/turn.socket")
|
||||||
|
with client_socket.makefile("rb") as file:
|
||||||
|
return file.readline().decode("utf-8").strip()
|
||||||
@@ -87,7 +87,7 @@ def run_cmd_options(parser):
|
|||||||
def run_cmd(args, out):
|
def run_cmd(args, out):
|
||||||
"""Deploy chatmail services on the remote server."""
|
"""Deploy chatmail services on the remote server."""
|
||||||
|
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain_bare
|
ssh_host = args.ssh_host if args.ssh_host else args.config.ssh_host
|
||||||
sshexec = get_sshexec(ssh_host)
|
sshexec = get_sshexec(ssh_host)
|
||||||
require_iroh = args.config.enable_iroh_relay
|
require_iroh = args.config.enable_iroh_relay
|
||||||
strict_tls = args.config.tls_cert_mode == "acme"
|
strict_tls = args.config.tls_cert_mode == "acme"
|
||||||
@@ -107,7 +107,7 @@ def run_cmd(args, out):
|
|||||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
||||||
|
|
||||||
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
|
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
|
||||||
if ssh_host == "localhost":
|
if ssh_host in ["localhost", "@local"]:
|
||||||
cmd = f"{pyinf} @local {deploy_path} -y"
|
cmd = f"{pyinf} @local {deploy_path} -y"
|
||||||
|
|
||||||
if version.parse(pyinfra.__version__) < version.parse("3"):
|
if version.parse(pyinfra.__version__) < version.parse("3"):
|
||||||
@@ -148,7 +148,7 @@ def dns_cmd(args, out):
|
|||||||
ipv4 = args.config.ipv4_relay
|
ipv4 = args.config.ipv4_relay
|
||||||
print(f"[WARNING] {ipv4} is not a domain, skipping DNS checks.")
|
print(f"[WARNING] {ipv4} is not a domain, skipping DNS checks.")
|
||||||
return 0
|
return 0
|
||||||
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.ssh_host
|
||||||
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
||||||
tls_cert_mode = args.config.tls_cert_mode
|
tls_cert_mode = args.config.tls_cert_mode
|
||||||
strict_tls = tls_cert_mode == "acme"
|
strict_tls = tls_cert_mode == "acme"
|
||||||
@@ -185,7 +185,7 @@ def status_cmd_options(parser):
|
|||||||
def status_cmd(args, out):
|
def status_cmd(args, out):
|
||||||
"""Display status for online chatmail instance."""
|
"""Display status for online chatmail instance."""
|
||||||
|
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain_bare
|
ssh_host = args.ssh_host if args.ssh_host else args.config.ssh_host
|
||||||
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
||||||
|
|
||||||
out.green(f"chatmail domain: {args.config.mail_domain}")
|
out.green(f"chatmail domain: {args.config.mail_domain}")
|
||||||
|
|||||||
@@ -1,23 +1,3 @@
|
|||||||
# List of headers for incoming messages
|
/^DKIM-Signature:/ IGNORE
|
||||||
# that must be retained for functionality and compatibility reasons
|
/^Authentication-Results:/ IGNORE
|
||||||
/^From:/ DUNNO
|
/^Received:/ IGNORE
|
||||||
/^Message-Id:/ DUNNO
|
|
||||||
/^Chat-Is-Post-Message:/ DUNNO
|
|
||||||
/^Content-Type:/ DUNNO
|
|
||||||
|
|
||||||
# For receiving clear-text messages (still supported in May 2026)
|
|
||||||
/^Subject:/ DUNNO
|
|
||||||
/^Date:/ DUNNO
|
|
||||||
|
|
||||||
# Senders might support Autocrypt 1 but not RFC9788 (Header Protection)
|
|
||||||
/^Autocrypt:/ DUNNO
|
|
||||||
|
|
||||||
# SecureJoin V2 protocol headers (for backward compatibility)
|
|
||||||
/^Secure-Join:/ DUNNO
|
|
||||||
/^Secure-Join-Invitenumber:/ DUNNO
|
|
||||||
/^Secure-Join-Auth:/ DUNNO
|
|
||||||
/^Secure-Join-Fingerprint:/ DUNNO
|
|
||||||
/^Secure-Join-Group:/ DUNNO
|
|
||||||
|
|
||||||
# Ignore all other headers
|
|
||||||
/.*/ IGNORE
|
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ def maildomain(chatmail_config):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def sshdomain(maildomain):
|
def sshdomain(chatmail_config):
|
||||||
return os.environ.get("CHATMAIL_SSH", maildomain)
|
return os.environ.get("CHATMAIL_SSH", chatmail_config.ssh_host)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -14,21 +14,14 @@ Minimal requirements and prerequisites
|
|||||||
|
|
||||||
You will need the following:
|
You will need the following:
|
||||||
|
|
||||||
- A Debian 12 **deployment server** with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
|
- Control over a domain through a DNS provider of your choice.
|
||||||
|
(there is experimental support for :ref:`IP-only relays <iponly>`).
|
||||||
|
|
||||||
|
- A Debian 12 server with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
|
||||||
IPv6 is encouraged if available. Chatmail relay servers only require
|
IPv6 is encouraged if available. Chatmail relay servers only require
|
||||||
1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active
|
1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active
|
||||||
chatmail addresses.
|
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
|
|
||||||
can’t 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>`_)
|
|
||||||
|
|
||||||
- Control over a domain through a DNS provider of your choice
|
|
||||||
(there is experimental support for :ref:`IP-only relays <iponly>`).
|
|
||||||
|
|
||||||
|
|
||||||
.. _setup:
|
.. _setup:
|
||||||
|
|
||||||
@@ -38,7 +31,7 @@ Setup with ``scripts/cmdeploy``
|
|||||||
We use ``chat.example.org`` as the chatmail domain in the following
|
We use ``chat.example.org`` as the chatmail domain in the following
|
||||||
steps. Please substitute it with your own domain.
|
steps. Please substitute it with your own domain.
|
||||||
|
|
||||||
1. Setup the initial DNS records for your deployment server.
|
1. Setup the initial DNS records for your relay.
|
||||||
The following is an example in the
|
The following is an example in the
|
||||||
familiar BIND zone file format with a TTL of 1 hour (3600 seconds).
|
familiar BIND zone file format with a TTL of 1 hour (3600 seconds).
|
||||||
Please substitute your domain and IP addresses.
|
Please substitute your domain and IP addresses.
|
||||||
@@ -58,22 +51,25 @@ steps. Please substitute it with your own domain.
|
|||||||
The ``mta-sts`` CNAME and ``_mta-sts`` TXT records
|
The ``mta-sts`` CNAME and ``_mta-sts`` TXT records
|
||||||
are not needed for such domains.
|
are not needed for such domains.
|
||||||
|
|
||||||
2. On your local PC, clone the repository and bootstrap the Python
|
2. Login to the server with SSH, clone the repository and bootstrap the Python
|
||||||
virtualenv.
|
virtualenv.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
ssh root@chat.example.org
|
||||||
git clone https://github.com/chatmail/relay
|
git clone https://github.com/chatmail/relay
|
||||||
cd relay
|
cd relay
|
||||||
scripts/initenv.sh
|
scripts/initenv.sh
|
||||||
|
|
||||||
3. On your local build machine (PC), create a chatmail configuration file
|
3. Then, create a chatmail configuration file
|
||||||
``chatmail.ini``:
|
``chatmail.ini``:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
scripts/cmdeploy init chat.example.org # <-- use your domain
|
scripts/cmdeploy init chat.example.org # <-- use your domain
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
To use self-signed TLS certificates
|
To use self-signed TLS certificates
|
||||||
instead of Let's Encrypt,
|
instead of Let's Encrypt,
|
||||||
use a domain name starting with ``_``
|
use a domain name starting with ``_``
|
||||||
@@ -84,13 +80,7 @@ steps. Please substitute it with your own domain.
|
|||||||
See the :doc:`overview`
|
See the :doc:`overview`
|
||||||
for details on certificate provisioning.
|
for details on certificate provisioning.
|
||||||
|
|
||||||
4. Verify that SSH root login to the deployment server server works:
|
4. Now run the deployment script to install the relay to the server:
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
ssh root@chat.example.org # <-- use your domain
|
|
||||||
|
|
||||||
5. From your local build machine, setup and configure the remote deployment server:
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -102,7 +92,6 @@ steps. Please substitute it with your own domain.
|
|||||||
public).
|
public).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Docker installation
|
Docker installation
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
@@ -110,27 +99,33 @@ There is experimental support for running chatmail via Docker.
|
|||||||
A monolithic image based on the above cmdeploy method is available `through a separate repository <https://github.com/chatmail/docker/pkgs/container/docker>`_.
|
A monolithic image based on the above cmdeploy method is available `through a separate repository <https://github.com/chatmail/docker/pkgs/container/docker>`_.
|
||||||
See the `chatmail/docker README <https://github.com/chatmail/docker>`_ for full setup instructions.
|
See the `chatmail/docker README <https://github.com/chatmail/docker>`_ for full setup instructions.
|
||||||
|
|
||||||
Other helpful commands
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
To check the status of your deployment server running the chatmail service:
|
Next Steps
|
||||||
|
----------
|
||||||
|
|
||||||
::
|
Now you should display and check all recommended DNS records
|
||||||
|
to enable federation with other relays:
|
||||||
scripts/cmdeploy status
|
|
||||||
|
|
||||||
To display and check all recommended DNS records:
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
scripts/cmdeploy dns
|
scripts/cmdeploy dns
|
||||||
|
|
||||||
To test whether your chatmail service is working correctly:
|
You should also test whether your chatmail service is working correctly:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
scripts/cmdeploy test
|
scripts/cmdeploy test
|
||||||
|
|
||||||
|
Other Helpful Commands
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
To check the status of your chatmail relay:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
scripts/cmdeploy status
|
||||||
|
|
||||||
|
|
||||||
To measure the performance of your chatmail service:
|
To measure the performance of your chatmail service:
|
||||||
|
|
||||||
::
|
::
|
||||||
@@ -171,8 +166,9 @@ This starts a local live development cycle for chatmail web pages:
|
|||||||
directory and generating HTML files and copying assets to the
|
directory and generating HTML files and copying assets to the
|
||||||
``www/build`` directory.
|
``www/build`` directory.
|
||||||
|
|
||||||
- Starts a browser window automatically where you can “refresh” as
|
- if you are running scripts/cmdeploy webdev on the relay itself,
|
||||||
needed.
|
you need to configure a route in /etc/nginx/nginx.conf
|
||||||
|
to expose the build directory.
|
||||||
|
|
||||||
Custom web pages
|
Custom web pages
|
||||||
----------------
|
----------------
|
||||||
@@ -190,7 +186,7 @@ Disable automatic address creation
|
|||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
If you need to stop address creation, e.g. because some script is wildly
|
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 to the relay and run:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -246,25 +242,3 @@ The deploy will verify that both files exist on the server.
|
|||||||
If you use such a setup, you must trigger the reload explicitly after renewal::
|
If you use such a setup, you must trigger the reload explicitly after renewal::
|
||||||
|
|
||||||
systemctl start tls-cert-reload.service
|
systemctl start tls-cert-reload.service
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user