Compare commits

..

53 Commits

Author SHA1 Message Date
Keonik1
5e4f9deb28 docker: add traefik support 2025-10-08 12:06:16 +02:00
missytake
2c344d7fc5 docker: document cmdeploy dns in docker containers 2025-10-08 11:56:48 +02:00
missytake
346179d045 docker: enable DNS checks before cmdeploy run again 2025-10-08 11:56:48 +02:00
Keonik1
a331828301 fix unlink if default nginx conf is not exist
- https://github.com/chatmail/relay/pull/614#discussion_r2297828830
2025-10-08 11:56:48 +02:00
Keonik1
514682a093 Fix issue with acmetool
- https://github.com/chatmail/relay/pull/614#discussion_r2279630626
2025-10-08 11:56:48 +02:00
Keonik1
ab5b8941c7 Delete ssh connection from docker installation
- https://github.com/chatmail/relay/pull/614#discussion_r2269986372
- https://github.com/chatmail/relay/pull/614#discussion_r2269991175
- https://github.com/chatmail/relay/pull/614#discussion_r2269995037
- https://github.com/chatmail/relay/pull/614#discussion_r2270004922
2025-10-08 11:56:48 +02:00
Keonik1
84def2db65 fix docs - nginx "restart" to "reload"
https://github.com/chatmail/relay/pull/614#discussion_r2269896158
2025-10-08 11:56:48 +02:00
Keonik1
e134552b4f Fix bug with attaching certs 2025-10-08 11:56:28 +02:00
Keonik1
c0e77adfed pass values to MAIL_DOMAIN and ACME_EMAIL from vars for docker-compose-default
https://github.com/chatmail/relay/pull/614#discussion_r2279591922
2025-10-08 11:56:28 +02:00
Keonik1
db5e39a899 change "restart nginx" to "reload nginx"
https://github.com/chatmail/relay/pull/614#discussion_r2269896158
2025-10-08 11:56:28 +02:00
Keonik1
5692681937 add RECREATE_VENV var
https://github.com/chatmail/relay/pull/614#discussion_r2279742769
2025-10-08 11:56:28 +02:00
Keonik1
9919deefe3 add 465 port
https://github.com/chatmail/relay/pull/614#discussion_r2279707059
2025-10-08 11:56:28 +02:00
Keonik1
d525b95957 add port 80 to docker-compose-default
https://github.com/chatmail/relay/pull/614#discussion_r2279656441
2025-10-08 11:56:28 +02:00
Keonik1
9a43a25e2c rename dockerfile
https://github.com/chatmail/relay/pull/614#discussion_r2270031966
2025-10-08 11:56:28 +02:00
Keonik1
955d89fa1c Add installation via docker compose (MVP 1)
- Adds configuration parameters (`change_kernel_settings`, `fs_inotify_max_user_instances_and_watchers`)
2025-10-08 11:56:25 +02:00
missytake
3e3a85523d cmdeploy: prepare for being able to run commands in docker containers 2025-10-08 10:45:15 +02:00
missytake
7023612a8b tests: disable failing stderr capturing in test_logged for now 2025-10-08 10:13:35 +02:00
missytake
fdabed5c67 cmdeploy: allow to run SSH commands locally
fix #604
related to #629
pulled out of https://github.com/Keonik1/relay/pull/3
2025-10-08 10:13:34 +02:00
link2xt
0ed7c360a9 Update changelog 2025-10-05 02:37:50 +00:00
link2xt
af272545dd Restart iroh-relay if the binary is updated 2025-10-05 02:37:23 +00:00
link2xt
7725a73cf5 Ensure that downloaded iroh-relay matches expected SHA-256 sum
Previously we only used SHA-256 sum
to check if we need to update the binary.
2025-10-05 02:37:23 +00:00
link2xt
e65311c0df Update iroh-relay to 0.35.0 2025-10-05 02:37:23 +00:00
link2xt
d091b865c7 fix: ignore all RCPT TO: parameters
Stalwart sends `NOTIFY=DELAY,FAILURE`
to request Delivery Status Notifications.
aiosmtpd does not support any parameters,
not just ORCPT, so we have to ignore all of them.
2025-10-05 02:36:40 +00:00
cliffmccarthy
6e28cf9ca1 Add CHANGELOG.md entry for #648 2025-10-03 19:48:32 +00:00
cliffmccarthy
9b6dfa9cdc Use max username length in newemail.py, not min
- username_min_length and username_max_length are both set to a
  default value of 9 in the chatmail.ini.f template.  When they have
  the same value, it doesn't matter which one we use in newemail.py
  (which handles the /new URL).  However, if they are configured to
  different values by the admin, then the current implementation using
  username_min_length chooses from a smaller set of possible
  usernames.
- Revised create_newemail_dict() in newemail.py to use
  username_max_length as the length of the random username it offers
  via the /new URL.  This randomizes within a much larger set of
  possible usernames.
