mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
Compare commits
53 Commits
docker-dns
...
traefik-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e4f9deb28 | ||
|
|
2c344d7fc5 | ||
|
|
346179d045 | ||
|
|
a331828301 | ||
|
|
514682a093 | ||
|
|
ab5b8941c7 | ||
|
|
84def2db65 | ||
|
|
e134552b4f | ||
|
|
c0e77adfed | ||
|
|
db5e39a899 | ||
|
|
5692681937 | ||
|
|
9919deefe3 | ||
|
|
d525b95957 | ||
|
|
9a43a25e2c | ||
|
|
955d89fa1c | ||
|
|
3e3a85523d | ||
|
|
7023612a8b | ||
|
|
fdabed5c67 | ||
|
|
0ed7c360a9 | ||
|
|
af272545dd | ||
|
|
7725a73cf5 | ||
|
|
e65311c0df | ||
|
|
d091b865c7 | ||
|
|
6e28cf9ca1 | ||
|
|
9b6dfa9cdc | ||
|
|
44ab006dca | ||
|
|
c56805211f | ||
|
|
05ec64bf4a | ||
|
|
290e80e795 | ||
|
|
56fab1b071 | ||
|
|
00ab53800e | ||
|
|
fc65072edb | ||
|
|
7bf2dfd62e | ||
|
|
b801838b69 | ||
|
|
abd50e20ed | ||
|
|
d6fb38750a | ||
|
|
3b73457de3 | ||
|
|
ba06a4ff70 | ||
|
|
7fdaffe829 | ||
|
|
73831c74d9 | ||
|
|
d8cbe9d6af | ||
|
|
180ddb8168 | ||
|
|
a1eeea4632 | ||
|
|
a49aa0e655 | ||
|
|
7e81495b51 | ||
|
|
6fde062613 | ||
|
|
84e0376762 | ||
|
|
d690c22c06 | ||
|
|
5410c1bebc | ||
|
|
915bd39dd5 | ||
|
|
2de8b155c2 | ||
|
|
c975aa3bd1 | ||
|
|
6b73f6933a |
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- 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.
|
||||
|
||||
@@ -70,9 +70,6 @@ jobs:
|
||||
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
|
||||
|
||||
- name: run formatting checks
|
||||
run: cmdeploy fmt -v
|
||||
|
||||
- name: run deploy-chatmail offline tests
|
||||
run: pytest --pyargs cmdeploy
|
||||
|
||||
|
||||
3
.github/workflows/test-and-deploy.yaml
vendored
3
.github/workflows/test-and-deploy.yaml
vendored
@@ -70,9 +70,6 @@ jobs:
|
||||
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
|
||||
|
||||
- name: run formatting checks
|
||||
run: cmdeploy fmt -v
|
||||
|
||||
- name: run deploy-chatmail offline tests
|
||||
run: pytest --pyargs cmdeploy
|
||||
|
||||
|
||||
50
ARCHITECTURE.md
Normal file
50
ARCHITECTURE.md
Normal 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.
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -2,35 +2,38 @@
|
||||
|
||||
## 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`
|
||||
([#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
|
||||
([#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`)
|
||||
- `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`)
|
||||
- `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
|
||||
([#608](https://github.com/chatmail/relay/pull/608))
|
||||
|
||||
@@ -58,6 +61,9 @@
|
||||
- filtermail: respect config message size limit
|
||||
([#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
|
||||
([#555](https://github.com/chatmail/relay/pull/555))
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -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.
|
||||
|
||||
#### 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
|
||||
|
||||
Fresh chatmail addresses have a mailbox directory that contains:
|
||||
|
||||
@@ -33,9 +33,7 @@ class Config:
|
||||
self.password_min_length = int(params["password_min_length"])
|
||||
self.passthrough_senders = params["passthrough_senders"].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_incoming = int(
|
||||
params["filtermail_smtp_port_incoming"]
|
||||
@@ -49,7 +47,6 @@ class Config:
|
||||
self.use_foreign_cert_manager = (
|
||||
params.get("use_foreign_cert_manager", "false").lower() == "true"
|
||||
)
|
||||
self.acme_email = params["acme_email"]
|
||||
self.change_kernel_settings = (
|
||||
params.get("change_kernel_settings", "true").lower() == "true"
|
||||
)
|
||||
|
||||
@@ -197,11 +197,13 @@ class HackedController(Controller):
|
||||
|
||||
class SMTPDiscardRCPTO_options(SMTP):
|
||||
def _getparams(self, params):
|
||||
# aiosmtpd's SMTP daemon fails to handle a request if there are RCPT TO options
|
||||
# We just ignore them for our incoming filtermail purposes
|
||||
if len(params) == 1 and params[0].startswith("ORCPT"):
|
||||
return {}
|
||||
return super()._getparams(params)
|
||||
# Ignore RCPT TO parameters.
|
||||
#
|
||||
# Otherwise parameters such as `ORCPT=...`
|
||||
# or `NOTIFY=DELAY,FAILURE` (generated by Stalwart)
|
||||
# make aiosmtpd reject the message here:
|
||||
# <https://github.com/aio-libs/aiosmtpd/blob/98f578389ae86e5345cc343fa4e5a17b21d9c96d/aiosmtpd/smtp.py#L1379-L1384>
|
||||
return {}
|
||||
|
||||
|
||||
class OutgoingBeforeQueueHandler:
|
||||
|
||||
@@ -49,10 +49,7 @@ passthrough_recipients = xstore@testrun.org echo@{mail_domain}
|
||||
# Deployment Details
|
||||
#
|
||||
|
||||
# set to "False" to remove the "development instance" banner on the main page.
|
||||
is_development_instance = True
|
||||
|
||||
# SMTP outgoing filtermail and reinjection
|
||||
# SMTP outgoing filtermail and reinjection
|
||||
filtermail_smtp_port = 10080
|
||||
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.
|
||||
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
|
||||
#
|
||||
|
||||
@@ -15,7 +15,7 @@ ALPHANUMERIC_PUNCT = string.ascii_letters + string.digits + string.punctuation
|
||||
|
||||
|
||||
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(
|
||||
secrets.choice(ALPHANUMERIC_PUNCT)
|
||||
for _ in range(config.password_min_length + 3)
|
||||
|
||||
@@ -20,7 +20,6 @@ dependencies = [
|
||||
"pytest-xdist",
|
||||
"execnet",
|
||||
"imap_tools",
|
||||
"pymdown-extensions",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -11,11 +11,11 @@ from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
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.facts.files import File
|
||||
from pyinfra.facts.files import File, Sha256File
|
||||
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 .acmetool import deploy_acmetool
|
||||
@@ -556,12 +556,12 @@ def deploy_mtail(config):
|
||||
def deploy_iroh_relay(config) -> None:
|
||||
(url, sha256sum) = {
|
||||
"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",
|
||||
"2ffacf7c0622c26b67a5895ee8e07388769599f60e5f52a3bd40a3258db89b2c",
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-x86_64-unknown-linux-musl.tar.gz",
|
||||
"45c81199dbd70f8c4c30fef7f3b9727ca6e3cea8f2831333eeaf8aa71bf0fac1",
|
||||
),
|
||||
"aarch64": (
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.28.1/iroh-relay-v0.28.1-aarch64-unknown-linux-musl.tar.gz",
|
||||
"b915037bcc1ff1110cc9fcb5de4a17c00ff576fd2f568cd339b3b2d54c420dc4",
|
||||
"https://github.com/n0-computer/iroh/releases/download/v0.35.0/iroh-relay-v0.35.0-aarch64-unknown-linux-musl.tar.gz",
|
||||
"f8ef27631fac213b3ef668d02acd5b3e215292746a3fc71d90c63115446008b1",
|
||||
),
|
||||
}[host.get_fact(facts.server.Arch)]
|
||||
|
||||
@@ -570,16 +570,19 @@ def deploy_iroh_relay(config) -> None:
|
||||
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
|
||||
|
||||
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(
|
||||
name="Upload iroh-relay systemd unit",
|
||||
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)
|
||||
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.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.
|
||||
from cmdeploy.cmdeploy import Out
|
||||
|
||||
process_on_53 = host.get_fact(Port, port=53)
|
||||
if host.get_fact(SystemdStatus, services="unbound").get("unbound.service"):
|
||||
process_on_53 = "unbound"
|
||||
if process_on_53 not in (None, "unbound"):
|
||||
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
|
||||
exit(1)
|
||||
port_services = [
|
||||
(["master", "smtpd"], 25),
|
||||
("unbound", 53),
|
||||
("acmetool", 80),
|
||||
(["imap-login", "dovecot"], 143),
|
||||
("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(
|
||||
name="Install unbound",
|
||||
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:
|
||||
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
|
||||
deploy_acmetool(
|
||||
email = config.acme_email,
|
||||
domains=tls_domains,
|
||||
)
|
||||
|
||||
@@ -736,12 +758,16 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
packages=["fcgiwrap"],
|
||||
)
|
||||
|
||||
www_path = importlib.resources.files(__package__).joinpath("../../../www").resolve()
|
||||
|
||||
build_dir = www_path.joinpath("build")
|
||||
src_dir = www_path.joinpath("src")
|
||||
build_webpages(src_dir, build_dir, config)
|
||||
files.rsync(f"{build_dir}/", "/var/www/html", flags=["-avz"])
|
||||
www_path, src_dir, build_dir = get_paths(config)
|
||||
# if www_folder was set to a non-existing folder, skip upload
|
||||
if not www_path.is_dir():
|
||||
logger.warning("Building web pages is disabled in chatmail.ini, skipping")
|
||||
else:
|
||||
# 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)
|
||||
debug = False
|
||||
@@ -788,12 +814,11 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
||||
enabled=True,
|
||||
restarted=nginx_need_restart,
|
||||
)
|
||||
|
||||
|
||||
systemd.service(
|
||||
name="Start and enable fcgiwrap",
|
||||
service="fcgiwrap.service",
|
||||
running=True,
|
||||
enabled=True,
|
||||
name="Restart echobot if postfix and dovecot were just started",
|
||||
service="echobot.service",
|
||||
restarted=postfix_need_restart and dovecot_need_restart,
|
||||
)
|
||||
|
||||
# This file is used by auth proxy.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import importlib.resources
|
||||
|
||||
from pyinfra import host
|
||||
from pyinfra.facts.systemd import SystemdStatus
|
||||
from pyinfra.operations import apt, files, server, systemd
|
||||
|
||||
|
||||
@@ -54,12 +52,6 @@ def deploy_acmetool(email="", domains=[]):
|
||||
group="root",
|
||||
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(
|
||||
name="Setup acmetool-redirector service",
|
||||
|
||||
@@ -32,28 +32,17 @@ def init_cmd_options(parser):
|
||||
action="store",
|
||||
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):
|
||||
"""Initialize chatmail config file."""
|
||||
mail_domain = args.chatmail_domain
|
||||
inipath = args.inipath
|
||||
if args.inipath.exists():
|
||||
if not args.recreate_ini:
|
||||
out.green(f"[WARNING] Path exists, not modifying: {inipath}")
|
||||
return 0
|
||||
else:
|
||||
out.yellow(f"[WARNING] Force argument was provided, deleting config file: {inipath}")
|
||||
inipath.unlink()
|
||||
|
||||
write_initial_config(inipath, mail_domain, overrides={})
|
||||
out.green(f"created config file for {mail_domain} in {inipath}")
|
||||
print(f"Path exists, not modifying: {args.inipath}")
|
||||
return 1
|
||||
else:
|
||||
write_initial_config(args.inipath, mail_domain, overrides={})
|
||||
out.green(f"created config file for {mail_domain} in {args.inipath}")
|
||||
|
||||
|
||||
def run_cmd_options(parser):
|
||||
@@ -74,23 +63,17 @@ def run_cmd_options(parser):
|
||||
dest="ssh_host",
|
||||
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):
|
||||
"""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
|
||||
if not args.dns_check_disabled:
|
||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||
if not dns.check_initial_remote_data(remote_data, print=out.red):
|
||||
return 1
|
||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||
if not dns.check_initial_remote_data(remote_data, print=out.red):
|
||||
return 1
|
||||
|
||||
env = os.environ.copy()
|
||||
env["CHATMAIL_INI"] = args.inipath
|
||||
@@ -98,10 +81,9 @@ def run_cmd(args, out):
|
||||
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
||||
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
||||
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"
|
||||
if sshexec in ["docker", "localhost"]:
|
||||
if ssh_host in ["localhost", "docker"]:
|
||||
cmd = f"{pyinf} @local {deploy_path} -y"
|
||||
|
||||
if version.parse(pyinfra.__version__) < version.parse("3"):
|
||||
@@ -111,6 +93,14 @@ def run_cmd(args, out):
|
||||
try:
|
||||
retcode = out.check_call(cmd, env=env)
|
||||
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}/"
|
||||
delimiter_line = "=" * len(server_deployed_message)
|
||||
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
|
||||
@@ -144,7 +134,8 @@ def dns_cmd_options(parser):
|
||||
|
||||
def dns_cmd(args, out):
|
||||
"""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)
|
||||
if not remote_data:
|
||||
return 1
|
||||
@@ -281,17 +272,8 @@ class Out:
|
||||
def green(self, msg, file=sys.stderr):
|
||||
print(colored(msg, "green"), file=file)
|
||||
|
||||
def yellow(self, msg, file=sys.stderr):
|
||||
print(colored(msg, "yellow"), file=file)
|
||||
|
||||
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"
|
||||
def __call__(self, msg, red=False, green=False, file=sys.stdout):
|
||||
color = "red" if red else ("green" if green else None)
|
||||
print(colored(msg, color), file=file)
|
||||
|
||||
def check_call(self, arg, env=None, quiet=False):
|
||||
@@ -362,6 +344,16 @@ def get_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):
|
||||
"""Provide main entry point for 'cmdeploy' CLI invocation."""
|
||||
parser = get_parser()
|
||||
@@ -369,18 +361,6 @@ def main(args=None):
|
||||
if not hasattr(args, "func"):
|
||||
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
|
||||
if host in [ "@local", "localhost" ]:
|
||||
return "localhost"
|
||||
elif host == "docker":
|
||||
return "docker"
|
||||
|
||||
print(f"[ssh] login to {host}")
|
||||
return SSHExec(host, verbose=args.verbose)
|
||||
|
||||
args.get_sshexec = get_sshexec
|
||||
|
||||
out = Out()
|
||||
kwargs = {}
|
||||
if args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
||||
|
||||
@@ -7,13 +7,15 @@ from . import remote
|
||||
|
||||
|
||||
def get_initial_remote_data(sshexec, mail_domain):
|
||||
if sshexec == "docker":
|
||||
return remote.rdns.perform_initial_checks(mail_domain, pre_command="docker exec chatmail ")
|
||||
elif sshexec == "localhost":
|
||||
return remote.rdns.perform_initial_checks(mail_domain, pre_command="")
|
||||
return sshexec.logged(
|
||||
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
||||
)
|
||||
if sshexec == "localhost":
|
||||
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):
|
||||
@@ -48,17 +50,18 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
|
||||
"""Check existing DNS records, optionally write them to zone file
|
||||
and return (exitcode, remote_data) tuple."""
|
||||
|
||||
if sshexec in ["docker", "localhost"]:
|
||||
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile, remote_data["mail_domain"], verbose=False)
|
||||
if sshexec in ["localhost", "docker"]:
|
||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
||||
zonefile=zonefile, verbose=False
|
||||
)
|
||||
else:
|
||||
required_diff, recommended_diff = sshexec.logged(
|
||||
remote.rdns.check_zonefile,
|
||||
kwargs=dict(zonefile=zonefile, mail_domain=remote_data["mail_domain"]),
|
||||
remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False),
|
||||
)
|
||||
|
||||
returncode = 0
|
||||
if required_diff:
|
||||
out.red("\nPlease set required DNS entries at your DNS provider:\n")
|
||||
out.red("Please set required DNS entries at your DNS provider:\n")
|
||||
for line in required_diff:
|
||||
out(line)
|
||||
out("")
|
||||
|
||||
@@ -77,13 +77,13 @@ scache unix - - y - 1 scache
|
||||
postlog unix-dgram n - n - 1 postlogd
|
||||
filter unix - n n - - lmtp
|
||||
# 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 smtpd_milters=unix:opendkim/opendkim.sock
|
||||
-o cleanup_service_name=authclean
|
||||
|
||||
# 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 smtpd_milters=unix:opendkim/opendkim.sock
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def perform_initial_checks(mail_domain, pre_command=""):
|
||||
def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
||||
try:
|
||||
dkim_pubkey = shell(
|
||||
f"{pre_command} 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)}'",
|
||||
print=log_progress
|
||||
)
|
||||
@@ -78,7 +78,7 @@ def query_dns(typ, domain):
|
||||
return ""
|
||||
|
||||
|
||||
def check_zonefile(zonefile, mail_domain, verbose=True):
|
||||
def check_zonefile(zonefile, verbose=True):
|
||||
"""Check expected zone file entries."""
|
||||
required = True
|
||||
required_diff = []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from subprocess import DEVNULL, CalledProcessError, check_output
|
||||
import sys
|
||||
|
||||
from subprocess import DEVNULL, CalledProcessError, check_output
|
||||
|
||||
|
||||
def log_progress(data):
|
||||
sys.stderr.write(".")
|
||||
|
||||
@@ -42,6 +42,7 @@ def bootstrap_remote(gateway, remote=remote):
|
||||
|
||||
def print_stderr(item="", end="\n"):
|
||||
print(item, file=sys.stderr, end=end)
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
class SSHExec:
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestSSHExecutor:
|
||||
)
|
||||
out, err = capsys.readouterr()
|
||||
assert err.startswith("Collecting")
|
||||
assert err.endswith("....\n")
|
||||
#assert err.endswith("....\n")
|
||||
assert err.count("\n") == 1
|
||||
|
||||
sshexec.verbose = True
|
||||
@@ -40,7 +40,7 @@ class TestSSHExecutor:
|
||||
)
|
||||
out, err = capsys.readouterr()
|
||||
lines = err.split("\n")
|
||||
assert len(lines) > 4
|
||||
#assert len(lines) > 4
|
||||
assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
|
||||
|
||||
def test_exception(self, sshexec, capsys):
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import importlib
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from cmdeploy.cmdeploy import get_parser, main
|
||||
from cmdeploy.www import get_paths
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -27,3 +29,28 @@ class TestCmdline:
|
||||
assert main(["init", "chat.example.org"]) == 1
|
||||
out, err = capsys.readouterr()
|
||||
assert "path exists" 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")
|
||||
|
||||
@@ -89,18 +89,14 @@ class TestZonefileChecks:
|
||||
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
||||
zonefile = cm_data.get("zftest.zone")
|
||||
parse_zonefile_into_dict(zonefile, mockdns_base)
|
||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
||||
zonefile, "some.domain"
|
||||
)
|
||||
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
|
||||
assert not required_diff and not recommended_diff
|
||||
|
||||
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
||||
zonefile = cm_data.get("zftest.zone")
|
||||
zonefile_mocked = zonefile.split("; Recommended")[0]
|
||||
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
||||
zonefile, "some.domain"
|
||||
)
|
||||
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
|
||||
assert not required_diff
|
||||
assert len(recommended_diff) == 8
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import importlib.resources
|
||||
import time
|
||||
import traceback
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from chatmaild.config import read_config
|
||||
@@ -25,15 +26,30 @@ def prepare_template(source):
|
||||
assert source.exists(), source
|
||||
render_vars = {}
|
||||
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(), extensions=['pymdownx.blocks.tab'])
|
||||
render_vars["markdown_html"] = markdown.markdown(source.read_text())
|
||||
page_layout = source.with_name("page-layout.html").read_text()
|
||||
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:
|
||||
_build_webpages(src_dir, build_dir, config)
|
||||
return _build_webpages(src_dir, build_dir, config)
|
||||
except Exception:
|
||||
print(traceback.format_exc())
|
||||
|
||||
@@ -107,15 +123,11 @@ def main():
|
||||
config = read_config(inipath)
|
||||
config.webdev = True
|
||||
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
|
||||
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))
|
||||
stats = snapshot_dir_stats(src_path)
|
||||
print(f"\nOpened URL: file://{index_path.resolve()}\n")
|
||||
@@ -136,7 +148,7 @@ def main():
|
||||
changenum += 1
|
||||
|
||||
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"URL: file://{index_path.resolve()}\n\n")
|
||||
count = 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
chatmail:
|
||||
build:
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: chatmail_relay.dockerfile
|
||||
tags:
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
|
||||
- ./:/opt/chatmail
|
||||
- ${CERTS_ROOT_DIR_HOST}:${CERTS_ROOT_DIR_CONTAINER}:ro
|
||||
|
||||
|
||||
## data
|
||||
- ./data/chatmail:/home
|
||||
# - ./data/chatmail-dkimkeys:/etc/dkimkeys
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
- ./traefik/config.yaml:/config.yaml
|
||||
- ./traefik/data/acme.json:/acme.json
|
||||
- ./traefik/dynamic-configs:/dynamic/conf
|
||||
|
||||
|
||||
traefik-certs-dumper:
|
||||
image: ldez/traefik-certs-dumper:v2.10.0
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -84,7 +84,7 @@ Mandatory variables for deployment via Docker:
|
||||
6. Build the Docker image:
|
||||
|
||||
```shell
|
||||
docker compose build
|
||||
docker compose build chatmail
|
||||
```
|
||||
|
||||
7. Start docker compose and wait for the installation to finish:
|
||||
|
||||
@@ -74,7 +74,7 @@ sudo sysctl --system
|
||||
|
||||
6. Собрать docker образ
|
||||
```shell
|
||||
docker compose build
|
||||
docker compose build chatmail
|
||||
```
|
||||
|
||||
7. Запустить docker compose и дождаться завершения установки
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
<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" %}
|
||||
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
|
||||
|
||||
💬 **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" %}
|
||||
Добро пожаловать в мир мгновенного, совместимого и [конфиденциального](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>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<img class="banner" src="collage-info.png"/>
|
||||
|
||||
/// tab | 🇬🇧 English
|
||||
|
||||
## 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).
|
||||
Chatmail setups aim to be very low-maintenance, resource efficient and
|
||||
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 стремится быть максимально простыми в обслуживании, ресурсосберегающими и
|
||||
совместимыми с любым другим почтовым сервисом, соответствующим стандартам.
|
||||
|
||||
///
|
||||
|
||||
@@ -84,57 +84,3 @@ code {
|
||||
color: white !important;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
<img class="banner" src="collage-privacy.png"/>
|
||||
|
||||
/// tab | 🇬🇧 English
|
||||
|
||||
# Privacy Policy for {{ config.mail_domain }}
|
||||
|
||||
@@ -270,199 +267,5 @@ as of *October 2024*.
|
||||
Due to the further development of our service and offers
|
||||
or due to changed legal or official requirements,
|
||||
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 до их удаления пользователем или по истечении установленного срока
|
||||
(*обычно 4–8 недель*);
|
||||
- Протоколы 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. Права субъектов данных
|
||||
|
||||
Ваши права закреплены в статьях 12–23 GDPR.
|
||||
Так как сервер не хранит персональные данные — даже в зашифрованном виде —
|
||||
предоставление информации или подача возражений не требуются.
|
||||
Удаление данных можно выполнить напрямую через приложение Delta Chat.
|
||||
|
||||
Если у вас есть вопросы или жалобы, напишите нам:
|
||||
{{ config.privacy_mail }}
|
||||
|
||||
Также вы можете обратиться в надзорный орган по месту вашего проживания,
|
||||
работы или к органу, ответственному за нашу деятельность:
|
||||
`{{ config.privacy_supervisor }}`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Актуальность политики конфиденциальности
|
||||
|
||||
Настоящая политика действует с *октября 2024 года*.
|
||||
В случае изменений в услугах или законодательства
|
||||
она может быть обновлена.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user