mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 16:34:39 +00:00
Compare commits
36 Commits
ssh-host-6
...
markdown-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62f028bc67 | ||
|
|
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
|
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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
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
|
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
|
||||||
|
|
||||||
|
|||||||
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.
|
||||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
|
- 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))
|
||||||
|
|
||||||
|
- Add markdown tabs blocks for rendering multilingual pages.
|
||||||
|
Add russian language support to `index.md`, `privacy.md`, and `info.md`.
|
||||||
|
([#658](https://github.com/chatmail/relay/pull/658))
|
||||||
|
|
||||||
|
- Allow ports 143 and 993 to be used by `dovecot` process
|
||||||
|
([#639](https://github.com/chatmail/relay/pull/639))
|
||||||
|
|
||||||
|
## 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))
|
||||||
|
|
||||||
@@ -29,6 +53,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))
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -255,6 +255,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:
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ 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.languages = (params.get("languages", "EN").split())
|
||||||
|
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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ passthrough_recipients = xstore@testrun.org echo@{mail_domain}
|
|||||||
# Deployment Details
|
# Deployment Details
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# A space-separated list of languages to be displayed on the site.
|
||||||
|
# Now available languages: EN RU
|
||||||
|
# You can also use the keyword "ALL"
|
||||||
|
# NOTE: The order of languages affects their order on the page
|
||||||
|
languages = EN
|
||||||
|
|
||||||
# SMTP outgoing filtermail and reinjection
|
# SMTP outgoing filtermail and reinjection
|
||||||
filtermail_smtp_port = 10080
|
filtermail_smtp_port = 10080
|
||||||
postfix_reinject_port = 10025
|
postfix_reinject_port = 10025
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ dependencies = [
|
|||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"execnet",
|
"execnet",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
|
"pymdown-extensions",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ 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
|
from pyinfra.facts.systemd import SystemdEnabled
|
||||||
from pyinfra.operations import apt, files, pip, server, systemd
|
from pyinfra.operations import apt, files, pip, server, systemd
|
||||||
@@ -555,12 +555,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)]
|
||||||
|
|
||||||
@@ -569,16 +569,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"),
|
||||||
@@ -618,7 +621,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)
|
||||||
@@ -675,10 +678,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 process_on_53 not in (None, "unbound"):
|
(["master", "smtpd"], 25),
|
||||||
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
|
("unbound", 53),
|
||||||
exit(1)
|
("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(
|
apt.packages(
|
||||||
name="Install unbound",
|
name="Install unbound",
|
||||||
packages=["unbound", "unbound-anchor", "dnsutils"],
|
packages=["unbound", "unbound-anchor", "dnsutils"],
|
||||||
@@ -731,12 +756,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
|
||||||
@@ -784,6 +813,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
|
|||||||
restarted=nginx_need_restart,
|
restarted=nginx_need_restart,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
systemd.service(
|
||||||
|
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.
|
# This file is used by auth proxy.
|
||||||
# https://wiki.debian.org/EtcMailName
|
# https://wiki.debian.org/EtcMailName
|
||||||
server.shell(
|
server.shell(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from packaging import version
|
|||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
|
|
||||||
from . import dns, remote
|
from . import dns, remote
|
||||||
from .sshexec import SSHExec, Local
|
from .sshexec import SSHExec
|
||||||
|
|
||||||
#
|
#
|
||||||
# cmdeploy sub commands and options
|
# cmdeploy sub commands and options
|
||||||
@@ -62,18 +62,13 @@ def run_cmd_options(parser):
|
|||||||
"--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="specify an SSH host to deploy to; uses mail_domain from chatmail.ini by default",
|
||||||
default=None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_cmd(args, out):
|
def run_cmd(args, out):
|
||||||
"""Deploy chatmail services on the remote server."""
|
"""Deploy chatmail services on the remote server."""
|
||||||
|
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
sshexec = args.get_sshexec()
|
||||||
if ssh_host == "localhost":
|
|
||||||
sshexec = Local(ssh_host)
|
|
||||||
else:
|
|
||||||
sshexec = args.get_sshexec(ssh_host)
|
|
||||||
require_iroh = args.config.enable_iroh_relay
|
require_iroh = args.config.enable_iroh_relay
|
||||||
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):
|
||||||
@@ -85,7 +80,7 @@ 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 = "@local" if ssh_host == "localhost" else f"--ssh-host {ssh_host}"
|
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 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.")
|
||||||
@@ -94,6 +89,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"),
|
||||||
|
)
|
||||||
|
)
|
||||||
out.green("Deploy completed, call `cmdeploy dns` next.")
|
out.green("Deploy completed, call `cmdeploy dns` next.")
|
||||||
elif not remote_data["acme_account_url"]:
|
elif not remote_data["acme_account_url"]:
|
||||||
out.red("Deploy completed but letsencrypt not configured")
|
out.red("Deploy completed but letsencrypt not configured")
|
||||||
@@ -335,9 +338,9 @@ 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):
|
def get_sshexec():
|
||||||
print(f"[ssh] login to {host}")
|
print(f"[ssh] login to {args.config.mail_domain}")
|
||||||
return SSHExec(host, verbose=args.verbose)
|
return SSHExec(args.config.mail_domain, verbose=args.verbose)
|
||||||
|
|
||||||
args.get_sshexec = get_sshexec
|
args.get_sshexec = get_sshexec
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
@@ -45,16 +44,30 @@ def print_stderr(item="", end="\n"):
|
|||||||
print(item, file=sys.stderr, end=end)
|
print(item, file=sys.stderr, end=end)
|
||||||
|
|
||||||
|
|
||||||
class Exec:
|
class SSHExec:
|
||||||
|
RemoteError = execnet.RemoteError
|
||||||
FuncError = FuncError
|
FuncError = FuncError
|
||||||
|
|
||||||
def __init__(self, host, verbose, timeout):
|
def __init__(self, host, verbose=False, python="python3", timeout=60):
|
||||||
self.host = host
|
self.gateway = execnet.makegateway(f"ssh=root@{host}//python={python}")
|
||||||
|
self._remote_cmdloop_channel = bootstrap_remote(self.gateway, remote)
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
|
|
||||||
def __call__(self, call, kwargs=None, log_callback=None):
|
def __call__(self, call, kwargs=None, log_callback=None):
|
||||||
return subprocess.check_output(call)
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
assert call.__module__.startswith("cmdeploy.remote")
|
||||||
|
modname = call.__module__.replace("cmdeploy.", "")
|
||||||
|
self._remote_cmdloop_channel.send((modname, call.__name__, kwargs))
|
||||||
|
while 1:
|
||||||
|
code, data = self._remote_cmdloop_channel.receive(timeout=self.timeout)
|
||||||
|
if log_callback is not None and code == "log":
|
||||||
|
log_callback(data)
|
||||||
|
elif code == "finish":
|
||||||
|
return data
|
||||||
|
elif code == "error":
|
||||||
|
raise self.FuncError(data)
|
||||||
|
|
||||||
def logged(self, call, kwargs):
|
def logged(self, call, kwargs):
|
||||||
def log_progress(data):
|
def log_progress(data):
|
||||||
@@ -72,33 +85,3 @@ class Exec:
|
|||||||
res = self(call, kwargs, log_callback=log_progress)
|
res = self(call, kwargs, log_callback=log_progress)
|
||||||
print_stderr()
|
print_stderr()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class Local(Exec):
|
|
||||||
|
|
||||||
def __init__(self, host, verbose=False, timeout=60):
|
|
||||||
super().__init__(host, verbose, timeout)
|
|
||||||
|
|
||||||
|
|
||||||
class SSHExec(Exec):
|
|
||||||
RemoteError = execnet.RemoteError
|
|
||||||
|
|
||||||
def __init__(self, host, verbose=False, timeout=60):
|
|
||||||
super().__init__(host, verbose, timeout)
|
|
||||||
self.gateway = execnet.makegateway(f"ssh=root@{host}//python=python3")
|
|
||||||
self._remote_cmdloop_channel = bootstrap_remote(self.gateway, remote)
|
|
||||||
|
|
||||||
def __call__(self, call, kwargs=None, log_callback=None):
|
|
||||||
if kwargs is None:
|
|
||||||
kwargs = {}
|
|
||||||
assert call.__module__.startswith("cmdeploy.remote")
|
|
||||||
modname = call.__module__.replace("cmdeploy.", "")
|
|
||||||
self._remote_cmdloop_channel.send((modname, call.__name__, kwargs))
|
|
||||||
while 1:
|
|
||||||
code, data = self._remote_cmdloop_channel.receive(timeout=self.timeout)
|
|
||||||
if log_callback is not None and code == "log":
|
|
||||||
log_callback(data)
|
|
||||||
elif code == "finish":
|
|
||||||
return data
|
|
||||||
elif code == "error":
|
|
||||||
raise self.FuncError(data)
|
|
||||||
|
|||||||
@@ -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,3 +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()
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -10,6 +11,13 @@ from jinja2 import Template
|
|||||||
|
|
||||||
from .genqr import gen_qr_png_data
|
from .genqr import gen_qr_png_data
|
||||||
|
|
||||||
|
LANGUAGE_NAMES = {
|
||||||
|
"EN": " 🇬🇧 English",
|
||||||
|
"RU": " 🇷🇺 Русский",
|
||||||
|
# "UA": "Українська",
|
||||||
|
# "FR": "Français",
|
||||||
|
# "DE": "Deutsch",
|
||||||
|
}
|
||||||
|
|
||||||
def snapshot_dir_stats(somedir):
|
def snapshot_dir_stats(somedir):
|
||||||
d = {}
|
d = {}
|
||||||
@@ -21,18 +29,81 @@ def snapshot_dir_stats(somedir):
|
|||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def prepare_template(source):
|
def prepare_template(source, locales_dir, languages=["EN"]):
|
||||||
assert source.exists(), source
|
assert source.exists(), f"Template {source} not found."
|
||||||
render_vars = {}
|
assert locales_dir.exists(), f"Locales directory {locales_dir} not found."
|
||||||
render_vars["pagename"] = "home" if source.stem == "index" else source.stem
|
base_name = source.stem
|
||||||
render_vars["markdown_html"] = markdown.markdown(source.read_text())
|
render_vars = {
|
||||||
page_layout = source.with_name("page-layout.html").read_text()
|
"pagename": "home" if base_name == "index" else base_name
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_langs = (
|
||||||
|
sorted([d.name.upper() for d in locales_dir.iterdir() if d.is_dir()])
|
||||||
|
if "ALL" in [l.upper() for l in languages]
|
||||||
|
else [l.upper() for l in languages]
|
||||||
|
)
|
||||||
|
|
||||||
|
markdown_blocks = []
|
||||||
|
|
||||||
|
tabs_enabled = False
|
||||||
|
if len(selected_langs) > 1:
|
||||||
|
tabs_enabled = True
|
||||||
|
|
||||||
|
for lang_code in selected_langs:
|
||||||
|
lang_folder = locales_dir / lang_code
|
||||||
|
lang_file = lang_folder / f"{base_name}.md"
|
||||||
|
lang_name = LANGUAGE_NAMES.get(lang_code, lang_code)
|
||||||
|
|
||||||
|
if lang_file.exists():
|
||||||
|
content = lang_file.read_text().strip()
|
||||||
|
else:
|
||||||
|
print(f"[WARNING]: Missing file {lang_file}. Inserting fallback message.")
|
||||||
|
content = "Content for this language is not available, please contact your server administrator."
|
||||||
|
|
||||||
|
if tabs_enabled:
|
||||||
|
markdown_blocks.append(f"/// tab | {lang_name}\n{content}\n///")
|
||||||
|
continue
|
||||||
|
|
||||||
|
markdown_blocks.append(content)
|
||||||
|
|
||||||
|
if not markdown_blocks:
|
||||||
|
print("[WARNING] No valid language content found. Skipping file.")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
original_markdown = source.read_text()
|
||||||
|
combined_markdown = original_markdown.replace("%content placeholder%", "\n\n".join(markdown_blocks))
|
||||||
|
|
||||||
|
render_vars["markdown_html"] = markdown.markdown(
|
||||||
|
combined_markdown,
|
||||||
|
extensions=["pymdownx.blocks.tab"]
|
||||||
|
)
|
||||||
|
|
||||||
|
page_layout_path = source.with_name("page-layout.html")
|
||||||
|
assert page_layout_path.exists(), f"Missing template: {page_layout_path}"
|
||||||
|
page_layout = page_layout_path.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())
|
||||||
|
|
||||||
@@ -63,6 +134,7 @@ def int_to_english(number):
|
|||||||
|
|
||||||
def _build_webpages(src_dir, build_dir, config):
|
def _build_webpages(src_dir, build_dir, config):
|
||||||
mail_domain = config.mail_domain
|
mail_domain = config.mail_domain
|
||||||
|
languages = config.languages
|
||||||
assert src_dir.exists(), src_dir
|
assert src_dir.exists(), src_dir
|
||||||
if not build_dir.exists():
|
if not build_dir.exists():
|
||||||
build_dir.mkdir()
|
build_dir.mkdir()
|
||||||
@@ -70,18 +142,19 @@ def _build_webpages(src_dir, build_dir, config):
|
|||||||
qr_path = build_dir.joinpath(f"qr-chatmail-invite-{mail_domain}.png")
|
qr_path = build_dir.joinpath(f"qr-chatmail-invite-{mail_domain}.png")
|
||||||
qr_path.write_bytes(gen_qr_png_data(mail_domain).read())
|
qr_path.write_bytes(gen_qr_png_data(mail_domain).read())
|
||||||
|
|
||||||
|
locales_dir = src_dir / "locales"
|
||||||
|
|
||||||
for path in src_dir.iterdir():
|
for path in src_dir.iterdir():
|
||||||
if path.suffix == ".md":
|
if path.suffix == ".md":
|
||||||
render_vars, content = prepare_template(path)
|
render_vars, content = prepare_template(path, locales_dir, languages)
|
||||||
render_vars["username_min_length"] = int_to_english(
|
|
||||||
config.username_min_length
|
if render_vars is None:
|
||||||
)
|
continue
|
||||||
render_vars["username_max_length"] = int_to_english(
|
|
||||||
config.username_max_length
|
render_vars["username_min_length"] = int_to_english(config.username_min_length)
|
||||||
)
|
render_vars["username_max_length"] = int_to_english(config.username_max_length)
|
||||||
render_vars["password_min_length"] = int_to_english(
|
render_vars["password_min_length"] = int_to_english(config.password_min_length)
|
||||||
config.password_min_length
|
|
||||||
)
|
|
||||||
target = build_dir.joinpath(path.stem + ".html")
|
target = build_dir.joinpath(path.stem + ".html")
|
||||||
|
|
||||||
# recursive jinja2 rendering
|
# recursive jinja2 rendering
|
||||||
@@ -93,9 +166,11 @@ def _build_webpages(src_dir, build_dir, config):
|
|||||||
|
|
||||||
with target.open("w") as f:
|
with target.open("w") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
elif path.name != "page-layout.html":
|
|
||||||
|
elif path.name != "page-layout.html" and path.name != "locales":
|
||||||
target = build_dir.joinpath(path.name)
|
target = build_dir.joinpath(path.name)
|
||||||
target.write_bytes(path.read_bytes())
|
target.write_bytes(path.read_bytes())
|
||||||
|
|
||||||
return build_dir
|
return build_dir
|
||||||
|
|
||||||
|
|
||||||
@@ -106,15 +181,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")
|
||||||
@@ -135,7 +206,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
|
||||||
|
|||||||
@@ -1,29 +1,8 @@
|
|||||||
|
|
||||||
<img class="banner" src="collage-top.png"/>
|
<img class="banner" src="collage-top.png"/>
|
||||||
|
|
||||||
## Dear [Delta Chat](https://get.delta.chat) users and newcomers ...
|
%content placeholder%
|
||||||
|
|
||||||
{% if config.mail_domain != "nine.testrun.org" %}
|
{% if config.is_development_instance == True %}
|
||||||
Welcome to instant, interoperable and [privacy-preserving](privacy.html) messaging :)
|
|
||||||
{% else %}
|
|
||||||
Welcome to the default onboarding server ({{ config.mail_domain }})
|
|
||||||
for Delta Chat users. For details how it avoids storing personal information
|
|
||||||
please see our [privacy policy](privacy.html).
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a class="cta-button" href="DCACCOUNT:https://{{ config.mail_domain }}/new">Get a {{config.mail_domain}} chat profile</a>
|
|
||||||
|
|
||||||
If you are viewing this page on a different device
|
|
||||||
without a Delta Chat app,
|
|
||||||
you can also **scan this QR code** with 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>
|
|
||||||
|
|
||||||
🐣 **Choose** your Avatar and Name
|
|
||||||
|
|
||||||
💬 **Start** chatting with any Delta Chat contacts using [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
|
||||||
|
|
||||||
{% if config.mail_domain != "nine.testrun.org" %}
|
|
||||||
<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 %}
|
||||||
|
|||||||
@@ -1,43 +1,3 @@
|
|||||||
|
<img class="banner" src="collage-info.png"/>
|
||||||
|
|
||||||
## More information
|
%content placeholder%
|
||||||
|
|
||||||
{{ config.mail_domain }} provides a low-maintenance, resource efficient and
|
|
||||||
interoperable e-mail service for everyone. What's behind a `chatmail` is
|
|
||||||
effectively a normal e-mail address just like any other but optimized
|
|
||||||
for the usage in chats, especially DeltaChat.
|
|
||||||
|
|
||||||
|
|
||||||
### Rate and storage limits
|
|
||||||
|
|
||||||
- Un-encrypted messages are blocked to recipients outside
|
|
||||||
{{config.mail_domain}} but setting up contact via [QR invite codes](https://delta.chat/en/help#howtoe2ee)
|
|
||||||
allows your messages to pass freely to any outside recipients.
|
|
||||||
|
|
||||||
- You may send up to {{ config.max_user_send_per_minute }} messages per minute.
|
|
||||||
|
|
||||||
- You can store up to [{{ config.max_mailbox_size }} messages on the server](https://delta.chat/en/help#what-happens-if-i-turn-on-delete-old-messages-from-server).
|
|
||||||
|
|
||||||
- Messages are unconditionally removed latest {{ config.delete_mails_after }} days after arriving on the server.
|
|
||||||
Earlier, if storage may exceed otherwise.
|
|
||||||
|
|
||||||
|
|
||||||
### <a name="account-deletion"></a> Account deletion
|
|
||||||
|
|
||||||
If you remove a {{ config.mail_domain }} profile from within the Delta Chat app,
|
|
||||||
then the according account on the server, along with all associated data,
|
|
||||||
is automatically deleted {{ config.delete_inactive_users_after }} days afterwards.
|
|
||||||
|
|
||||||
If you use multiple devices
|
|
||||||
then you need to remove the according chat profile from each device
|
|
||||||
in order for all account data to be removed on the server side.
|
|
||||||
|
|
||||||
If you have any further questions or requests regarding account deletion
|
|
||||||
please send a message from your account to {{ config.privacy_mail }}.
|
|
||||||
|
|
||||||
|
|
||||||
### Who are the operators? Which software is running?
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -84,3 +84,57 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,271 +1,3 @@
|
|||||||
|
<img class="banner" src="collage-privacy.png"/>
|
||||||
|
|
||||||
# Privacy Policy for {{ config.mail_domain }}
|
%content placeholder%
|
||||||
|
|
||||||
{% if config.mail_domain == "nine.testrun.org" %}
|
|
||||||
Welcome to `{{config.mail_domain}}`, the default chatmail onboarding server for Delta Chat users.
|
|
||||||
It is operated on the side by a small sysops team
|
|
||||||
on a voluntary basis.
|
|
||||||
See [other chatmail servers](https://delta.chat/en/chatmail) for alternative server operators.
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
## Summary: No personal data asked or collected
|
|
||||||
|
|
||||||
This chatmail server neither asks for nor retains personal information.
|
|
||||||
Chatmail servers exist to reliably transmit (store and deliver) end-to-end encrypted messages
|
|
||||||
between user's devices running the Delta Chat messenger app.
|
|
||||||
Technically, you may think of a Chatmail server as
|
|
||||||
an end-to-end encrypted "messaging router" at Internet-scale.
|
|
||||||
|
|
||||||
A chatmail server is very unlike classic e-mail servers (for example Google Mail servers)
|
|
||||||
that ask for personal data and permanently store messages.
|
|
||||||
A chatmail server behaves more like the Signal messaging server
|
|
||||||
but does not know about phone numbers and securely and automatically interoperates
|
|
||||||
with other chatmail and classic e-mail servers.
|
|
||||||
|
|
||||||
Unlike classic e-mail servers, this chatmail server
|
|
||||||
|
|
||||||
- unconditionally removes messages after {{ config.delete_mails_after }} days,
|
|
||||||
|
|
||||||
- prohibits sending out un-encrypted messages,
|
|
||||||
|
|
||||||
- does not store Internet addresses ("IP addresses"),
|
|
||||||
|
|
||||||
- does not process IP addresses in relation to email addresses.
|
|
||||||
|
|
||||||
Due to the resulting lack of personal data processing
|
|
||||||
this chatmail server may not require a privacy policy.
|
|
||||||
|
|
||||||
Nevertheless, we provide legal details below to make life easier
|
|
||||||
for data protection specialists and lawyers scrutinizing chatmail operations.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 1. Name and contact information
|
|
||||||
|
|
||||||
Responsible for the processing of your personal data is:
|
|
||||||
```
|
|
||||||
{{ config.privacy_postal }}
|
|
||||||
```
|
|
||||||
|
|
||||||
E-mail: {{ config.privacy_mail }}
|
|
||||||
|
|
||||||
We have appointed a data protection officer:
|
|
||||||
|
|
||||||
```
|
|
||||||
{{ config.privacy_pdo }}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Processing when using chat e-mail services
|
|
||||||
|
|
||||||
We provide services optimized for the use from [Delta Chat](https://delta.chat) apps
|
|
||||||
and process only the data necessary
|
|
||||||
for the setup and technical execution of message delivery.
|
|
||||||
The purpose of the processing is that users can
|
|
||||||
read, write, manage, delete, send, and receive chat messages.
|
|
||||||
For this purpose,
|
|
||||||
we operate server-side software
|
|
||||||
that enables us to send and receive messages.
|
|
||||||
|
|
||||||
We process the following data and details:
|
|
||||||
|
|
||||||
- Outgoing and incoming messages (SMTP) are stored for transit
|
|
||||||
on behalf of their users until the message can be delivered.
|
|
||||||
|
|
||||||
- E-Mail-Messages are stored for the recipient and made accessible via IMAP protocols,
|
|
||||||
until explicitly deleted by the user or until a fixed time period is exceeded,
|
|
||||||
(*usually 4-8 weeks*).
|
|
||||||
|
|
||||||
- IMAP and SMTP protocols are password protected with unique credentials for each account.
|
|
||||||
|
|
||||||
- Users can retrieve or delete all stored messages
|
|
||||||
without intervention from the operators using standard IMAP client tools.
|
|
||||||
|
|
||||||
- Users can connect to a "realtime relay service"
|
|
||||||
to establish Peer-to-Peer connection between user devices,
|
|
||||||
allowing them to send and retrieve ephemeral messages
|
|
||||||
which are never stored on the chatmail server, also not in encrypted form.
|
|
||||||
|
|
||||||
|
|
||||||
### 2.1 Account setup
|
|
||||||
|
|
||||||
Creating an account happens in one of two ways on our mail servers:
|
|
||||||
|
|
||||||
- with a QR invitation token
|
|
||||||
which is scanned using the Delta Chat app
|
|
||||||
and then the account is created.
|
|
||||||
|
|
||||||
- by letting Delta Chat otherwise create an account
|
|
||||||
and register it with a {{ config.mail_domain }} mail server.
|
|
||||||
|
|
||||||
In either case, we process the newly created email address.
|
|
||||||
No phone numbers,
|
|
||||||
other email addresses,
|
|
||||||
or other identifiable data
|
|
||||||
is currently required.
|
|
||||||
The legal basis for the processing is
|
|
||||||
Art. 6 (1) lit. b GDPR,
|
|
||||||
as you have a usage contract with us
|
|
||||||
by using our services.
|
|
||||||
|
|
||||||
### 2.2 Processing of E-Mail-Messages
|
|
||||||
|
|
||||||
In addition,
|
|
||||||
we will process data
|
|
||||||
to keep the server infrastructure operational
|
|
||||||
for purposes of e-mail dispatch
|
|
||||||
and abuse prevention.
|
|
||||||
|
|
||||||
- Therefore,
|
|
||||||
it is necessary to process the content and/or metadata
|
|
||||||
(e.g., headers of the email as well as smtp chatter)
|
|
||||||
of E-Mail-Messages in transit.
|
|
||||||
|
|
||||||
- We will keep logs of messages in transit for a limited time.
|
|
||||||
These logs are used to debug delivery problems and software bugs.
|
|
||||||
|
|
||||||
In addition,
|
|
||||||
we process data to protect the systems from excessive use.
|
|
||||||
Therefore, limits are enforced:
|
|
||||||
|
|
||||||
- rate limits
|
|
||||||
|
|
||||||
- storage limits
|
|
||||||
|
|
||||||
- message size limits
|
|
||||||
|
|
||||||
- any other limit necessary for the whole server to function in a healthy way
|
|
||||||
and to prevent abuse.
|
|
||||||
|
|
||||||
The processing and use of the above permissions
|
|
||||||
are performed to provide the service.
|
|
||||||
The data processing is necessary for the use of our services,
|
|
||||||
therefore the legal basis of the processing is
|
|
||||||
Art. 6 (1) lit. b GDPR,
|
|
||||||
as you have a usage contract with us
|
|
||||||
by using our services.
|
|
||||||
The legal basis for the data processing
|
|
||||||
for the purposes of security and abuse prevention is
|
|
||||||
Art. 6 (1) lit. f GDPR.
|
|
||||||
Our legitimate interest results
|
|
||||||
from the aforementioned purposes.
|
|
||||||
We will not use the collected data
|
|
||||||
for the purpose of drawing conclusions
|
|
||||||
about your person.
|
|
||||||
|
|
||||||
|
|
||||||
## 3. Processing when using our Website
|
|
||||||
|
|
||||||
When you visit our website,
|
|
||||||
the browser used on your end device
|
|
||||||
automatically sends information to the server of our website.
|
|
||||||
This information is temporarily stored in a so-called log file.
|
|
||||||
The following information is collected and stored
|
|
||||||
until it is automatically deleted
|
|
||||||
(*usually 7 days*):
|
|
||||||
|
|
||||||
- used type of browser,
|
|
||||||
|
|
||||||
- used operating system,
|
|
||||||
|
|
||||||
- access date and time as well as
|
|
||||||
|
|
||||||
- country of origin and IP address,
|
|
||||||
|
|
||||||
- the requested file name or HTTP resource,
|
|
||||||
|
|
||||||
- the amount of data transferred,
|
|
||||||
|
|
||||||
- the access status (file transferred, file not found, etc.) and
|
|
||||||
|
|
||||||
- the page from which the file was requested.
|
|
||||||
|
|
||||||
This website is hosted by an external service provider (hoster).
|
|
||||||
The personal data collected on this website is stored
|
|
||||||
on the hoster's servers.
|
|
||||||
Our hoster will process your data
|
|
||||||
only to the extent necessary to fulfill its obligations
|
|
||||||
to perform under our instructions.
|
|
||||||
In order to ensure data protection-compliant processing,
|
|
||||||
we have concluded a data processing agreement with our hoster.
|
|
||||||
|
|
||||||
The aforementioned data is processed by us for the following purposes:
|
|
||||||
|
|
||||||
- Ensuring a reliable connection setup of the website,
|
|
||||||
|
|
||||||
- ensuring a convenient use of our website,
|
|
||||||
|
|
||||||
- checking and ensuring system security and stability, and
|
|
||||||
|
|
||||||
- for other administrative purposes.
|
|
||||||
|
|
||||||
The legal basis for the data processing is
|
|
||||||
Art. 6 (1) lit. f GDPR.
|
|
||||||
Our legitimate interest results
|
|
||||||
from the aforementioned purposes of data collection.
|
|
||||||
We will not use the collected data
|
|
||||||
for the purpose of drawing conclusions about your person.
|
|
||||||
|
|
||||||
## 4. Transfer of Data
|
|
||||||
|
|
||||||
We do not retain any personal data but e-mail messages waiting to be delivered
|
|
||||||
may contain personal data.
|
|
||||||
Any such residual personal data will not be transferred to third parties
|
|
||||||
for purposes other than those listed below:
|
|
||||||
|
|
||||||
a) you have given your express consent
|
|
||||||
in accordance with Art. 6 para. 1 sentence 1 lit. a GDPR,
|
|
||||||
|
|
||||||
b) the disclosure is necessary for the assertion, exercise or defence of legal claims
|
|
||||||
pursuant to Art. 6 (1) sentence 1 lit. f GDPR
|
|
||||||
and there is no reason to assume that you have
|
|
||||||
an overriding interest worthy of protection
|
|
||||||
in the non-disclosure of your data,
|
|
||||||
|
|
||||||
c) in the event that there is a legal obligation to disclose your data
|
|
||||||
pursuant to Art. 6 para. 1 sentence 1 lit. c GDPR,
|
|
||||||
as well as
|
|
||||||
|
|
||||||
d) this is legally permissible and necessary
|
|
||||||
in accordance with Art. 6 Para. 1 S. 1 lit. b GDPR
|
|
||||||
for the processing of contractual relationships with you,
|
|
||||||
|
|
||||||
e) this is carried out by a service provider
|
|
||||||
acting on our behalf and on our exclusive instructions,
|
|
||||||
whom we have carefully selected (Art. 28 (1) GDPR)
|
|
||||||
and with whom we have concluded a corresponding contract on commissioned processing (Art. 28 (3) GDPR),
|
|
||||||
which obliges our contractor,
|
|
||||||
among other things,
|
|
||||||
to implement appropriate security measures
|
|
||||||
and grants us comprehensive control powers.
|
|
||||||
|
|
||||||
## 5. Rights of the data subject
|
|
||||||
|
|
||||||
The rights arise from Articles 12 to 23 GDPR.
|
|
||||||
Since no personal data is stored on our servers,
|
|
||||||
even in encrypted form,
|
|
||||||
there is no need to provide information
|
|
||||||
on these or possible objections.
|
|
||||||
A deletion can be made
|
|
||||||
directly in the Delta Chat email messenger.
|
|
||||||
|
|
||||||
If you have any questions or complaints,
|
|
||||||
please feel free to contact us by email:
|
|
||||||
{{ config.privacy_mail }}
|
|
||||||
|
|
||||||
As a rule, you can contact the supervisory authority of your usual place of residence
|
|
||||||
or workplace
|
|
||||||
or our registered office for this purpose.
|
|
||||||
The supervisory authority responsible for our place of business
|
|
||||||
is the `{{ config.privacy_supervisor }}`.
|
|
||||||
|
|
||||||
|
|
||||||
## 6. Validity of this privacy policy
|
|
||||||
|
|
||||||
This data protection declaration is valid
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user