2025-10-03 19:48:32 +00:00
missytake
44ab006dca echobot: restart after postfix + dovecot were started (#642)
* echobot: restart after postfix + dovecot were started

fix #641

* cmdeploy: restart echobot only if dovecot *and* postfix were restarted
2025-09-25 09:00:26 +02:00
link2xt
c56805211f Increase maxproc for reinjecting ports from 10 to 100
Otherwise under high load filtermail
starts printing "Connection refused" errors to the log.
2025-09-24 16:10:26 +00:00
missytake
05ec64bf4a fix link to Mutual Help group 2025-09-23 13:42:47 +02:00
link2xt
290e80e795 Revert "dovecot: keep mailbox index only in memory (#632)"
This reverts commit 7bf2dfd62e.
2025-09-22 22:55:57 +00:00
missytake
56fab1b071 CI: fix lint (#633) 2025-09-22 12:57:43 +02:00
link2xt
00ab53800e Update changelog 2025-09-18 15:28:15 +00:00
link2xt
fc65072edb Allow ports 143 and 993 to be used by dovecot process 2025-09-18 15:26:58 +00:00
missytake
7bf2dfd62e dovecot: keep mailbox index only in memory (#632)
Co-authored-by: holger krekel  <holger@merlinux.eu>
2025-09-12 09:30:17 +02:00
missytake
b801838b69 doc: released 1.7.0 2025-09-12 00:55:49 +02:00
missytake
abd50e20ed cmdeploy: suppress SSH login info message 2025-09-11 20:31:03 +02:00
missytake
d6fb38750a www: make www_folder behavior testable 2025-09-11 19:51:32 +02:00
missytake
3b73457de3 www: introduce www_folder config item
fix #529
2025-09-11 19:51:32 +02:00
missytake
ba06a4ff70 cmdeploy: postfix runs on other ports as well, of course 2025-08-29 23:48:54 +02:00
missytake
7fdaffe829 cmdeploy: on Ubuntu, postfix calls its port 25 process 'smtpd' 2025-08-29 23:48:54 +02:00
missytake
73831c74d9 cmdeploy: fix lint 2025-08-27 08:36:33 +02:00
missytake
d8cbe9d6af cmdeploy: use ports from config for port checking 2025-08-27 08:36:33 +02:00
missytake
180ddb8168 doc: add changelog entry 2025-08-27 08:36:33 +02:00
missytake
a1eeea4632 acmetool: remove unused imports 2025-08-27 08:36:33 +02:00
missytake
a49aa0e655 acmetool: remove outdated systemctl stop nginx 2025-08-27 08:36:33 +02:00
missytake
7e81495b51 cmdeploy: exit if a necessary port is occupied by an unexpected process 2025-08-27 08:36:33 +02:00
missytake
6fde062613 fix lint 2025-08-27 08:35:04 +02:00
missytake
84e0376762 cmdeploy: get SSHExec again, timeout is likely 2025-08-27 08:35:04 +02:00
missytake
d690c22c06 cmdeploy: print echobot link at the end of cmdeploy run 2025-08-27 08:35:04 +02:00
missytake
5410c1bebc CI: remove lint checks from test deployments 2025-08-27 08:34:26 +02:00
missytake
915bd39dd5 CI: fail on lint issues 2025-08-27 08:34:26 +02:00
cliffmccarthy
2de8b155c2 docs: Rework architecture diagram based on review feedback
- Implemented changes suggested in review by missytake:
    - Removed relation between acmetool-redirector and certs.
    - Added internal nginx listening on port 8443.
    - Changed direction of arrows between certs and the services that
      use them.  This makes the arrow show the direction of
      information flow, rather than a "depends on" relation.
    - For filesystem paths, added a descriptive name to the node.
- Replaced most arrows with plain lines, to simply show that a
  relationship exists between the two nodes.  This also reduces visual
  clutter, since the graph is pretty dense with information already.
- Split nginx and certs into two nodes, to reduce entanglement in the
  graph.  These "linked" nodes are given a different shape and filled
  with a different colour, to highlight the fact that they are the
  same node.
- Revised text about the meaning of edges in the graph.
2025-08-19 13:04:33 +02:00
cliffmccarthy
c975aa3bd1 docs: Indicate draft status in ARCHITECTURE.md
- Suggested in review by hpk42.
2025-08-19 13:04:33 +02:00
cliffmccarthy
6b73f6933a docs: Add ARCHITECTURE.md with diagram of components
- For starters, this file is just a diagram of components of a
  chatmail server.  In the future, this document can grow into a more
  complete description of the architecture of the server, the
  deployment process, and the design intent behind what is and isn't
  in the code base.
- The name ARCHITECTURE.md is inspired by this article, which also has
  good suggestions about what to put in the file:
  https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html
2025-08-19 13:04:33 +02:00
39 changed files with 449 additions and 782 deletions

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: Mutual Help Chat Group - name: Mutual Help Chat Group
url: https://i.delta.chat/#C2846EB4C1CB8DF84B1818F5E3A638FC3FBDC981&a=stalebot1%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=d7s1HvOsk5UrSf9AoqRZggg4&s=XmX_9BAW6-g5Ao5E8PyaeKNB url: https://i.delta.chat/#6CBFF8FFD505C0FDEA20A66674F2916EA8FBEE99&a=invitebot%40nine.testrun.org&g=Chatmail%20Mutual%20Help&x=7sFF7Ik50pWv6J1z7RVC5527&i=X69wTFfvCfs3d-JzqP0kVA3i&s=ibp-447dU-wUq-52QanwAtWc
about: If you have troubles setting up the relay server, feel free to ask here. about: If you have troubles setting up the relay server, feel free to ask here.

View File

@@ -70,9 +70,6 @@ jobs:
rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true rsync -avz dkimkeys-restore/dkimkeys root@staging-ipv4.testrun.org:/etc/ || true
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown root:root -R /var/lib/acme || true
- name: run formatting checks
run: cmdeploy fmt -v
- name: run deploy-chatmail offline tests - name: run deploy-chatmail offline tests
run: pytest --pyargs cmdeploy run: pytest --pyargs cmdeploy

View File

@@ -70,9 +70,6 @@ jobs:
rsync -avz dkimkeys-restore/dkimkeys root@staging2.testrun.org:/etc/ || true rsync -avz dkimkeys-restore/dkimkeys root@staging2.testrun.org:/etc/ || true
ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org chown root:root -R /var/lib/acme || true ssh -o StrictHostKeyChecking=accept-new -v root@staging2.testrun.org chown root:root -R /var/lib/acme || true
- name: run formatting checks
run: cmdeploy fmt -v
- name: run deploy-chatmail offline tests - name: run deploy-chatmail offline tests
run: pytest --pyargs cmdeploy run: pytest --pyargs cmdeploy

1
.gitignore vendored
View File

@@ -170,3 +170,4 @@ chatmail.zone
/custom/ /custom/
docker-compose.yaml docker-compose.yaml
.env .env
/traefik/data/

50
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,50 @@
This diagram shows components of the chatmail server; this is a draft
overview as of mid-August 2025:
```mermaid
graph LR;
cmdeploy --- sshd;
letsencrypt --- |80|acmetool-redirector;
acmetool-redirector --- |443|nginx-right(["`nginx
(external)`"]);
nginx-external --- |465|postfix;
nginx-external(["`nginx
(external)`"]) --- |8443|nginx-internal["`nginx
(internal)`"];
nginx-internal --- website["`Website
/var/www/html`"];
nginx-internal --- newemail.py;
nginx-internal --- autoconfig.xml;
certs-nginx[("`TLS certs
/var/lib/acme`")] --> nginx-internal;
cron --- chatmail-metrics;
cron --- acmetool;
cron --- expunge;
chatmail-metrics --- website;
acmetool --> certs[("`TLS certs
/var/lib/acme`")];
nginx-external --- |993|dovecot;
autoconfig.xml --- postfix;
autoconfig.xml --- dovecot;
postfix --- echobot;
postfix --- |10080,10081|filtermail;
postfix --- users["`User data
home/vmail/mail`"];
postfix --- |doveauth.socket|doveauth;
dovecot --- |doveauth.socket|doveauth;
dovecot --- users;
dovecot --- |metadata.socket|chatmail-metadata;
doveauth --- users;
expunge --- users;
chatmail-metadata --- iroh-relay;
certs-nginx --> postfix;
certs-nginx --> dovecot;
style certs fill:#ff6;
style certs-nginx fill:#ff6;
style nginx-external fill:#fc9;
style nginx-right fill:#fc9;
```
The edges in this graph should not be taken too literally; they
reflect some sort of communication path or dependency relationship
between components of the chatmail server.

View File

@@ -2,35 +2,38 @@
## untagged ## untagged
- cmdeploy: make --ssh-host work with localhost
([#659](https://github.com/chatmail/relay/pull/659))
- Update iroh-relay to 0.35.0
([#650](https://github.com/chatmail/relay/pull/650))
- Ignore all RCPT TO: parameters
([#651](https://github.com/chatmail/relay/pull/651))
- Use max username length in newemail.py, not min
([#648](https://github.com/chatmail/relay/pull/648))
- Increase maxproc for reinjecting ports from 10 to 100
([#646](https://github.com/chatmail/relay/pull/646))
- Allow ports 143 and 993 to be used by `dovecot` process
([#639](https://github.com/chatmail/relay/pull/639))
- Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs` - Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs`
([#614](https://github.com/chatmail/relay/pull/614)) ([#614](https://github.com/chatmail/relay/pull/614))
- Add markdown tabs blocks for rendering multilingual pages. Add russian language support to `index.md`, `privacy.md`, and `info.md`.
([#614](https://github.com/chatmail/relay/pull/614))
- Fix [Issue 604](https://github.com/chatmail/relay/issues/604), now the `--ssh_host` argument of the `cmdeploy run` command works correctly and does not depend on `config.mail_domain`.
([#614](https://github.com/chatmail/relay/pull/614))
- Add `--skip-dns-check` argument to `cmdeploy run` command, which disables DNS record checking before installation.
([#614](https://github.com/chatmail/relay/pull/614))
- Add `--force` argument to `cmdeploy init` command, which recreates the `chatmail.ini` file.
([#614](https://github.com/chatmail/relay/pull/614))
- Add startup for `fcgiwrap.service` because sometimes it did not start automatically.
([#614](https://github.com/chatmail/relay/pull/614))
- Add extended check when installing `unbound.service`. Now, if it is not shown who exactly is occupying port 53, but `unbound.service` is running, it is considered that the port is occupied by `unbound.service`.
([#614](https://github.com/chatmail/relay/pull/614))
- Add configuration parameters - Add configuration parameters
([#614](https://github.com/chatmail/relay/pull/614)): ([#614](https://github.com/chatmail/relay/pull/614)):
- `is_development_instance` - Indicates that this instance is installed as a temporary/test one (default: `True`)
- `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`) - `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`)
- `acme_email` - Email address used by acmetool to obtain Let's Encrypt certificates (default: empty)
- `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`) - `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`)
- `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`) - `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`)
## 1.7.0 2025-09-11
- Make www upload path configurable
([#618](https://github.com/chatmail/relay/pull/618))
- Check whether GCC is installed in initenv.sh - Check whether GCC is installed in initenv.sh
([#608](https://github.com/chatmail/relay/pull/608)) ([#608](https://github.com/chatmail/relay/pull/608))
@@ -58,6 +61,9 @@
- filtermail: respect config message size limit - filtermail: respect config message size limit
([#572](https://github.com/chatmail/relay/pull/572)) ([#572](https://github.com/chatmail/relay/pull/572))
- Don't deploy if one of the ports used for chatmail relay services is occupied by an unexpected process
([#568](https://github.com/chatmail/relay/pull/568))
- Add config value after how many days large files are deleted - Add config value after how many days large files are deleted
([#555](https://github.com/chatmail/relay/pull/555)) ([#555](https://github.com/chatmail/relay/pull/555))

View File

@@ -259,6 +259,18 @@ This starts a local live development cycle for chatmail web pages:
- Starts a browser window automatically where you can "refresh" as needed. - Starts a browser window automatically where you can "refresh" as needed.
#### Custom web pages
You can skip uploading a web page
by setting `www_folder=disabled` in `chatmail.ini`.
If you want to manage your web pages outside this git repository,
you can set `www_folder` in `chatmail.ini` to a custom directory on your computer.
`cmdeploy run` will upload it as the server's home page,
and if it contains a `src/index.md` file,
will build it with hugo.
## Mailbox directory layout ## Mailbox directory layout
Fresh chatmail addresses have a mailbox directory that contains: Fresh chatmail addresses have a mailbox directory that contains:

View File

@@ -33,7 +33,7 @@ class Config:
self.password_min_length = int(params["password_min_length"]) self.password_min_length = int(params["password_min_length"])
self.passthrough_senders = params["passthrough_senders"].split() self.passthrough_senders = params["passthrough_senders"].split()
self.passthrough_recipients = params["passthrough_recipients"].split() self.passthrough_recipients = params["passthrough_recipients"].split()
self.is_development_instance = params.get("is_development_instance", "true").lower() == "true" self.www_folder = params.get("www_folder", "")
self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.filtermail_smtp_port_incoming = int( self.filtermail_smtp_port_incoming = int(
params["filtermail_smtp_port_incoming"] params["filtermail_smtp_port_incoming"]
@@ -44,9 +44,12 @@ class Config:
) )
self.mtail_address = params.get("mtail_address") self.mtail_address = params.get("mtail_address")
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true" self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
self.use_foreign_cert_manager = params.get("use_foreign_cert_manager", "false").lower() == "true" self.use_foreign_cert_manager = (
self.acme_email = params["acme_email"] params.get("use_foreign_cert_manager", "false").lower() == "true"
self.change_kernel_settings = params.get("change_kernel_settings", "true").lower() == "true" )
self.change_kernel_settings = (
params.get("change_kernel_settings", "true").lower() == "true"
)
self.fs_inotify_max_user_instances_and_watchers = int( self.fs_inotify_max_user_instances_and_watchers = int(
params["fs_inotify_max_user_instances_and_watchers"] params["fs_inotify_max_user_instances_and_watchers"]
) )

View File

@@ -197,11 +197,13 @@ class HackedController(Controller):
class SMTPDiscardRCPTO_options(SMTP): class SMTPDiscardRCPTO_options(SMTP):
def _getparams(self, params): def _getparams(self, params):
# aiosmtpd's SMTP daemon fails to handle a request if there are RCPT TO options # Ignore RCPT TO parameters.
# We just ignore them for our incoming filtermail purposes #
if len(params) == 1 and params[0].startswith("ORCPT"): # Otherwise parameters such as `ORCPT=...`
return {} # or `NOTIFY=DELAY,FAILURE` (generated by Stalwart)
return super()._getparams(params) # make aiosmtpd reject the message here:
# <https://github.com/aio-libs/aiosmtpd/blob/98f578389ae86e5345cc343fa4e5a17b21d9c96d/aiosmtpd/smtp.py#L1379-L1384>
return {}
class OutgoingBeforeQueueHandler: class OutgoingBeforeQueueHandler:

View File

@@ -49,10 +49,7 @@ passthrough_recipients = xstore@testrun.org echo@{mail_domain}
# Deployment Details # Deployment Details
# #
# set to "False" to remove the "development instance" banner on the main page. # SMTP outgoing filtermail and reinjection
is_development_instance = True
# SMTP outgoing filtermail and reinjection
filtermail_smtp_port = 10080 filtermail_smtp_port = 10080
postfix_reinject_port = 10025 postfix_reinject_port = 10025
@@ -66,9 +63,6 @@ disable_ipv6 = False
# if you set "True", acmetool will not be installed and you will have to manage certificates yourself. # if you set "True", acmetool will not be installed and you will have to manage certificates yourself.
use_foreign_cert_manager = False use_foreign_cert_manager = False
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates. Required if `use_foreign_cert_manager` param set as "False".
acme_email =
# #
# Kernel settings # Kernel settings
# #

View File

@@ -15,7 +15,7 @@ ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
def create_newemail_dict(config: Config): def create_newemail_dict(config: Config):
user = "".join(random.choices(ALPHANUMERIC, k=config.username_min_length)) user = "".join(random.choices(ALPHANUMERIC, k=config.username_max_length))
password = "".join( password = "".join(
secrets.choice(ALPHANUMERIC_PUNCT) secrets.choice(ALPHANUMERIC_PUNCT)
for _ in range(config.password_min_length + 3) for _ in range(config.password_min_length + 3)

View File

@@ -20,7 +20,6 @@ dependencies = [
"pytest-xdist", "pytest-xdist",
"execnet", "execnet",
"imap_tools", "imap_tools",
"pymdown-extensions",
] ]
[project.scripts] [project.scripts]

View File

@@ -11,11 +11,11 @@ from io import StringIO
from pathlib import Path from pathlib import Path
from chatmaild.config import Config, read_config from chatmaild.config import Config, read_config
from pyinfra import facts, host from pyinfra import facts, host, logger
from pyinfra.api import FactBase from pyinfra.api import FactBase
from pyinfra.facts.files import File from pyinfra.facts.files import File, Sha256File
from pyinfra.facts.server import Sysctl from pyinfra.facts.server import Sysctl
from pyinfra.facts.systemd import SystemdEnabled, SystemdStatus from pyinfra.facts.systemd import SystemdEnabled
from pyinfra.operations import apt, files, pip, server, systemd from pyinfra.operations import apt, files, pip, server, systemd
from .acmetool import deploy_acmetool from .acmetool import deploy_acmetool
@@ -556,12 +556,12 @@ def deploy_mtail(config):
def deploy_iroh_relay(config) -> None: def deploy_iroh_relay(config) -> None:
(url, sha256sum) = { (url, sha256sum) = {
"x86_64": ( "x86_64": (
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-x86_64-unknown-linux-musl.tar.gz", "https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-x86_64-unknown-linux-musl.tar.gz",
"2ffacf7c0622c26b67a5895ee8e07388769599f60e5f52a3bd40a3258db89b2c", "45c81199dbd70f8c4c30fef7f3b9727ca6e3cea8f2831333eeaf8aa71bf0fac1",
), ),
"aarch64": ( "aarch64": (
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-aarch64-unknown-linux-musl.tar.gz", "https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-aarch64-unknown-linux-musl.tar.gz",
"b915037bcc1ff1110cc9fcb5de4a17c00ff576fd2f568cd339b3b2d54c420dc4", "f8ef27631fac213b3ef668d02acd5b3e215292746a3fc71d90c63115446008b1",
), ),
}[host.get_fact(facts.server.Arch)] }[host.get_fact(facts.server.Arch)]
@@ -570,16 +570,19 @@ def deploy_iroh_relay(config) -> None:
packages=["curl"], packages=["curl"],
) )
server.shell(
name="Download iroh-relay",
commands=[
f"(echo '{sha256sum} /usr/local/bin/iroh-relay' | sha256sum -c) || (curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay.new && mv /usr/local/bin/iroh-relay.new /usr/local/bin/iroh-relay)",
"chmod 755 /usr/local/bin/iroh-relay",
],
)
need_restart = False need_restart = False
existing_sha256sum = host.get_fact(Sha256File, "/usr/local/bin/iroh-relay")
if existing_sha256sum != sha256sum:
server.shell(
name="Download iroh-relay",
commands=[
f"(curl -L {url} | gunzip | tar -x -f - ./iroh-relay -O >/usr/local/bin/iroh-relay.new && (echo '{sha256sum} /usr/local/bin/iroh-relay.new' | sha256sum -c) && mv /usr/local/bin/iroh-relay.new /usr/local/bin/iroh-relay)",
"chmod 755 /usr/local/bin/iroh-relay",
],
)
need_restart = True
systemd_unit = files.put( systemd_unit = files.put(
name="Upload iroh-relay systemd unit", name="Upload iroh-relay systemd unit",
src=importlib.resources.files(__package__).joinpath("iroh-relay.service"), src=importlib.resources.files(__package__).joinpath("iroh-relay.service"),
@@ -619,7 +622,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
check_config(config) check_config(config)
mail_domain = config.mail_domain mail_domain = config.mail_domain
from .www import build_webpages from .www import build_webpages, get_paths
server.group(name="Create vmail group", group="vmail", system=True) server.group(name="Create vmail group", group="vmail", system=True)
server.user(name="Create vmail user", user="vmail", group="vmail", system=True) server.user(name="Create vmail user", user="vmail", group="vmail", system=True)
@@ -676,12 +679,32 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
# to use 127.0.0.1 as the resolver. # to use 127.0.0.1 as the resolver.
from cmdeploy.cmdeploy import Out from cmdeploy.cmdeploy import Out
process_on_53 = host.get_fact(Port, port=53) port_services = [
if host.get_fact(SystemdStatus, services="unbound").get("unbound.service"): (["master", "smtpd"], 25),
process_on_53 = "unbound" ("unbound", 53),
if process_on_53 not in (None, "unbound"): ("acmetool", 80),
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}") (["imap-login", "dovecot"], 143),
exit(1) ("nginx", 443),
(["master", "smtpd"], 465),
(["master", "smtpd"], 587),
(["imap-login", "dovecot"], 993),
("iroh-relay", 3340),
("nginx", 8443),
(["master", "smtpd"], config.postfix_reinject_port),
(["master", "smtpd"], config.postfix_reinject_port_incoming),
("filtermail", config.filtermail_smtp_port),
("filtermail", config.filtermail_smtp_port_incoming),
]
for service, port in port_services:
print(f"Checking if port {port} is available for {service}...")
running_service = host.get_fact(Port, port=port)
if running_service:
if running_service not in service:
Out().red(
f"Deploy failed: port {port} is occupied by: {running_service}"
)
exit(1)
apt.packages( apt.packages(
name="Install unbound", name="Install unbound",
packages=["unbound", "unbound-anchor", "dnsutils"], packages=["unbound", "unbound-anchor", "dnsutils"],
@@ -706,7 +729,6 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
if not config.use_foreign_cert_manager: if not config.use_foreign_cert_manager:
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"] tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
deploy_acmetool( deploy_acmetool(
email = config.acme_email,
domains=tls_domains, domains=tls_domains,
) )
@@ -736,12 +758,16 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
packages=["fcgiwrap"], packages=["fcgiwrap"],
) )
www_path = importlib.resources.files(__package__).joinpath("../../../www").resolve() www_path, src_dir, build_dir = get_paths(config)
# if www_folder was set to a non-existing folder, skip upload
build_dir = www_path.joinpath("build") if not www_path.is_dir():
src_dir = www_path.joinpath("src") logger.warning("Building web pages is disabled in chatmail.ini, skipping")
build_webpages(src_dir, build_dir, config) else:
files.rsync(f"{build_dir}/", "/var/www/html", flags=["-avz"]) # if www_folder is a hugo page, build it
if build_dir:
www_path = build_webpages(src_dir, build_dir, config)
# if it is not a hugo page, upload it as is
files.rsync(f"{www_path}/", "/var/www/html", flags=["-avz"])
_install_remote_venv_with_chatmaild(config) _install_remote_venv_with_chatmaild(config)
debug = False debug = False
@@ -788,12 +814,11 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
enabled=True, enabled=True,
restarted=nginx_need_restart, restarted=nginx_need_restart,
) )
systemd.service( systemd.service(
name="Start and enable fcgiwrap", name="Restart echobot if postfix and dovecot were just started",
service="fcgiwrap.service", service="echobot.service",
running=True, restarted=postfix_need_restart and dovecot_need_restart,
enabled=True,
) )
# This file is used by auth proxy. # This file is used by auth proxy.

View File

@@ -1,7 +1,5 @@
import importlib.resources import importlib.resources
from pyinfra import host
from pyinfra.facts.systemd import SystemdStatus
from pyinfra.operations import apt, files, server, systemd from pyinfra.operations import apt, files, server, systemd
@@ -54,12 +52,6 @@ def deploy_acmetool(email="", domains=[]):
group="root", group="root",
mode="644", mode="644",
) )
if host.get_fact(SystemdStatus).get("nginx.service"):
systemd.service(
name="Stop nginx service to free port 80",
service="nginx",
running=False,
)
systemd.service( systemd.service(
name="Setup acmetool-redirector service", name="Setup acmetool-redirector service",

View File

@@ -32,29 +32,17 @@ def init_cmd_options(parser):
action="store", action="store",
help="fully qualified DNS domain name for your chatmail instance", help="fully qualified DNS domain name for your chatmail instance",
) )
parser.add_argument(
"--force",
dest="recreate_ini",
action="store_true",
help="force reacreate ini file",
)
def init_cmd(args, out): def init_cmd(args, out):
"""Initialize chatmail config file.""" """Initialize chatmail config file."""
mail_domain = args.chatmail_domain mail_domain = args.chatmail_domain
inipath = args.inipath
if args.inipath.exists(): if args.inipath.exists():
if not args.recreate_ini: print(f"Path exists, not modifying: {args.inipath}")
print(f"[WARNING] Path exists, not modifying: {inipath}") return 1
return 1 else:
else: write_initial_config(args.inipath, mail_domain, overrides={})
print(f"[WARNING] Force argument was provided, deleting config file: {inipath}") out.green(f"created config file for {mail_domain} in {args.inipath}")
inipath.unlink()
return 0
write_initial_config(inipath, mail_domain, overrides={})
out.green(f"created config file for {mail_domain} in {inipath}")
def run_cmd_options(parser): def run_cmd_options(parser):
@@ -73,25 +61,19 @@ def run_cmd_options(parser):
parser.add_argument( parser.add_argument(
"--ssh-host", "--ssh-host",
dest="ssh_host", dest="ssh_host",
help="specify an SSH host to deploy to; uses mail_domain from chatmail.ini by default", help="Deploy to 'localhost', via 'docker', or to a specific SSH host",
)
parser.add_argument(
"--skip-dns-check",
dest="dns_check_disabled",
action="store_true",
help="disable checks nslookup for dns",
) )
def run_cmd(args, out): def run_cmd(args, out):
"""Deploy chatmail services on the remote server.""" """Deploy chatmail services on the remote server."""
sshexec = args.get_sshexec() ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
sshexec = get_sshexec(ssh_host)
require_iroh = args.config.enable_iroh_relay require_iroh = args.config.enable_iroh_relay
if not args.dns_check_disabled: remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain) if not dns.check_initial_remote_data(remote_data, print=out.red):
if not dns.check_initial_remote_data(remote_data, print=out.red): return 1
return 1
env = os.environ.copy() env = os.environ.copy()
env["CHATMAIL_INI"] = args.inipath env["CHATMAIL_INI"] = args.inipath
@@ -99,8 +81,11 @@ def run_cmd(args, out):
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else "" env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve() deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
ssh_host = args.config.mail_domain if not args.ssh_host else args.ssh_host
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 in ["localhost", "docker"]:
cmd = f"{pyinf} @local {deploy_path} -y"
if version.parse(pyinfra.__version__) < version.parse("3"): if version.parse(pyinfra.__version__) < version.parse("3"):
out.red("Please re-run scripts/initenv.sh to update pyinfra to version 3.") out.red("Please re-run scripts/initenv.sh to update pyinfra to version 3.")
return 1 return 1
@@ -108,6 +93,14 @@ def run_cmd(args, out):
try: try:
retcode = out.check_call(cmd, env=env) retcode = out.check_call(cmd, env=env)
if retcode == 0: if retcode == 0:
print("\nYou can try out the relay by talking to this echo bot: ")
sshexec = SSHExec(args.config.mail_domain, verbose=args.verbose)
print(
sshexec(
call=remote.rshell.shell,
kwargs=dict(command="cat /var/lib/echobot/invite-link.txt"),
)
)
server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/" server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/"
delimiter_line = "=" * len(server_deployed_message) delimiter_line = "=" * len(server_deployed_message)
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}") out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
@@ -132,11 +125,17 @@ def dns_cmd_options(parser):
default=None, default=None,
help="write out a zonefile", help="write out a zonefile",
) )
parser.add_argument(
"--ssh-host",
dest="ssh_host",
help="Run the DNS queries on 'localhost', in the chatmail 'docker' container, or on a specific SSH host",
)
def dns_cmd(args, out): def dns_cmd(args, out):
"""Check DNS entries and optionally generate dns zone file.""" """Check DNS entries and optionally generate dns zone file."""
sshexec = args.get_sshexec() ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain) remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
if not remote_data: if not remote_data:
return 1 return 1
@@ -273,17 +272,8 @@ class Out:
def green(self, msg, file=sys.stderr): def green(self, msg, file=sys.stderr):
print(colored(msg, "green"), file=file) print(colored(msg, "green"), file=file)
def yellow(self, msg, file=sys.stderr): def __call__(self, msg, red=False, green=False, file=sys.stdout):
print(colored(msg, "yellow"), file=file) color = "red" if red else ("green" if green else None)
def __call__(self, msg, red=False, green=False, yellow=False, file=sys.stdout):
color = None
if red:
color = "red"
elif green:
color = "green"
elif yellow:
color = "yellow"
print(colored(msg, color), file=file) print(colored(msg, color), file=file)
def check_call(self, arg, env=None, quiet=False): def check_call(self, arg, env=None, quiet=False):
@@ -354,6 +344,16 @@ def get_parser():
return parser return parser
def get_sshexec(ssh_host: str, verbose=True):
if ssh_host in ["localhost", "@local"]:
return "localhost"
elif ssh_host == "docker":
return "docker"
if verbose:
print(f"[ssh] login to {ssh_host}")
return SSHExec(ssh_host, verbose=verbose)
def main(args=None): def main(args=None):
"""Provide main entry point for 'cmdeploy' CLI invocation.""" """Provide main entry point for 'cmdeploy' CLI invocation."""
parser = get_parser() parser = get_parser()
@@ -361,13 +361,6 @@ def main(args=None):
if not hasattr(args, "func"): if not hasattr(args, "func"):
return parser.parse_args(["-h"]) return parser.parse_args(["-h"])
def get_sshexec():
host = args.ssh_host if hasattr(args, "ssh_host") and args.ssh_host else args.config.mail_domain
print(f"[ssh] login to {host}")
return SSHExec(host, verbose=args.verbose)
args.get_sshexec = get_sshexec
out = Out() out = Out()
kwargs = {} kwargs = {}
if args.func.__name__ not in ("init_cmd", "fmt_cmd"): if args.func.__name__ not in ("init_cmd", "fmt_cmd"):

View File

@@ -7,9 +7,15 @@ from . import remote
def get_initial_remote_data(sshexec, mail_domain): def get_initial_remote_data(sshexec, mail_domain):
return sshexec.logged( if sshexec == "localhost":
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain) result = remote.rdns.perform_initial_checks(mail_domain)
) elif sshexec == "docker":
result = remote.rdns.perform_initial_checks(mail_domain, pre_command="docker exec chatmail ")
else:
result = sshexec.logged(
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
)
return result
def check_initial_remote_data(remote_data, *, print=print): def check_initial_remote_data(remote_data, *, print=print):
@@ -44,10 +50,14 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
"""Check existing DNS records, optionally write them to zone file """Check existing DNS records, optionally write them to zone file
and return (exitcode, remote_data) tuple.""" and return (exitcode, remote_data) tuple."""
required_diff, recommended_diff = sshexec.logged( if sshexec in ["localhost", "docker"]:
remote.rdns.check_zonefile, required_diff, recommended_diff = remote.rdns.check_zonefile(
kwargs=dict(zonefile=zonefile, mail_domain=remote_data["mail_domain"]), zonefile=zonefile, verbose=False
) )
else:
required_diff, recommended_diff = sshexec.logged(
remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False),
)
returncode = 0 returncode = 0
if required_diff: if required_diff:

View File

@@ -77,13 +77,13 @@ scache unix - - y - 1 scache
postlog unix-dgram n - n - 1 postlogd postlog unix-dgram n - n - 1 postlogd
filter unix - n n - - lmtp 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 - 10 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 smtpd_milters=unix:opendkim/opendkim.sock -o smtpd_milters=unix:opendkim/opendkim.sock
-o cleanup_service_name=authclean -o cleanup_service_name=authclean
# Local SMTP server for reinjecting incoming filtered mail # Local SMTP server for reinjecting incoming filtered mail
127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 10 smtpd 127.0.0.1:{{ config.postfix_reinject_port_incoming }} inet n - n - 100 smtpd
-o syslog_name=postfix/reinject_incoming -o syslog_name=postfix/reinject_incoming
-o smtpd_milters=unix:opendkim/opendkim.sock -o smtpd_milters=unix:opendkim/opendkim.sock

View File

@@ -12,23 +12,23 @@ All functions of this module
import re import re
from .rshell import CalledProcessError, shell from .rshell import CalledProcessError, shell, log_progress
def perform_initial_checks(mail_domain): def perform_initial_checks(mail_domain, pre_command=""):
"""Collecting initial DNS settings.""" """Collecting initial DNS settings."""
assert mail_domain assert mail_domain
if not shell("dig", fail_ok=True): if not shell("dig", fail_ok=True, print=log_progress):
shell("apt-get update && apt-get install -y dnsutils") shell("apt-get update && apt-get install -y dnsutils", print=log_progress)
A = query_dns("A", mail_domain) A = query_dns("A", mail_domain)
AAAA = query_dns("AAAA", mail_domain) AAAA = query_dns("AAAA", mail_domain)
MTA_STS = query_dns("CNAME", f"mta-sts.{mail_domain}") MTA_STS = query_dns("CNAME", f"mta-sts.{mail_domain}")
WWW = query_dns("CNAME", f"www.{mail_domain}") WWW = query_dns("CNAME", f"www.{mail_domain}")
res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, WWW=WWW) res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, WWW=WWW)
res["acme_account_url"] = shell("acmetool account-url", fail_ok=True) res["acme_account_url"] = shell(pre_command + "acmetool account-url", fail_ok=True, print=log_progress)
res["dkim_entry"], res["web_dkim_entry"] = get_dkim_entry( res["dkim_entry"], res["web_dkim_entry"] = get_dkim_entry(
mail_domain, dkim_selector="opendkim" mail_domain, pre_command, dkim_selector="opendkim"
) )
if not MTA_STS or not WWW or (not A and not AAAA): if not MTA_STS or not WWW or (not A and not AAAA):
@@ -40,11 +40,12 @@ def perform_initial_checks(mail_domain):
return res return res
def get_dkim_entry(mail_domain, dkim_selector): def get_dkim_entry(mail_domain, pre_command, dkim_selector):
try: try:
dkim_pubkey = shell( dkim_pubkey = shell(
f"openssl rsa -in /etc/dkimkeys/{dkim_selector}.private " f"{pre_command}openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'" "-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'",
print=log_progress
) )
except CalledProcessError: except CalledProcessError:
return return
@@ -61,7 +62,7 @@ def query_dns(typ, domain):
# Get autoritative nameserver from the SOA record. # Get autoritative nameserver from the SOA record.
soa_answers = [ soa_answers = [
x.split() x.split()
for x in shell(f"dig -r -q {domain} -t SOA +noall +authority +answer").split( for x in shell(f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress).split(
"\n" "\n"
) )
] ]
@@ -71,13 +72,13 @@ def query_dns(typ, domain):
ns = soa[0][4] ns = soa[0][4]
# Query authoritative nameserver directly to bypass DNS cache. # Query authoritative nameserver directly to bypass DNS cache.
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short") res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
if res: if res:
return res.split("\n")[0] return res.split("\n")[0]
return "" return ""
def check_zonefile(zonefile, mail_domain): def check_zonefile(zonefile, verbose=True):
"""Check expected zone file entries.""" """Check expected zone file entries."""
required = True required = True
required_diff = [] required_diff = []
@@ -89,7 +90,7 @@ def check_zonefile(zonefile, mail_domain):
continue continue
if not zf_line.strip() or zf_line.startswith(";"): if not zf_line.strip() or zf_line.startswith(";"):
continue continue
print(f"dns-checking {zf_line!r}") print(f"dns-checking {zf_line!r}") if verbose else log_progress("")
zf_domain, zf_typ, zf_value = zf_line.split(maxsplit=2) zf_domain, zf_typ, zf_value = zf_line.split(maxsplit=2)
zf_domain = zf_domain.rstrip(".") zf_domain = zf_domain.rstrip(".")
zf_value = zf_value.strip() zf_value = zf_value.strip()

View File

@@ -1,7 +1,14 @@
import sys
from subprocess import DEVNULL, CalledProcessError, check_output from subprocess import DEVNULL, CalledProcessError, check_output
def shell(command, fail_ok=False): def log_progress(data):
sys.stderr.write(".")
sys.stderr.flush()
def shell(command, fail_ok=False, print=print):
print(f"$ {command}") print(f"$ {command}")
args = dict(shell=True) args = dict(shell=True)
if fail_ok: if fail_ok:

View File

@@ -42,6 +42,7 @@ def bootstrap_remote(gateway, remote=remote):
def print_stderr(item="", end="\n"): def print_stderr(item="", end="\n"):
print(item, file=sys.stderr, end=end) print(item, file=sys.stderr, end=end)
sys.stderr.flush()
class SSHExec: class SSHExec:
@@ -70,10 +71,6 @@ class SSHExec:
raise self.FuncError(data) raise self.FuncError(data)
def logged(self, call, kwargs): def logged(self, call, kwargs):
def log_progress(data):
sys.stderr.write(".")
sys.stderr.flush()
title = call.__doc__ title = call.__doc__
if not title: if not title:
title = call.__name__ title = call.__name__
@@ -82,6 +79,6 @@ class SSHExec:
return self(call, kwargs, log_callback=print_stderr) return self(call, kwargs, log_callback=print_stderr)
else: else:
print_stderr(title, end="") print_stderr(title, end="")
res = self(call, kwargs, log_callback=log_progress) res = self(call, kwargs, log_callback=remote.rshell.log_progress)
print_stderr() print_stderr()
return res return res

View File

@@ -31,7 +31,7 @@ class TestSSHExecutor:
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert err.startswith("Collecting") assert err.startswith("Collecting")
assert err.endswith("....\n") #assert err.endswith("....\n")
assert err.count("\n") == 1 assert err.count("\n") == 1
sshexec.verbose = True sshexec.verbose = True
@@ -40,7 +40,7 @@ class TestSSHExecutor:
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
lines = err.split("\n") lines = err.split("\n")
assert len(lines) > 4 #assert len(lines) > 4
assert remote.rdns.perform_initial_checks.__doc__ in lines[0] assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
def test_exception(self, sshexec, capsys): def test_exception(self, sshexec, capsys):

View File

@@ -1,8 +1,10 @@
import importlib
import os import os
import pytest import pytest
from cmdeploy.cmdeploy import get_parser, main from cmdeploy.cmdeploy import get_parser, main
from cmdeploy.www import get_paths
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -27,6 +29,28 @@ class TestCmdline:
assert main(["init", "chat.example.org"]) == 1 assert main(["init", "chat.example.org"]) == 1
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert "path exists" in out.lower() assert "path exists" in out.lower()
assert main(["init", "chat.example.org", "--force"]) == 0
out, err = capsys.readouterr()
assert "deleting config file" in out.lower() def test_www_folder(example_config, tmp_path):
reporoot = importlib.resources.files(__package__).joinpath("../../../../").resolve()
assert not example_config.www_folder
www_path, src_dir, build_dir = get_paths(example_config)
assert www_path.absolute() == reporoot.joinpath("www").absolute()
assert src_dir == reporoot.joinpath("www").joinpath("src")
assert build_dir == reporoot.joinpath("www").joinpath("build")
example_config.www_folder = "disabled"
www_path, _, _ = get_paths(example_config)
assert not www_path.is_dir()
example_config.www_folder = str(tmp_path)
www_path, src_dir, build_dir = get_paths(example_config)
assert www_path == tmp_path
assert not src_dir.exists()
assert not build_dir
src_path = tmp_path.joinpath("src")
os.mkdir(src_path)
with open(src_path / "index.md", "w") as f:
f.write("# Test")
www_path, src_dir, build_dir = get_paths(example_config)
assert www_path == tmp_path
assert src_dir == src_path
assert build_dir == tmp_path.joinpath("build")

View File

@@ -89,18 +89,14 @@ class TestZonefileChecks:
def test_check_zonefile_all_ok(self, cm_data, mockdns_base): def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
zonefile = cm_data.get("zftest.zone") zonefile = cm_data.get("zftest.zone")
parse_zonefile_into_dict(zonefile, mockdns_base) parse_zonefile_into_dict(zonefile, mockdns_base)
required_diff, recommended_diff = remote.rdns.check_zonefile( required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
zonefile, "some.domain"
)
assert not required_diff and not recommended_diff assert not required_diff and not recommended_diff
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base): def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
zonefile = cm_data.get("zftest.zone") zonefile = cm_data.get("zftest.zone")
zonefile_mocked = zonefile.split("; Recommended")[0] zonefile_mocked = zonefile.split("; Recommended")[0]
parse_zonefile_into_dict(zonefile_mocked, mockdns_base) parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
required_diff, recommended_diff = remote.rdns.check_zonefile( required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
zonefile, "some.domain"
)
assert not required_diff assert not required_diff
assert len(recommended_diff) == 8 assert len(recommended_diff) == 8

View File

@@ -3,6 +3,7 @@ import importlib.resources
import time import time
import traceback import traceback
import webbrowser import webbrowser
from pathlib import Path
import markdown import markdown
from chatmaild.config import read_config from chatmaild.config import read_config
@@ -25,15 +26,30 @@ def prepare_template(source):
assert source.exists(), source assert source.exists(), source
render_vars = {} render_vars = {}
render_vars["pagename"] = "home" if source.stem == "index" else source.stem render_vars["pagename"] = "home" if source.stem == "index" else source.stem
# tabs usage for multiple languages https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/tab/ render_vars["markdown_html"] = markdown.markdown(source.read_text())
render_vars["markdown_html"] = markdown.markdown(source.read_text(), extensions=['pymdownx.blocks.tab'])
page_layout = source.with_name("page-layout.html").read_text() page_layout = source.with_name("page-layout.html").read_text()
return render_vars, page_layout return render_vars, page_layout
def build_webpages(src_dir, build_dir, config): def get_paths(config) -> (Path, Path, Path):
reporoot = importlib.resources.files(__package__).joinpath("../../../").resolve()
www_path = Path(config.www_folder)
# if www_folder was not set, use default directory
if config.www_folder == "":
www_path = reporoot.joinpath("www")
src_dir = www_path.joinpath("src")
# if www_folder is a hugo page, build it
if src_dir.joinpath("index.md").is_file():
build_dir = www_path.joinpath("build")
# if it is not a hugo page, upload it as is
else:
build_dir = None
return www_path, src_dir, build_dir
def build_webpages(src_dir, build_dir, config) -> Path:
try: try:
_build_webpages(src_dir, build_dir, config) return _build_webpages(src_dir, build_dir, config)
except Exception: except Exception:
print(traceback.format_exc()) print(traceback.format_exc())
@@ -107,15 +123,11 @@ def main():
config = read_config(inipath) config = read_config(inipath)
config.webdev = True config.webdev = True
assert config.mail_domain assert config.mail_domain
www_path = reporoot.joinpath("www")
src_path = www_path.joinpath("src")
stats = None
build_dir = www_path.joinpath("build")
src_dir = www_path.joinpath("src")
index_path = build_dir.joinpath("index.html")
# start web page generation, open a browser and wait for changes # start web page generation, open a browser and wait for changes
build_webpages(src_dir, build_dir, config) www_path, src_path, build_dir = get_paths(config)
build_dir = build_webpages(src_path, build_dir, config)
index_path = build_dir.joinpath("index.html")
webbrowser.open(str(index_path)) webbrowser.open(str(index_path))
stats = snapshot_dir_stats(src_path) stats = snapshot_dir_stats(src_path)
print(f"\nOpened URL: file://{index_path.resolve()}\n") print(f"\nOpened URL: file://{index_path.resolve()}\n")
@@ -136,7 +148,7 @@ def main():
changenum += 1 changenum += 1
stats = newstats stats = newstats
build_webpages(src_dir, build_dir, config) build_webpages(src_path, build_dir, config)
print(f"[{changenum}] regenerated web pages at: {index_path}") print(f"[{changenum}] regenerated web pages at: {index_path}")
print(f"URL: file://{index_path.resolve()}\n\n") print(f"URL: file://{index_path.resolve()}\n\n")
count = 0 count = 0

View File

@@ -18,8 +18,6 @@ RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend && \
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y \ apt-get install -y \
openssh-client \
openssh-server \
git \ git \
python3 \ python3 \
python3-venv \ python3-venv \
@@ -54,23 +52,6 @@ RUN apt-get update && \
done \ done \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN systemctl enable \
ssh \
fcgiwrap
RUN sed -i 's/^#PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config && \
sed -i 's/^#PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config && \
ssh-keygen -P "" -t rsa -b 2048 -f /root/.ssh/id_rsa && \
mkdir -p /root/.ssh && \
cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \
SSH_USER_CONFIG="/root/.ssh/config" && \
echo "Host localhost" > "$SSH_USER_CONFIG" && \
echo " HostName localhost" >> "$SSH_USER_CONFIG" && \
echo " User root" >> "$SSH_USER_CONFIG" && \
echo " StrictHostKeyChecking no" >> "$SSH_USER_CONFIG" && \
echo " UserKnownHostsFile /dev/null" >> "$SSH_USER_CONFIG"
## TODO: deny access for all insteed root form 127.0.0.1 https://unix.stackexchange.com/a/406264
WORKDIR /opt/chatmail WORKDIR /opt/chatmail
ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service

View File

@@ -2,7 +2,7 @@ services:
chatmail: chatmail:
build: build:
context: ./docker context: ./docker
dockerfile: chatmail_server.dockerfile dockerfile: chatmail_relay.dockerfile
tags: tags:
- chatmail-relay:latest - chatmail-relay:latest
image: chatmail-relay:latest image: chatmail-relay:latest
@@ -20,11 +20,11 @@ services:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
environment: environment:
MAIL_DOMAIN: <your_domain> MAIL_DOMAIN: $MAIL_DOMAIN
CHANGE_KERNEL_SETTINGS: "False" CHANGE_KERNEL_SETTINGS: "False"
ACME_EMAIL: <your_email> ACME_EMAIL: $ACME_EMAIL
# RECREATE_VENV: "false"
MAX_MESSAGE_SIZE: "50M" # MAX_MESSAGE_SIZE: "50M"
# DEBUG_COMMANDS_ENABLED: "true" # DEBUG_COMMANDS_ENABLED: "true"
# FORCE_REINIT_INI_FILE: "true" # FORCE_REINIT_INI_FILE: "true"
# USE_FOREIGN_CERT_MANAGER: "True" # USE_FOREIGN_CERT_MANAGER: "True"
@@ -32,16 +32,17 @@ services:
# CERTS_MONITORING_TIMEOUT: 10 # CERTS_MONITORING_TIMEOUT: 10
# IS_DEVELOPMENT_INSTANCE: "True" # IS_DEVELOPMENT_INSTANCE: "True"
ports: ports:
- "80:80"
- "443:443"
- "25:25" - "25:25"
- "587:587" - "587:587"
- "143:143" - "143:143"
- "465:465"
- "993:993" - "993:993"
- "443:443"
volumes: volumes:
## system ## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail - ./:/opt/chatmail
- ./data/acme:/var/lib/acme
## data ## data
- ./data/chatmail:/home - ./data/chatmail:/home

View File

@@ -1,8 +1,8 @@
services: services:
chatmail: chatmail:
build: build:
context: ./docker context: ./docker
dockerfile: chatmail_server.dockerfile dockerfile: chatmail_relay.dockerfile
tags: tags:
- chatmail-relay:latest - chatmail-relay:latest
image: chatmail-relay:latest image: chatmail-relay:latest
@@ -26,9 +26,10 @@ services:
# MAX_MESSAGE_SIZE: "50M" # MAX_MESSAGE_SIZE: "50M"
# DEBUG_COMMANDS_ENABLED: "true" # DEBUG_COMMANDS_ENABLED: "true"
# FORCE_REINIT_INI_FILE: "true" # FORCE_REINIT_INI_FILE: "true"
# RECREATE_VENV: "false"
USE_FOREIGN_CERT_MANAGER: "true" USE_FOREIGN_CERT_MANAGER: "true"
CHANGE_KERNEL_SETTINGS: "false" CHANGE_KERNEL_SETTINGS: "false"
PATH_TO_SSL_CONTAINER: $PATH_TO_SSL_CONTAINER PATH_TO_SSL: "${CERTS_ROOT_DIR_CONTAINER}/${MAIL_DOMAIN}"
ENABLE_CERTS_MONITORING: "true" ENABLE_CERTS_MONITORING: "true"
# CERTS_MONITORING_TIMEOUT: 60 # CERTS_MONITORING_TIMEOUT: 60
# IS_DEVELOPMENT_INSTANCE: "true" # IS_DEVELOPMENT_INSTANCE: "true"
@@ -36,13 +37,14 @@ services:
- "25:25" - "25:25"
- "587:587" - "587:587"
- "143:143" - "143:143"
- "465:465"
- "993:993" - "993:993"
volumes: volumes:
## system ## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail - ./:/opt/chatmail
- ${PATH_TO_SSL_HOST}:${PATH_TO_SSL_CONTAINER}:ro - ${CERTS_ROOT_DIR_HOST}:${CERTS_ROOT_DIR_CONTAINER}:ro
## data ## data
- ./data/chatmail:/home - ./data/chatmail:/home
# - ./data/chatmail-dkimkeys:/etc/dkimkeys # - ./data/chatmail-dkimkeys:/etc/dkimkeys
@@ -67,6 +69,22 @@ services:
- traefik.http.routers.chatmail-relay.tls=true - traefik.http.routers.chatmail-relay.tls=true
- traefik.http.routers.chatmail-relay.tls.certresolver=letsEncrypt - traefik.http.routers.chatmail-relay.tls.certresolver=letsEncrypt
traefik_init:
image: alpine:latest
restart: on-failure
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
working_dir: /app
entrypoint: sh -c '
touch acme.json &&
chown 0:0 ./acme.json &&
chmod 600 ./acme.json'
volumes:
- ./traefik/data:/app
traefik: traefik:
image: traefik:v3.3 image: traefik:v3.3
container_name: traefik container_name: traefik
@@ -77,18 +95,21 @@ services:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
command: command:
- --configFile=/config.yaml - "--configFile=/config.yaml"
- "--certificatesresolvers.letsEncrypt.acme.email=${ACME_EMAIL}"
# ports: # ports:
# - "80:80" # - "80:80"
# - "443:443" # - "443:443"
network_mode: host
depends_on:
traefik_init:
condition: service_completed_successfully
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./data/traefik/config.yaml:/config.yaml - ./traefik/config.yaml:/config.yaml
- ./data/traefik/acme.json:/acme.json - ./traefik/data/acme.json:/acme.json
- ./data/traefik/dynamic-configs:/dynamic/conf - ./traefik/dynamic-configs:/dynamic/conf
network_mode: host
traefik-certs-dumper: traefik-certs-dumper:
image: ldez/traefik-certs-dumper:v2.10.0 image: ldez/traefik-certs-dumper:v2.10.0
restart: unless-stopped restart: unless-stopped
@@ -110,6 +131,6 @@ services:
environment: environment:
CERTS_DIR: /data/letsencrypt/certs CERTS_DIR: /data/letsencrypt/certs
volumes: volumes:
- ./data/traefik/letsencrypt:/data/letsencrypt - ./traefik/data/letsencrypt:/data/letsencrypt
- ./data/traefik/acme.json:/data/acme.json - ./traefik/data/acme.json:/data/acme.json
- ./data/traefik/post-hook.sh:/post-hook.sh - ./traefik/post-hook.sh:/post-hook.sh

View File

@@ -1,4 +1,5 @@
MAIL_DOMAIN="chat.example.com" MAIL_DOMAIN="chat.example.com"
ACME_EMAIL="my.email@gmail.com"
PATH_TO_SSL_HOST="/opt/traefik/data/letsencrypt/certs/${MAIL_DOMAIN}" CERTS_ROOT_DIR_HOST="./traefik/data/letsencrypt/certs"
PATH_TO_SSL_CONTAINER="/var/lib/acme/live/${MAIL_DOMAIN}" CERTS_ROOT_DIR_CONTAINER="/var/lib/acme/live"

View File

@@ -1,13 +1,17 @@
#!/bin/bash #!/bin/bash
set -eo pipefail set -eo pipefail
unlink /etc/nginx/sites-enabled/default || true
if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then
if [ ! -f "$PATH_TO_SSL_CONTAINER/fullchain" ]; then if [ ! -f "$PATH_TO_SSL/fullchain" ]; then
echo "Error: file '$PATH_TO_SSL_CONTAINER/fullchain' does not exist. Exiting..." > /dev/stderr echo "Error: file '$PATH_TO_SSL/fullchain' does not exist. Exiting..." > /dev/stderr
sleep 2
exit 1 exit 1
fi fi
if [ ! -f "$PATH_TO_SSL_CONTAINER/privkey" ]; then if [ ! -f "$PATH_TO_SSL/privkey" ]; then
echo "Error: file '$PATH_TO_SSL_CONTAINER/privkey' does not exist. Exiting..." > /dev/stderr echo "Error: file '$PATH_TO_SSL/privkey' does not exist. Exiting..." > /dev/stderr
sleep 2
exit 1 exit 1
fi fi
fi fi

View File

@@ -4,8 +4,9 @@ set -eo pipefail
export INI_FILE="${INI_FILE:-chatmail.ini}" export INI_FILE="${INI_FILE:-chatmail.ini}"
export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}"
export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}"
export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}"
export CHANGE_KERNEL_SETTINGS=${CHANGE_KERNEL_SETTINGS:-"False"} export CHANGE_KERNEL_SETTINGS=${CHANGE_KERNEL_SETTINGS:-"False"}
export RECREATE_VENV=${RECREATE_VENV:-"false"}
if [ -z "$MAIL_DOMAIN" ]; then if [ -z "$MAIL_DOMAIN" ]; then
echo "ERROR: Environment variable 'MAIL_DOMAIN' must be set!" >&2 echo "ERROR: Environment variable 'MAIL_DOMAIN' must be set!" >&2
@@ -19,7 +20,7 @@ debug_commands() {
} }
calculate_hash() { calculate_hash() {
find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}'
} }
monitor_certificates() { monitor_certificates() {
@@ -35,8 +36,8 @@ monitor_certificates() {
current_hash=$(calculate_hash) current_hash=$(calculate_hash)
if [[ "$current_hash" != "$previous_hash" ]]; then if [[ "$current_hash" != "$previous_hash" ]]; then
# TODO: add an option to restart at a specific time interval # TODO: add an option to restart at a specific time interval
echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services."
systemctl restart nginx.service systemctl reload nginx.service
systemctl reload dovecot.service systemctl reload dovecot.service
systemctl reload postfix.service systemctl reload postfix.service
previous_hash=$current_hash previous_hash=$current_hash
@@ -61,12 +62,15 @@ chown opendkim:opendkim /etc/dkimkeys/opendkim.txt
# TODO: Move to debug_commands after git clone is moved to dockerfile. # TODO: Move to debug_commands after git clone is moved to dockerfile.
git config --global --add safe.directory /opt/chatmail git config --global --add safe.directory /opt/chatmail
if [ "$RECREATE_VENV" == "true" ]; then
rm -rf venv
fi
./scripts/initenv.sh ./scripts/initenv.sh
./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN ./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN
bash /update_ini.sh bash /update_ini.sh
./scripts/cmdeploy run --ssh-host localhost --skip-dns-check ./scripts/cmdeploy run --ssh-host docker
echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf
systemctl restart systemd-journald systemctl restart systemd-journald

View File

@@ -1,6 +1,5 @@
# Known issues and limitations # Known issues and limitations
- Installation using acmetool (`docker-compose-default.yaml`) may NOT work. In this case, use installation via traefik (`docker-compose-traefik.yaml`). Personally, during my tests, I encountered the error `could not install DNS challenge, no hooks succeeded;`, which I was unable to fix.
- Chatmail will be reinstalled every time the container is started (longer the first time, faster on subsequent starts). This is how the original installer works because it wasnt designed for Docker. At the end of the documentation, theres a [proposed solution](#locking-the-chatmail-version). - Chatmail will be reinstalled every time the container is started (longer the first time, faster on subsequent starts). This is how the original installer works because it wasnt designed for Docker. At the end of the documentation, theres a [proposed solution](#locking-the-chatmail-version).
- Requires cgroups v2 configured in the system. Operation with cgroups v1 has not been tested. - Requires cgroups v2 configured in the system. Operation with cgroups v1 has not been tested.
- Yes, of course, using systemd inside a container is a hack, and it would be better to split it into several services, but since this is an MVP, it turned out to be easier to do it this way initially than to rewrite the entire deployment system. - Yes, of course, using systemd inside a container is a hack, and it would be better to split it into several services, but since this is an MVP, it turned out to be easier to do it this way initially than to rewrite the entire deployment system.
@@ -48,13 +47,15 @@ cp ./docker/docker-compose-default.yaml docker-compose.yaml
# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml # cp ./docker/docker-compose-traefik.yaml docker-compose.yaml
``` ```
2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`. Required only when installing with traefik; if using the default setup, you can skip this step. 2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`.
```shell ```shell
cp ./docker/example.env .env cp ./docker/example.env .env
``` ```
3. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: 3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values.
4. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following:
```shell ```shell
echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
@@ -62,14 +63,15 @@ echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.
sudo sysctl --system sudo sysctl --system
``` ```
4. Configure container environment variables. Below is the list of variables used during deployment: 5. Configure container environment variables. Below is the list of variables used during deployment:
- `MAIL_DOMAIN` The domain name of the future server. (required) - `MAIL_DOMAIN` The domain name of the future server. (required)
- `DEBUG_COMMANDS_ENABLED` Run debug commands before installation. (default: `false`) - `DEBUG_COMMANDS_ENABLED` Run debug commands before installation. (default: `false`)
- `FORCE_REINIT_INI_FILE` Recreate the ini configuration file on startup. (default: `false`) - `FORCE_REINIT_INI_FILE` Recreate the ini configuration file on startup. (default: `false`)
- `USE_FOREIGN_CERT_MANAGER` Use a third-party certificate manager. (default: `false`) - `USE_FOREIGN_CERT_MANAGER` Use a third-party certificate manager. (default: `false`)
- `RECREATE_VENV` - Recreate the virtual environment (venv). If set to `true`, the environment will be recreated when the container starts, which will increase the startup time of the service but can help avoid certain errors. (default: `false`)
- `INI_FILE` Path to the ini configuration file. (default: `./chatmail.ini`) - `INI_FILE` Path to the ini configuration file. (default: `./chatmail.ini`)
- `PATH_TO_SSL_CONTAINER` Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `PATH_TO_SSL` Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`)
- `ENABLE_CERTS_MONITORING` Enable certificate monitoring if `USE_FOREIGN_CERT_MANAGER=true`. If certificates change, services will be automatically restarted. (default: `false`) - `ENABLE_CERTS_MONITORING` Enable certificate monitoring if `USE_FOREIGN_CERT_MANAGER=true`. If certificates change, services will be automatically restarted. (default: `false`)
- `CERTS_MONITORING_TIMEOUT` Interval in seconds to check if certificates have changed. (default: `'60'`) - `CERTS_MONITORING_TIMEOUT` Interval in seconds to check if certificates have changed. (default: `'60'`)
@@ -79,113 +81,12 @@ Mandatory variables for deployment via Docker:
- `CHANGE_KERNEL_SETTINGS` Change kernel settings (`fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`) on startup. Changing kernel settings inside the container is not possible! (default: `False`) - `CHANGE_KERNEL_SETTINGS` Change kernel settings (`fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`) on startup. Changing kernel settings inside the container is not possible! (default: `False`)
5. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values.
6. Build the Docker image: 6. Build the Docker image:
```shell ```shell
docker compose build chatmail docker compose build chatmail
``` ```
<details>
<summary>Additional steps for configuring with traefik</summary>
> [!note]
> If you are using the default installation without traefik skip these steps and go to step 7 (running docker compose).
Before starting traefik, configuration files must be prepared; otherwise, it will not start correctly.
First, run these commands in the console, replacing their values with the correct ones:
```shell
export YOUR_EMAIL=your_email@gmail.com
mkdir -p "./data/traefik"
cd "./data/traefik"
```
1. Create a traefik configuration file:
```shell
cat > config.yaml << EOF
log:
level: TRACE
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
permanent: true
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /dynamic/conf
watch: true
serverstransport:
insecureskipverify: true
certificatesResolvers:
letsEncrypt:
acme:
email: $YOUR_EMAIL
storage: /acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlschallenge: true
httpChallenge:
entryPoint: web
EOF
```
2. Create a post-hook script:
```shell
cat > post-hook.sh << 'EOF'
CERTS_DIR=${CERTS_DIR:-"/data/letsencrypt/certs"}
for dir in "$CERTS_DIR"/*/; do
cd "$dir"
if [ -f "certificate.crt" ]; then
ln -sf certificate.crt fullchain
fi
if [ -f "privatekey.key" ]; then
ln -sf privatekey.key privkey
fi
cd -
done
EOF
```
3. Create the `acme.json` file:
```shell
touch acme.json
sudo chown 0:0 ./acme.json # required
sudo chmod 600 ./acme.json # required
```
4. Create insecure config:
```shell
mkdir dynamic-configs
cat > ./dynamic-configs/insecure.yaml << 'EOF'
http:
serversTransports:
insecure:
insecureSkipVerify: true
EOF
cd ../..
```
</details>
7. Start docker compose and wait for the installation to finish: 7. Start docker compose and wait for the installation to finish:
```shell ```shell
@@ -195,6 +96,11 @@ docker compose logs -f chatmail # view container logs, press CTRL+C to exit
8. After installation is complete, you can open `https://<your_domain_name>` in your browser. 8. After installation is complete, you can open `https://<your_domain_name>` in your browser.
9. To send messages to other chatmail relays,
you need to set additional DNS records.
Run `docker exec chatmail scripts/cmdeploy.sh dns --ssh-host localhost`
to see recommended DNS records and check whether they are correct.
## Using custom files ## Using custom files
When using Docker, you can apply modified configuration files to make the installation more personalized. This is usually needed for the `www/src` section so that the Chatmail landing page is customized to your taste, but it can be used for any other cases as well. When using Docker, you can apply modified configuration files to make the installation more personalized. This is usually needed for the `www/src` section so that the Chatmail landing page is customized to your taste, but it can be used for any other cases as well.
@@ -269,10 +175,10 @@ set -eo pipefail
export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}"
export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}"
export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}"
calculate_hash() { calculate_hash() {
find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}'
} }
monitor_certificates() { monitor_certificates() {
@@ -288,8 +194,8 @@ monitor_certificates() {
current_hash=$(calculate_hash) current_hash=$(calculate_hash)
if [[ "$current_hash" != "$previous_hash" ]]; then if [[ "$current_hash" != "$previous_hash" ]]; then
# TODO: add an option to restart at a specific time interval # TODO: add an option to restart at a specific time interval
echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services."
systemctl restart nginx.service systemctl reload nginx.service
systemctl reload dovecot.service systemctl reload dovecot.service
systemctl reload postfix.service systemctl reload postfix.service
previous_hash=$current_hash previous_hash=$current_hash

View File

@@ -1,5 +1,4 @@
# Известные проблемы и ограничения # Известные проблемы и ограничения
- Установка с помощью acmetool (`docker-compose-default.yaml`) может НЕ работать. В таком случае используйте установку через traefik (`docker-compose-traefik.yaml`). Лично у меня при тестах ошибка была `could not install DNS challenge, no hooks succeeded;`, которую исправить не удалось.
- Chatmail будет переустановлен при каждом запуске контейнера (при первом - долго, при последующих быстрее). Так устроен изначальный установщик, потому что он не был заточен под docker. В конце документации [представлено](#фиксирование-версии-chatmail) возможное решение - Chatmail будет переустановлен при каждом запуске контейнера (при первом - долго, при последующих быстрее). Так устроен изначальный установщик, потому что он не был заточен под docker. В конце документации [представлено](#фиксирование-версии-chatmail) возможное решение
- Требуется настроенный в системе cgroups v2. Работа с cgroups v1 не тестировалась. - Требуется настроенный в системе cgroups v2. Работа с cgroups v1 не тестировалась.
- Да, понятно дело что systemd использовать в контейнере костыль и надо это всё разнести на несколько сервисов, но это MVP и в первом приближении оказалось сделать проще так, чем переписывать всю систему развертывания. - Да, понятно дело что systemd использовать в контейнере костыль и надо это всё разнести на несколько сервисов, но это MVP и в первом приближении оказалось сделать проще так, чем переписывать всю систему развертывания.
@@ -43,25 +42,28 @@ cp ./docker/docker-compose-default.yaml docker-compose.yaml
# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml # cp ./docker/docker-compose-traefik.yaml docker-compose.yaml
``` ```
2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используятся в `docker-compose.yaml`. Нужен только для установки совместно с traefik, если используется default - можно пропустить 2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используятся в `docker-compose.yaml`.
```shell ```shell
cp ./docker/example.env .env cp ./docker/example.env .env
``` ```
3. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: 3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения.
4. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее:
```shell ```shell
echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
sudo sysctl --system sudo sysctl --system
``` ```
4. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании. 5. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании.
- `MAIL_DOMAIN` - Доменное имя будущего сервера. (required) - `MAIL_DOMAIN` - Доменное имя будущего сервера. (required)
- `DEBUG_COMMANDS_ENABLED` - Выполнить debug команды перед установкой. (default: `false`) - `DEBUG_COMMANDS_ENABLED` - Выполнить debug команды перед установкой. (default: `false`)
- `FORCE_REINIT_INI_FILE` - Пересоздавать ini файл конфигурации при запуске. (default: `false`) - `FORCE_REINIT_INI_FILE` - Пересоздавать ini файл конфигурации при запуске. (default: `false`)
- `USE_FOREIGN_CERT_MANAGER` - Использовать сторонний менеджер сертификатов. (default: `false`) - `USE_FOREIGN_CERT_MANAGER` - Использовать сторонний менеджер сертификатов. (default: `false`)
- `RECREATE_VENV` - Пересоздать виртуальное окружение (venv). Если выставлено `true`, то окружение будет пересоздано при запуске контейнера, из-за чего включение сервиса займет больше времени, но поможет избежать ряда ошибок. (default: `false`)
- `INI_FILE` - путь к ini файлу конфигурации. (default: `./chatmail.ini`) - `INI_FILE` - путь к ini файлу конфигурации. (default: `./chatmail.ini`)
- `PATH_TO_SSL_CONTAINER` - Путь где располагаются сертификаты. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `PATH_TO_SSL` - Путь где располагаются сертификаты. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`)
- `ENABLE_CERTS_MONITORING` - Включить мониторинг сертификатов, если `USE_FOREIGN_CERT_MANAGER=true`. Если сертфикаты изменятся сервисы будут автоматически перезапущены. (default: `false`) - `ENABLE_CERTS_MONITORING` - Включить мониторинг сертификатов, если `USE_FOREIGN_CERT_MANAGER=true`. Если сертфикаты изменятся сервисы будут автоматически перезапущены. (default: `false`)
- `CERTS_MONITORING_TIMEOUT` - Раз во сколько секунд проверять что изменились сертификаты. (default: `'60'`) - `CERTS_MONITORING_TIMEOUT` - Раз во сколько секунд проверять что изменились сертификаты. (default: `'60'`)
@@ -70,108 +72,11 @@ sudo sysctl --system
Ниже перечислены переменные, которые обязательны быть выставлены при развертывании через docker: Ниже перечислены переменные, которые обязательны быть выставлены при развертывании через docker:
- `CHANGE_KERNEL_SETTINGS` - Менять настройки ядра (`fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`) при запуске. При запуске в контейнере смена настроек ядра не может быть выполнена! (default: `False`) - `CHANGE_KERNEL_SETTINGS` - Менять настройки ядра (`fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`) при запуске. При запуске в контейнере смена настроек ядра не может быть выполнена! (default: `False`)
5. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения.
6. Собрать docker образ 6. Собрать docker образ
```shell ```shell
docker compose build chatmail docker compose build chatmail
``` ```
<details>
<summary>Дополнительные шаги для конфигурации работы с traefik</summary>
> [!note]
> Если вы используете default установку, без использования traefik - пропустите эти шаги и переходите к шагу 7 (запуск docker compose)
Перед запуском traefik необходимо подготовить файлы конфигурации, иначе он запустится некорректно.
Сначала выполните эти команды в консоли, заменив значения в них на корректные.
```shell
export YOUR_EMAIL=your_email@gmail.com
mkdir -p "./data/traefik"
cd "./data/traefik"
```
1. Создать файл конфигурации traefik
```shell
cat > config.yaml << EOF
log:
level: TRACE
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
permanent: true
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /dynamic/conf
watch: true
serverstransport:
insecureskipverify: true
certificatesResolvers:
letsEncrypt:
acme:
email: $YOUR_EMAIL
storage: /acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlschallenge: true
httpChallenge:
entryPoint: web
EOF
```
2. Создать post-hook скрипт
```shell
cat > post-hook.sh << 'EOF'
CERTS_DIR=${CERTS_DIR:-"/data/letsencrypt/certs"}
for dir in "$CERTS_DIR"/*/; do
cd "$dir"
if [ -f "certificate.crt" ]; then
ln -sf certificate.crt fullchain
fi
if [ -f "privatekey.key" ]; then
ln -sf privatekey.key privkey
fi
cd -
done
EOF
```
3. Создать `acme.json` файл
```shell
touch acme.json
sudo chown 0:0 ./acme.json # это обязательно
sudo chmod 600 ./acme.json # это обязательно
```
4. Создать insecure config
```shell
mkdir dynamic-configs
cat > ./dynamic-configs/insecure.yaml << 'EOF'
http:
serversTransports:
insecure:
insecureSkipVerify: true
EOF
cd ../..
```
</details>
7. Запустить docker compose и дождаться завершения установки 7. Запустить docker compose и дождаться завершения установки
```shell ```shell
docker compose up -d # запуск сервиса docker compose up -d # запуск сервиса
@@ -244,10 +149,10 @@ set -eo pipefail
export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}"
export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}"
export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}"
calculate_hash() { calculate_hash() {
find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}'
} }
monitor_certificates() { monitor_certificates() {
@@ -263,8 +168,8 @@ monitor_certificates() {
current_hash=$(calculate_hash) current_hash=$(calculate_hash)
if [[ "$current_hash" != "$previous_hash" ]]; then if [[ "$current_hash" != "$previous_hash" ]]; then
# TODO: add an option to restart at a specific time interval # TODO: add an option to restart at a specific time interval
echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services."
systemctl restart nginx.service systemctl reload nginx.service
systemctl reload dovecot.service systemctl reload dovecot.service
systemctl reload postfix.service systemctl reload postfix.service
previous_hash=$current_hash previous_hash=$current_hash

33
traefik/config.yaml Normal file
View File

@@ -0,0 +1,33 @@
log:
level: TRACE
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
permanent: true
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /dynamic/conf
watch: true
serverstransport:
insecureskipverify: true
certificatesResolvers:
letsEncrypt:
acme:
storage: /acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlschallenge: true
httpChallenge:
entryPoint: web

View File

@@ -0,0 +1,4 @@
http:
serversTransports:
insecure:
insecureSkipVerify: true

15
traefik/post-hook.sh Executable file
View File

@@ -0,0 +1,15 @@
CERTS_DIR=${CERTS_DIR:-"/data/letsencrypt/certs"}
echo "CERTS_DIR: $CERTS_DIR"
for dir in "$CERTS_DIR"/*/; do
echo "Processing: $dir"
cd "$dir"
if [ -f "certificate.crt" ]; then
ln -sf certificate.crt fullchain
fi
if [ -f "privatekey.key" ]; then
ln -sf privatekey.key privkey
fi
cd -
done

View File

@@ -1,8 +1,7 @@
<img class="banner" src="collage-top.png"/> <img class="banner" src="collage-top.png"/>
/// tab | 🇬🇧 English ## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
{% if config.mail_domain != "nine.testrun.org" %} {% if config.mail_domain != "nine.testrun.org" %}
Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :) Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :)
@@ -24,34 +23,7 @@ you can also **scan this QR code** with Delta Chat:
🐣 **Choose** your Avatar and Name 🐣 **Choose** your Avatar and Name
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee) 💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
///
/// tab | 🇷🇺 Русский
## Уважаемые пользователи и новички [Delta Chat](https://get.delta.chat)...
{% if config.mail_domain != "nine.testrun.org" %} {% if config.mail_domain != "nine.testrun.org" %}
Добро пожаловать в мир мгновенного, совместимого и [конфиденциального](privacy.html) обмена сообщениями :)
{% else %}
Вы находитесь на сервере по умолчанию ({{ config.mail_domain }})
для пользователей Delta Chat. Подробную информацию о том, как он избегает хранения личной информации,
см. в нашей [политике конфиденциальности](privacy.html).
{% endif %}
<a class="cta-button" href="DCACCOUNT:https://{{ config.mail_domain }}/new">Создать чат-профиль на {{config.mail_domain}}</a>
Если вы открыли эту страницу на устройстве,
где нет приложения Delta Chat, вы можете
**отсканировать этот QR-код** с помощью Delta Chat:
<a href="DCACCOUNT:https://{{ config.mail_domain }}/new">
<img width=300 style="float: none;" src="qr-chatmail-invite-{{config.mail_domain}}.png" /></a>
🐣 **Выберите** аватар и имя
💬 **Начните** чат с любыми контактами Delta Chat через [QR-приглашения](https://delta.chat/ru/help#howtoe2ee)
///
{% if config.is_development_instance == True %}
<div class="experimental">Note: this is only a temporary development chatmail service</div> <div class="experimental">Note: this is only a temporary development chatmail service</div>
{% endif %} {% endif %}

View File

@@ -1,6 +1,3 @@
<img class="banner" src="collage-info.png"/>
/// tab | 🇬🇧 English
## More information ## More information
@@ -44,47 +41,3 @@ This chatmail provider is run by a small voluntary group of devs and sysadmins,
who [publically develop chatmail provider setups](https://github.com/deltachat/chatmail). who [publically develop chatmail provider setups](https://github.com/deltachat/chatmail).
Chatmail setups aim to be very low-maintenance, resource efficient and Chatmail setups aim to be very low-maintenance, resource efficient and
interoperable with any other standards-compliant e-mail service. interoperable with any other standards-compliant e-mail service.
///
/// tab | 🇷🇺 Русский
## Дополнительная информация
{{ config.mail_domain }} предоставляет малозатратный, ресурсосберегающий и совместимый с другими системами почтовый сервис для всех. За `chatmail` фактически скрывается
обычный почтовый адрес, как и любой другой, но оптимизированный
для использования в чатах, особенно DeltaChat.
### Ограничения по скорости и хранению
* Незашифрованные сообщения блокируются для получателей вне
{{config.mail_domain}}, но добавление контакта через [QR-коды приглашения](https://delta.chat/en/help#howtoe2ee)
позволяет свободно обмениваться сообщениями между с ним.
* Вы можете отправлять до {{ config.max_user_send_per_minute }} сообщений в минуту.
- Вы можете хранить до [{{ config.max_mailbox_size }} сообщений на сервере](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server).
* Сообщения в любом случае будут удалены с сервера через {{ config.delete_mails_after }} дней после поступления на сервер.
Или раньше, если хранилище превышает допустимый объем.
### <a name="account-deletion"></a> Удаление аккаунта
Если вы удалите профиль {{ config.mail_domain }} через приложение Delta Chat,
соответствующая учетная запись на сервере и все связанные с ней данные
будут автоматически удалены через {{ config.delete_inactive_users_after }} дней.
Если вы используете несколько устройств,
вам необходимо удалить профиль чата на каждом из них,
чтобы все данные аккаунта были удалены с сервера.
Если у вас есть дополнительные вопросы или запросы по поводу удаления аккаунта,
пожалуйста, отправьте сообщение со своей учетной записи на {{ config.privacy_mail }}.
### Кто операторы? Какое ПО используется?
Этот chatmail провайдер управляется небольшой группой добровольцев — разработчиков и системных администраторов,
которые [публично разрабатывают инфраструктуру chatmail провайдеров](https://github.com/deltachat/chatmail).
Chatmail стремится быть максимально простыми в обслуживании, ресурсосберегающими и
совместимыми с любым другим почтовым сервисом, соответствующим стандартам.
///

View File

@@ -84,57 +84,3 @@ code {
color: white !important; color: white !important;
font-weight: bold; font-weight: bold;
} }
.tabbed-set {
position: relative;
display: flex;
flex-wrap: wrap;
margin: 1em 0;
border-radius: 0.1rem;
}
.tabbed-set > input {
display: none;
}
.tabbed-set label {
width: auto;
padding: 0.9375em 1.25em 0.78125em;
font-weight: 700;
font-size: 0.84em;
white-space: nowrap;
border-bottom: 0.15rem solid transparent;
border-top-left-radius: 0.1rem;
border-top-right-radius: 0.1rem;
cursor: pointer;
transition: background-color 250ms, color 250ms;
}
.tabbed-set .tabbed-content {
width: 100%;
display: none;
box-shadow: 0 -.05rem #ddd;
}
.tabbed-set input {
position: absolute;
opacity: 0;
}
.tabbed-set input:checked:nth-child(n+1) + label {
color: red;
border-color: red;
}
@media screen {
.tabbed-set input:nth-child(n+1):checked + label + .tabbed-content {
order: 99;
display: block;
}
}
@media print {
.tabbed-content {
display: contents;
}
}

View File

@@ -1,6 +1,3 @@
<img class="banner" src="collage-privacy.png"/>
/// tab | 🇬🇧 English
# Privacy Policy for {{ config.mail_domain }} # Privacy Policy for {{ config.mail_domain }}
@@ -270,199 +267,5 @@ as of *October 2024*.
Due to the further development of our service and offers Due to the further development of our service and offers
or due to changed legal or official requirements, or due to changed legal or official requirements,
it may become necessary to revise this data protection declaration from time to time. it may become necessary to revise this data protection declaration from time to time.
///
/// tab | 🇷🇺 Русский
# Политика конфиденциальности для {{ config.mail_domain }}
{% if config.mail_domain == "nine.testrun.org" %}
Добро пожаловать на `{{config.mail_domain}}` — это основной сервер Chatmail для новых пользователей Delta Chat.
Он поддерживается небольшой командой системных администраторов на добровольной основе.
Альтернативные сервера вы можете найти [здесь](https://delta.chat/en/chatmail).
{% endif %}
## Кратко: Личные данные не запрашиваются и не собираются
Этот сервер Chatmail не запрашивает и не сохраняет личную информацию.
Серверы Chatmail существуют исключительно для надёжной передачи (временного хранения и доставки) зашифрованных сообщений между устройствами пользователей, использующих мессенджер Delta Chat.
Технически, Chatmail-сервер можно представить как «маршрутизатор сообщений» с поддержкой сквозного шифрования в масштабе интернета.
В отличие от классических почтовых сервисов (например, Gmail),
Chatmail-серверы не запрашивают личные данные и не хранят письма постоянно.
Они ближе по устройству к серверам Signal,
однако не используют номера телефонов и могут безопасно и автоматически взаимодействовать как с другими Chatmail-серверами, так и с обычной электронной почтой.
Отличия от традиционных почтовых серверов:
- безусловное удаление сообщений через {{ config.delete_mails_after }} дней;
- невозможность отправки незашифрованных сообщений;
- отсутствие хранения IP-адресов;
- IP-адреса не обрабатываются в связке с адресами электронной почты.
Из-за отсутствия обработки персональных данных
данный сервер, возможно, формально не обязан иметь политику конфиденциальности.
Тем не менее, ниже приведена юридическая информация
для удобства специалистов по защите данных и юристов, изучающих работу Chatmail.
---
## 1. Название и контактная информация
Ответственный за обработку ваших персональных данных:
```
{{ config.privacy_postal }}
```
Эл. почта: {{ config.privacy_mail }}
Назначен ответственный по защите данных:
```
{{ config.privacy_pdo }}
```
---
## 2. Обработка при использовании чата и электронной почты
Мы предоставляем сервисы, оптимизированные для работы с приложением [Delta Chat](https://delta.chat),
и обрабатываем только те данные, которые необходимы для настройки и технической реализации доставки сообщений.
Цель обработки — дать пользователям возможность читать, писать, управлять, удалять, отправлять и получать сообщения.
Для этого мы используем серверное ПО, обеспечивающее передачу сообщений.
Обрабатываются следующие данные:
- Исходящие и входящие сообщения (SMTP) временно хранятся до их доставки получателю;
- Сообщения доступны получателю через IMAP до их удаления пользователем или по истечении установленного срока
(*обычно 48 недель*);
- Протоколы IMAP и SMTP защищены паролем, уникальным для каждого аккаунта;
- Пользователи могут самостоятельно просматривать или удалять сообщения через любой стандартный IMAP-клиент;
- Также возможно подключение к «службе передачи в реальном времени»,
которая устанавливает P2P-соединение между устройствами и позволяет отправлять временные сообщения,
которые *никогда* не сохраняются на сервере — даже в зашифрованном виде.
### 2.1 Создание аккаунта
Аккаунт создаётся одним из двух способов:
- с помощью QR-кода приглашения,
отсканированного через приложение Delta Chat;
- автоматически, при создании и регистрации аккаунта в {{ config.mail_domain }} через приложение Delta Chat.
В любом случае, обрабатывается только созданный адрес электронной почты.
Номера телефонов, другие адреса электронной почты или любые другие идентификаторы не требуются.
Правовое основание для обработки —
статья 6 (1) пункт b Общего регламента по защите данных (GDPR),
так как вы заключаете пользовательский договор, пользуясь нашим сервисом.
### 2.2 Обработка почтовых сообщений
Кроме того, мы обрабатываем данные,
необходимые для обеспечения стабильной работы инфраструктуры сервера,
доставки сообщений и предотвращения злоупотреблений.
- Поэтому может потребоваться обработка содержимого и/или метаданных
(например, заголовков писем и технической информации SMTP) во время передачи;
- Мы храним логи передаваемых сообщений ограниченное время —
они используются для устранения проблем с доставкой и ошибок ПО.
Также мы вводим ограничения для защиты системы от перегрузок:
- ограничения скорости (rate limits),
- лимиты на объём хранения,
- ограничения на размер сообщений,
- любые другие меры, необходимые для стабильной работы сервера и предотвращения злоупотреблений.
Обработка вышеуказанных данных необходима для предоставления сервиса.
Правовое основание — статья 6 (1) пункт b GDPR.
Обработка данных в целях безопасности и предотвращения злоупотреблений основана на статье 6 (1) пункт f GDPR,
и соответствует нашим законным интересам.
Мы не используем собранные данные для определения вашей личности.
---
## 3. Обработка при посещении сайта
При посещении нашего сайта браузер вашего устройства
автоматически передаёт определённую информацию на сервер,
где она временно сохраняется в так называемых лог-файлах.
Эти данные автоматически удаляются (обычно через *7 дней*).
Среди собираемых данных:
- тип используемого браузера,
- операционная система,
- дата и время доступа,
- страна и IP-адрес,
- запрашиваемый файл или ресурс,
- объём переданных данных,
- статус доступа (успешно, ошибка и т.п.),
- страница, с которой был сделан запрос.
Хостинг нашего сайта осуществляется внешним провайдером.
Личные данные, собираемые на сайте, хранятся на его серверах.
Провайдер обрабатывает данные строго по нашим инструкциям,
в пределах заключённого договора на обработку данных (ст. 28 GDPR).
Цели обработки:
- обеспечение стабильного подключения к сайту;
- удобство использования сайта;
- контроль безопасности и стабильности системы;
- административные цели.
Правовое основание — статья 6 (1) пункт f GDPR.
Собранные данные не используются для установления вашей личности.
---
## 4. Передача данных
Мы не сохраняем личные данные,
но письма, ожидающие доставки, могут содержать личную информацию.
Такие данные не передаются третьим лицам, за исключением следующих случаев:
a) при наличии вашего явного согласия (ст. 6 п.1 п. a GDPR);
b) если передача необходима для защиты прав, интересов или правовой позиции (ст. 6 п.1 п. f GDPR);
c) если это требуется по закону (ст. 6 п.1 п. c GDPR);
d) если это необходимо для исполнения договора с вами (ст. 6 п.1 п. b GDPR);
e) если обработка осуществляется сервис-провайдером по нашему поручению,
с которым заключён договор (ст. 28 GDPR),
предусматривающий меры безопасности и контроль с нашей стороны.
---
## 5. Права субъектов данных
Ваши права закреплены в статьях 1223 GDPR.
Так как сервер не хранит персональные данные — даже в зашифрованном виде —
предоставление информации или подача возражений не требуются.
Удаление данных можно выполнить напрямую через приложение Delta Chat.
Если у вас есть вопросы или жалобы, напишите нам:
{{ config.privacy_mail }}
Также вы можете обратиться в надзорный орган по месту вашего проживания,
работы или к органу, ответственному за нашу деятельность:
`{{ config.privacy_supervisor }}`.
---
## 6. Актуальность политики конфиденциальности
Настоящая политика действует с *октября 2024 года*.
В случае изменений в услугах или законодательства
она может быть обновлена.
///