mirror of
https://github.com/chatmail/relay.git
synced 2026-05-11 08:24:37 +00:00
Compare commits
1 Commits
cmdeploy-p
...
markdown-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62f028bc67 |
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
- cmdeploy: make --ssh-host work with localhost
|
|
||||||
([#659](https://github.com/chatmail/relay/pull/659))
|
|
||||||
|
|
||||||
- Update iroh-relay to 0.35.0
|
- Update iroh-relay to 0.35.0
|
||||||
([#650](https://github.com/chatmail/relay/pull/650))
|
([#650](https://github.com/chatmail/relay/pull/650))
|
||||||
|
|
||||||
@@ -17,6 +14,10 @@
|
|||||||
- Increase maxproc for reinjecting ports from 10 to 100
|
- Increase maxproc for reinjecting ports from 10 to 100
|
||||||
([#646](https://github.com/chatmail/relay/pull/646))
|
([#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
|
- Allow ports 143 and 993 to be used by `dovecot` process
|
||||||
([#639](https://github.com/chatmail/relay/pull/639))
|
([#639](https://github.com/chatmail/relay/pull/639))
|
||||||
|
|
||||||
|
|||||||
87
README.md
87
README.md
@@ -456,15 +456,94 @@ to send messages outside.
|
|||||||
|
|
||||||
To setup a reverse proxy
|
To setup a reverse proxy
|
||||||
(or rather Destination NAT, DNAT)
|
(or rather Destination NAT, DNAT)
|
||||||
for your chatmail relay, run:
|
for your chatmail relay,
|
||||||
|
put the following configuration in `/etc/nftables.conf`:
|
||||||
|
```
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
|
||||||
|
flush ruleset
|
||||||
|
|
||||||
|
define wan = eth0
|
||||||
|
|
||||||
|
# Which ports to proxy.
|
||||||
|
#
|
||||||
|
# Note that SSH is not proxied
|
||||||
|
# so it is possible to log into the proxy server
|
||||||
|
# and not the original one.
|
||||||
|
define ports = { smtp, http, https, imap, imaps, submission, submissions }
|
||||||
|
|
||||||
|
# The host we want to proxy to.
|
||||||
|
define ipv4_address = AAA.BBB.CCC.DDD
|
||||||
|
define ipv6_address = [XXX::1]
|
||||||
|
|
||||||
|
table ip nat {
|
||||||
|
chain prerouting {
|
||||||
|
type nat hook prerouting priority dstnat; policy accept;
|
||||||
|
iif $wan tcp dport $ports dnat to $ipv4_address
|
||||||
|
}
|
||||||
|
|
||||||
|
chain postrouting {
|
||||||
|
type nat hook postrouting priority 0;
|
||||||
|
|
||||||
|
oifname $wan masquerade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table ip6 nat {
|
||||||
|
chain prerouting {
|
||||||
|
type nat hook prerouting priority dstnat; policy accept;
|
||||||
|
iif $wan tcp dport $ports dnat to $ipv6_address
|
||||||
|
}
|
||||||
|
|
||||||
|
chain postrouting {
|
||||||
|
type nat hook postrouting priority 0;
|
||||||
|
|
||||||
|
oifname $wan masquerade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority filter; policy drop;
|
||||||
|
|
||||||
|
# Accept ICMP.
|
||||||
|
# It is especially important to accept ICMPv6 ND messages,
|
||||||
|
# otherwise IPv6 connectivity breaks.
|
||||||
|
icmp type { echo-request } accept
|
||||||
|
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
|
||||||
|
|
||||||
|
# Allow incoming SSH connections.
|
||||||
|
tcp dport { ssh } accept
|
||||||
|
|
||||||
|
ct state established accept
|
||||||
|
}
|
||||||
|
chain forward {
|
||||||
|
type filter hook forward priority filter; policy drop;
|
||||||
|
|
||||||
|
ct state established accept
|
||||||
|
ip daddr $ipv4_address counter accept
|
||||||
|
ip6 daddr $ipv6_address counter accept
|
||||||
|
}
|
||||||
|
chain output {
|
||||||
|
type filter hook output priority filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `systemctl enable nftables.service`
|
||||||
|
to ensure configuration is reloaded when the proxy relay reboots.
|
||||||
|
|
||||||
|
Uncomment in `/etc/sysctl.conf` the following two lines:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy proxy <proxy_ip_address> --relay-ipv4 <relay_ipv4_address> --relay-ipv6 <relay_ipv6_address>
|
net.ipv4.ip_forward=1
|
||||||
|
net.ipv6.conf.all.forwarding=1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then reboot the relay or do `sysctl -p` and `nft -f /etc/nftables.conf`.
|
||||||
|
|
||||||
Once proxy relay is set up,
|
Once proxy relay is set up,
|
||||||
you can add its IP address to the DNS,
|
you can add its IP address to the DNS.
|
||||||
or distribute it as you wish.
|
|
||||||
|
|
||||||
## Neighbors and Acquaintances
|
## Neighbors and Acquaintances
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ 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.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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ dependencies = [
|
|||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"execnet",
|
"execnet",
|
||||||
"imap_tools",
|
"imap_tools",
|
||||||
|
"pymdown-extensions",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -61,15 +61,14 @@ def run_cmd_options(parser):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--ssh-host",
|
"--ssh-host",
|
||||||
dest="ssh_host",
|
dest="ssh_host",
|
||||||
help="Deploy to 'localhost' or to a specific SSH host",
|
help="specify an SSH host to deploy to; uses mail_domain from chatmail.ini by default",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
sshexec = 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):
|
||||||
@@ -81,11 +80,8 @@ def run_cmd(args, out):
|
|||||||
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
|
||||||
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
||||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
||||||
|
ssh_host = args.config.mail_domain if not args.ssh_host else args.ssh_host
|
||||||
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
|
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
|
||||||
if ssh_host == "localhost":
|
|
||||||
cmd = f"{pyinf} @local {deploy_path} -y"
|
|
||||||
|
|
||||||
if version.parse(pyinfra.__version__) < version.parse("3"):
|
if version.parse(pyinfra.__version__) < version.parse("3"):
|
||||||
out.red("Please re-run scripts/initenv.sh to update pyinfra to version 3.")
|
out.red("Please re-run scripts/initenv.sh to update pyinfra to version 3.")
|
||||||
return 1
|
return 1
|
||||||
@@ -122,17 +118,11 @@ def dns_cmd_options(parser):
|
|||||||
default=None,
|
default=None,
|
||||||
help="write out a zonefile",
|
help="write out a zonefile",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--ssh-host",
|
|
||||||
dest="ssh_host",
|
|
||||||
help="Run the DNS queries on 'localhost' or on a specific SSH host",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def dns_cmd(args, out):
|
def dns_cmd(args, out):
|
||||||
"""Check DNS entries and optionally generate dns zone file."""
|
"""Check DNS entries and optionally generate dns zone file."""
|
||||||
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain
|
sshexec = args.get_sshexec()
|
||||||
sshexec = get_sshexec(ssh_host, verbose=args.verbose)
|
|
||||||
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
|
||||||
if not remote_data:
|
if not remote_data:
|
||||||
return 1
|
return 1
|
||||||
@@ -208,61 +198,6 @@ def test_cmd(args, out):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def proxy_cmd_options(parser: argparse.ArgumentParser):
|
|
||||||
parser.add_argument(
|
|
||||||
"ip_address",
|
|
||||||
help="specify a server to deploy to; can also be an inventory.py file",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--relay-ipv4",
|
|
||||||
dest="relay_ipv4",
|
|
||||||
help="The ipv4 address of the relay you want to forward traffic to",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--relay-ipv6",
|
|
||||||
dest="relay_ipv6",
|
|
||||||
help="The ipv6 address of the relay you want to forward traffic to",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dry-run",
|
|
||||||
dest="dry_run",
|
|
||||||
action="store_true",
|
|
||||||
help="don't actually modify the server",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def proxy_cmd(args, out):
|
|
||||||
"""Deploy reverse proxy on a second server."""
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["RELAY_IPV4"] = args.relay_ipv4
|
|
||||||
env["RELAY_IPV6"] = args.relay_ipv6
|
|
||||||
deploy_path = importlib.resources.files(__package__).joinpath("proxy-deploy.py").resolve()
|
|
||||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
|
||||||
|
|
||||||
sshexec = args.get_sshexec()
|
|
||||||
# :todo make sure relay is deployed to args.relay_ipv4 and args.relay_ipv6
|
|
||||||
|
|
||||||
# abort if IP address == the chatmail relay itself: if port 22 is open AND /etc/chatmail-version exists
|
|
||||||
if sshexec.logged(call=remote.rshell.get_port_service, args=[22]):
|
|
||||||
if sshexec.logged(call=remote.rshell.chatmail_version):
|
|
||||||
out.red("Can not deploy proxy on the chatmail relay itself, use another server")
|
|
||||||
return 1
|
|
||||||
cmd = f"{pyinf} --ssh-user root {args.ip_address} {deploy_path} -y"
|
|
||||||
out.check_call(cmd, env=env) # during first try, only set SSH port to 2222
|
|
||||||
|
|
||||||
cmd = f"{pyinf} --ssh-port 2222 --ssh-user root {args.ip_address} {deploy_path} -y"
|
|
||||||
try:
|
|
||||||
retcode = out.check_call(cmd, env=env)
|
|
||||||
if retcode == 0:
|
|
||||||
out.green("Reverse proxy deployed - you can distribute the IP address now.")
|
|
||||||
else:
|
|
||||||
out.red("Deploying reverse proxy failed")
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
out.red("Deploying reverse proxy failed")
|
|
||||||
retcode = 1
|
|
||||||
return retcode
|
|
||||||
|
|
||||||
|
|
||||||
def fmt_cmd_options(parser):
|
def fmt_cmd_options(parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--check",
|
"--check",
|
||||||
@@ -396,14 +331,6 @@ def get_parser():
|
|||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def get_sshexec(ssh_host: str, verbose=True):
|
|
||||||
if ssh_host in ["localhost", "@local"]:
|
|
||||||
return "localhost"
|
|
||||||
if verbose:
|
|
||||||
print(f"[ssh] login to {ssh_host}")
|
|
||||||
return SSHExec(ssh_host, verbose=verbose)
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
"""Provide main entry point for 'cmdeploy' CLI invocation."""
|
"""Provide main entry point for 'cmdeploy' CLI invocation."""
|
||||||
parser = get_parser()
|
parser = get_parser()
|
||||||
@@ -411,6 +338,12 @@ 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():
|
||||||
|
print(f"[ssh] login to {args.config.mail_domain}")
|
||||||
|
return SSHExec(args.config.mail_domain, verbose=args.verbose)
|
||||||
|
|
||||||
|
args.get_sshexec = get_sshexec
|
||||||
|
|
||||||
out = Out()
|
out = Out()
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
if args.func.__name__ not in ("init_cmd", "fmt_cmd"):
|
||||||
|
|||||||
@@ -7,13 +7,9 @@ from . import remote
|
|||||||
|
|
||||||
|
|
||||||
def get_initial_remote_data(sshexec, mail_domain):
|
def get_initial_remote_data(sshexec, mail_domain):
|
||||||
if sshexec == "localhost":
|
return sshexec.logged(
|
||||||
result = remote.rdns.perform_initial_checks(mail_domain)
|
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
||||||
else:
|
)
|
||||||
result = sshexec.logged(
|
|
||||||
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def check_initial_remote_data(remote_data, *, print=print):
|
def check_initial_remote_data(remote_data, *, print=print):
|
||||||
@@ -48,14 +44,10 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
|
|||||||
"""Check existing DNS records, optionally write them to zone file
|
"""Check existing DNS records, optionally write them to zone file
|
||||||
and return (exitcode, remote_data) tuple."""
|
and return (exitcode, remote_data) tuple."""
|
||||||
|
|
||||||
if sshexec == "localhost":
|
required_diff, recommended_diff = sshexec.logged(
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
remote.rdns.check_zonefile,
|
||||||
zonefile=zonefile, verbose=False
|
kwargs=dict(zonefile=zonefile, mail_domain=remote_data["mail_domain"]),
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
required_diff, recommended_diff = sshexec.logged(
|
|
||||||
remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False),
|
|
||||||
)
|
|
||||||
|
|
||||||
returncode = 0
|
returncode = 0
|
||||||
if required_diff:
|
if required_diff:
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
import pyinfra
|
|
||||||
from pyinfra import host
|
|
||||||
|
|
||||||
from proxy import configure_ssh, configure_proxy
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ipv4_relay = os.getenv("IPV4_RELAY")
|
|
||||||
ipv6_relay = os.getenv("IPV6_RELAY")
|
|
||||||
|
|
||||||
configure_ssh()
|
|
||||||
if host.data.get("ssh_port") not in (None, 22):
|
|
||||||
configure_proxy(ipv4_relay, ipv6_relay)
|
|
||||||
|
|
||||||
|
|
||||||
if pyinfra.is_cli:
|
|
||||||
main()
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import importlib
|
|
||||||
|
|
||||||
from pyinfra import host
|
|
||||||
from pyinfra.operations import files, server, apt, systemd
|
|
||||||
|
|
||||||
def configure_ssh():
|
|
||||||
files.replace(
|
|
||||||
name="Configure sshd to use port 2222",
|
|
||||||
path="/etc/ssh/sshd_config",
|
|
||||||
text="Port 22\n",
|
|
||||||
replace="Port 2222\n",
|
|
||||||
)
|
|
||||||
systemd.service(
|
|
||||||
name="apply SSH config",
|
|
||||||
service="ssh",
|
|
||||||
reloaded=True,
|
|
||||||
)
|
|
||||||
apt.update()
|
|
||||||
|
|
||||||
|
|
||||||
def configure_proxy(ipv4_relay, ipv6_relay):
|
|
||||||
files.put(
|
|
||||||
name="Configure nftables",
|
|
||||||
src=importlib.resources.files(__package__).joinpath("proxy_files/nftables.conf.j2"),
|
|
||||||
dest="/etc/nftables.conf",
|
|
||||||
ipv4_address=ipv4_relay, # :todo what if only one of them is specified?
|
|
||||||
ipv6_address=ipv6_relay,
|
|
||||||
)
|
|
||||||
|
|
||||||
server.sysctl(name="enable IPv4 forwarding", key="net.ipv4.ip_forward", value=1, persist=True)
|
|
||||||
|
|
||||||
server.sysctl(
|
|
||||||
name="enable IPv6 forwarding",
|
|
||||||
key="net.ipv6.conf.all.forwarding",
|
|
||||||
value=1,
|
|
||||||
persist=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
server.shell(
|
|
||||||
name="apply forwarding configuration",
|
|
||||||
commands=[
|
|
||||||
"sysctl -p",
|
|
||||||
"nft -f /etc/nftables.conf",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
if host.data.get("floating_ips"):
|
|
||||||
i = 0
|
|
||||||
for floating_ip in host.data.get("floating_ips"):
|
|
||||||
i += 1
|
|
||||||
files.template(
|
|
||||||
name="Add floating IPs",
|
|
||||||
src="servers/proxy-nine/files/60-floating.ip.cfg.j2",
|
|
||||||
dest=f"/etc/network/interfaces.d/{59 + i}-floating.ip.cfg",
|
|
||||||
ip_address=floating_ip,
|
|
||||||
i=i,
|
|
||||||
)
|
|
||||||
|
|
||||||
systemd.service(
|
|
||||||
name="apply floating IPs",
|
|
||||||
service="networking",
|
|
||||||
restarted=True,
|
|
||||||
)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
auto eth0:{{ i }}
|
|
||||||
iface eth0:{{ i }} inet static
|
|
||||||
address {{ ip_address }}
|
|
||||||
netmask 32
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/sbin/nft -f
|
|
||||||
|
|
||||||
flush ruleset
|
|
||||||
|
|
||||||
define wan = eth0
|
|
||||||
|
|
||||||
# which ports to proxy
|
|
||||||
define ports = { smtp, http, https, imap, imaps, submission, submissions }
|
|
||||||
|
|
||||||
# the host we want to proxy to
|
|
||||||
define ipv4_address = {{ ipv4_address }}
|
|
||||||
define ipv6_address = [{{ ipv6_address }}]
|
|
||||||
|
|
||||||
table ip nat {
|
|
||||||
chain prerouting {
|
|
||||||
type nat hook prerouting priority dstnat; policy accept;
|
|
||||||
iif $wan tcp dport $ports dnat to $ipv4_address
|
|
||||||
}
|
|
||||||
|
|
||||||
chain postrouting {
|
|
||||||
type nat hook postrouting priority 0;
|
|
||||||
|
|
||||||
oifname $wan masquerade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table ip6 nat {
|
|
||||||
chain prerouting {
|
|
||||||
type nat hook prerouting priority dstnat; policy accept;
|
|
||||||
iif $wan tcp dport $ports dnat to $ipv6_address
|
|
||||||
}
|
|
||||||
|
|
||||||
chain postrouting {
|
|
||||||
type nat hook postrouting priority 0;
|
|
||||||
|
|
||||||
oifname $wan masquerade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table inet filter {
|
|
||||||
chain input {
|
|
||||||
type filter hook input priority filter; policy drop;
|
|
||||||
|
|
||||||
# Accept ICMP.
|
|
||||||
# It is especially important to accept ICMPv6 ND messages,
|
|
||||||
# otherwise IPv6 connectivity breaks.
|
|
||||||
icmp type { echo-request } accept
|
|
||||||
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
|
|
||||||
|
|
||||||
# Allow incoming SSH connections.
|
|
||||||
tcp dport { 22, 2222 } accept
|
|
||||||
# Allow incoming shadowsocks connections.
|
|
||||||
tcp dport { 8388 } accept
|
|
||||||
|
|
||||||
ct state established accept
|
|
||||||
}
|
|
||||||
chain forward {
|
|
||||||
type filter hook forward priority filter; policy drop;
|
|
||||||
|
|
||||||
ct state established accept
|
|
||||||
ip daddr $ipv4_address counter accept
|
|
||||||
ip6 daddr $ipv6_address counter accept
|
|
||||||
}
|
|
||||||
chain output {
|
|
||||||
type filter hook output priority filter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,23 +12,23 @@ All functions of this module
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .rshell import CalledProcessError, shell, log_progress
|
from .rshell import CalledProcessError, shell
|
||||||
|
|
||||||
|
|
||||||
def perform_initial_checks(mail_domain, pre_command=""):
|
def perform_initial_checks(mail_domain):
|
||||||
"""Collecting initial DNS settings."""
|
"""Collecting initial DNS settings."""
|
||||||
assert mail_domain
|
assert mail_domain
|
||||||
if not shell("dig", fail_ok=True, print=log_progress):
|
if not shell("dig", fail_ok=True):
|
||||||
shell("apt-get update && apt-get install -y dnsutils", print=log_progress)
|
shell("apt-get update && apt-get install -y dnsutils")
|
||||||
A = query_dns("A", mail_domain)
|
A = query_dns("A", mail_domain)
|
||||||
AAAA = query_dns("AAAA", mail_domain)
|
AAAA = query_dns("AAAA", mail_domain)
|
||||||
MTA_STS = query_dns("CNAME", f"mta-sts.{mail_domain}")
|
MTA_STS = query_dns("CNAME", f"mta-sts.{mail_domain}")
|
||||||
WWW = query_dns("CNAME", f"www.{mail_domain}")
|
WWW = query_dns("CNAME", f"www.{mail_domain}")
|
||||||
|
|
||||||
res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, WWW=WWW)
|
res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, WWW=WWW)
|
||||||
res["acme_account_url"] = shell(pre_command + "acmetool account-url", fail_ok=True, print=log_progress)
|
res["acme_account_url"] = shell("acmetool account-url", fail_ok=True)
|
||||||
res["dkim_entry"], res["web_dkim_entry"] = get_dkim_entry(
|
res["dkim_entry"], res["web_dkim_entry"] = get_dkim_entry(
|
||||||
mail_domain, pre_command, dkim_selector="opendkim"
|
mail_domain, dkim_selector="opendkim"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not MTA_STS or not WWW or (not A and not AAAA):
|
if not MTA_STS or not WWW or (not A and not AAAA):
|
||||||
@@ -40,12 +40,11 @@ def perform_initial_checks(mail_domain, pre_command=""):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def get_dkim_entry(mail_domain, pre_command, dkim_selector):
|
def get_dkim_entry(mail_domain, dkim_selector):
|
||||||
try:
|
try:
|
||||||
dkim_pubkey = shell(
|
dkim_pubkey = shell(
|
||||||
f"{pre_command}openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
|
f"openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
|
||||||
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'",
|
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'"
|
||||||
print=log_progress
|
|
||||||
)
|
)
|
||||||
except CalledProcessError:
|
except CalledProcessError:
|
||||||
return
|
return
|
||||||
@@ -62,7 +61,7 @@ def query_dns(typ, domain):
|
|||||||
# Get autoritative nameserver from the SOA record.
|
# Get autoritative nameserver from the SOA record.
|
||||||
soa_answers = [
|
soa_answers = [
|
||||||
x.split()
|
x.split()
|
||||||
for x in shell(f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress).split(
|
for x in shell(f"dig -r -q {domain} -t SOA +noall +authority +answer").split(
|
||||||
"\n"
|
"\n"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -72,13 +71,13 @@ def query_dns(typ, domain):
|
|||||||
ns = soa[0][4]
|
ns = soa[0][4]
|
||||||
|
|
||||||
# Query authoritative nameserver directly to bypass DNS cache.
|
# Query authoritative nameserver directly to bypass DNS cache.
|
||||||
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
|
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short")
|
||||||
if res:
|
if res:
|
||||||
return res.split("\n")[0]
|
return res.split("\n")[0]
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_zonefile(zonefile, verbose=True):
|
def check_zonefile(zonefile, mail_domain):
|
||||||
"""Check expected zone file entries."""
|
"""Check expected zone file entries."""
|
||||||
required = True
|
required = True
|
||||||
required_diff = []
|
required_diff = []
|
||||||
@@ -90,7 +89,7 @@ def check_zonefile(zonefile, verbose=True):
|
|||||||
continue
|
continue
|
||||||
if not zf_line.strip() or zf_line.startswith(";"):
|
if not zf_line.strip() or zf_line.startswith(";"):
|
||||||
continue
|
continue
|
||||||
print(f"dns-checking {zf_line!r}") if verbose else log_progress("")
|
print(f"dns-checking {zf_line!r}")
|
||||||
zf_domain, zf_typ, zf_value = zf_line.split(maxsplit=2)
|
zf_domain, zf_typ, zf_value = zf_line.split(maxsplit=2)
|
||||||
zf_domain = zf_domain.rstrip(".")
|
zf_domain = zf_domain.rstrip(".")
|
||||||
zf_value = zf_value.strip()
|
zf_value = zf_value.strip()
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from subprocess import DEVNULL, CalledProcessError, check_output
|
from subprocess import DEVNULL, CalledProcessError, check_output
|
||||||
|
|
||||||
|
|
||||||
def log_progress(data):
|
def shell(command, fail_ok=False):
|
||||||
sys.stderr.write(".")
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def shell(command, fail_ok=False, print=print):
|
|
||||||
print(f"$ {command}")
|
print(f"$ {command}")
|
||||||
args = dict(shell=True)
|
args = dict(shell=True)
|
||||||
if fail_ok:
|
if fail_ok:
|
||||||
@@ -21,20 +14,6 @@ def shell(command, fail_ok=False, print=print):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def get_port_service(port: int) -> str:
|
|
||||||
return shell(
|
|
||||||
"ss -lptn 'src :%d' | awk 'NR>1 {print $6,$7}' | sed 's/users:((\"//;s/\".*//'"
|
|
||||||
% (port,)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def chatmail_version():
|
|
||||||
version = shell("cat /etc/chatmail-version")
|
|
||||||
if "cat: /etc/chatmail-version:" in version:
|
|
||||||
version = None
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
|
||||||
def get_systemd_running():
|
def get_systemd_running():
|
||||||
lines = shell("systemctl --type=service --state=running").split("\n")
|
lines = shell("systemctl --type=service --state=running").split("\n")
|
||||||
return [line for line in lines if line.startswith(" ")]
|
return [line for line in lines if line.startswith(" ")]
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ def bootstrap_remote(gateway, remote=remote):
|
|||||||
|
|
||||||
def print_stderr(item="", end="\n"):
|
def print_stderr(item="", end="\n"):
|
||||||
print(item, file=sys.stderr, end=end)
|
print(item, file=sys.stderr, end=end)
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
|
|
||||||
class SSHExec:
|
class SSHExec:
|
||||||
@@ -71,6 +70,10 @@ class SSHExec:
|
|||||||
raise self.FuncError(data)
|
raise self.FuncError(data)
|
||||||
|
|
||||||
def logged(self, call, kwargs):
|
def logged(self, call, kwargs):
|
||||||
|
def log_progress(data):
|
||||||
|
sys.stderr.write(".")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
title = call.__doc__
|
title = call.__doc__
|
||||||
if not title:
|
if not title:
|
||||||
title = call.__name__
|
title = call.__name__
|
||||||
@@ -79,6 +82,6 @@ class SSHExec:
|
|||||||
return self(call, kwargs, log_callback=print_stderr)
|
return self(call, kwargs, log_callback=print_stderr)
|
||||||
else:
|
else:
|
||||||
print_stderr(title, end="")
|
print_stderr(title, end="")
|
||||||
res = self(call, kwargs, log_callback=remote.rshell.log_progress)
|
res = self(call, kwargs, log_callback=log_progress)
|
||||||
print_stderr()
|
print_stderr()
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class TestSSHExecutor:
|
|||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert err.startswith("Collecting")
|
assert err.startswith("Collecting")
|
||||||
#assert err.endswith("....\n")
|
assert err.endswith("....\n")
|
||||||
assert err.count("\n") == 1
|
assert err.count("\n") == 1
|
||||||
|
|
||||||
sshexec.verbose = True
|
sshexec.verbose = True
|
||||||
@@ -40,7 +40,7 @@ class TestSSHExecutor:
|
|||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
lines = err.split("\n")
|
lines = err.split("\n")
|
||||||
#assert len(lines) > 4
|
assert len(lines) > 4
|
||||||
assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
|
assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
|
||||||
|
|
||||||
def test_exception(self, sshexec, capsys):
|
def test_exception(self, sshexec, capsys):
|
||||||
|
|||||||
@@ -89,14 +89,18 @@ class TestZonefileChecks:
|
|||||||
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
||||||
zonefile = cm_data.get("zftest.zone")
|
zonefile = cm_data.get("zftest.zone")
|
||||||
parse_zonefile_into_dict(zonefile, mockdns_base)
|
parse_zonefile_into_dict(zonefile, mockdns_base)
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
|
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
||||||
|
zonefile, "some.domain"
|
||||||
|
)
|
||||||
assert not required_diff and not recommended_diff
|
assert not required_diff and not recommended_diff
|
||||||
|
|
||||||
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
||||||
zonefile = cm_data.get("zftest.zone")
|
zonefile = cm_data.get("zftest.zone")
|
||||||
zonefile_mocked = zonefile.split("; Recommended")[0]
|
zonefile_mocked = zonefile.split("; Recommended")[0]
|
||||||
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(zonefile)
|
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
||||||
|
zonefile, "some.domain"
|
||||||
|
)
|
||||||
assert not required_diff
|
assert not required_diff
|
||||||
assert len(recommended_diff) == 8
|
assert len(recommended_diff) == 8
|
||||||
|
|
||||||
|
|||||||
@@ -11,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 = {}
|
||||||
@@ -22,12 +29,59 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -80,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()
|
||||||
@@ -87,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
|
||||||
@@ -110,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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