mirror of
https://github.com/chatmail/relay.git
synced 2026-05-14 01:44:38 +00:00
Compare commits
1 Commits
link2xt/do
...
link2xt/po
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e08ee25532 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -2,30 +2,15 @@
|
|||||||
|
|
||||||
## untagged
|
## untagged
|
||||||
|
|
||||||
- fix checking for required DNS records
|
|
||||||
([#412](https://github.com/deltachat/chatmail/pull/412))
|
|
||||||
|
|
||||||
- add a paragraph about "account deletion" to info page
|
|
||||||
([#405](https://github.com/deltachat/chatmail/pull/405))
|
|
||||||
|
|
||||||
- avoid nginx listening on ipv6 if v6 is dsiabled
|
- avoid nginx listening on ipv6 if v6 is dsiabled
|
||||||
([#402](https://github.com/deltachat/chatmail/pull/402))
|
([#402](https://github.com/deltachat/chatmail/pull/402))
|
||||||
|
|
||||||
- refactor ssh-based execution to allow organizing remote functions in
|
|
||||||
modules.
|
|
||||||
([#396](https://github.com/deltachat/chatmail/pull/396))
|
|
||||||
|
|
||||||
- trigger "apt upgrade" during "cmdeploy run"
|
- trigger "apt upgrade" during "cmdeploy run"
|
||||||
([#398](https://github.com/deltachat/chatmail/pull/398))
|
([#398](https://github.com/deltachat/chatmail/pull/398))
|
||||||
|
|
||||||
- drop hispanilandia passthrough address
|
- drop hispanilandia passthrough address
|
||||||
([#401](https://github.com/deltachat/chatmail/pull/401))
|
([#401](https://github.com/deltachat/chatmail/pull/401))
|
||||||
|
|
||||||
- set CAA record flags to 0
|
|
||||||
|
|
||||||
- add IMAP capabilities instead of overwriting them
|
|
||||||
([#413](https://github.com/deltachat/chatmail/pull/413))
|
|
||||||
|
|
||||||
|
|
||||||
## 1.4.1 2024-07-31
|
## 1.4.1 2024-07-31
|
||||||
|
|
||||||
|
|||||||
119
README.md
119
README.md
@@ -34,8 +34,8 @@ Please substitute it with your own domain.
|
|||||||
scripts/cmdeploy init chat.example.org # <-- use your domain
|
scripts/cmdeploy init chat.example.org # <-- use your domain
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Point your domain to the server's IP address,
|
3. Setup first DNS records for your chatmail domain,
|
||||||
if you haven't done so already.
|
according to the hints provided by `cmdeploy init`.
|
||||||
Verify that SSH root login works:
|
Verify that SSH root login works:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -47,8 +47,7 @@ Please substitute it with your own domain.
|
|||||||
```
|
```
|
||||||
scripts/cmdeploy run
|
scripts/cmdeploy run
|
||||||
```
|
```
|
||||||
This script will check that you have all necessary DNS records.
|
This script will also show you additional DNS records
|
||||||
If DNS records are missing, it will recommend
|
|
||||||
which you should configure at your DNS provider
|
which you should configure at your DNS provider
|
||||||
(it can take some time until they are public).
|
(it can take some time until they are public).
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ To check the status of your remotely running chatmail service:
|
|||||||
scripts/cmdeploy status
|
scripts/cmdeploy status
|
||||||
```
|
```
|
||||||
|
|
||||||
To display and check all recommended DNS records:
|
To check whether your DNS records are correct:
|
||||||
|
|
||||||
```
|
```
|
||||||
scripts/cmdeploy dns
|
scripts/cmdeploy dns
|
||||||
@@ -187,113 +186,3 @@ to MAIL FROM with
|
|||||||
and rejects incorrectly authenticated emails with [`reject_sender_login_mismatch`](reject_sender_login_mismatch) policy.
|
and rejects incorrectly authenticated emails with [`reject_sender_login_mismatch`](reject_sender_login_mismatch) policy.
|
||||||
`From:` header must correspond to envelope MAIL FROM,
|
`From:` header must correspond to envelope MAIL FROM,
|
||||||
this is ensured by `filtermail` proxy.
|
this is ensured by `filtermail` proxy.
|
||||||
|
|
||||||
## Migrating chatmail server to a new host
|
|
||||||
|
|
||||||
If you want to migrate your chatmail server to a new host,
|
|
||||||
follow these steps:
|
|
||||||
|
|
||||||
1. Block all ports except 80 and 22 with firewall on a new server.
|
|
||||||
|
|
||||||
To do this, add the following config to `/etc/nftables.conf`:
|
|
||||||
```
|
|
||||||
#!/usr/sbin/nft -f
|
|
||||||
|
|
||||||
flush ruleset
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
tcp dport { ssh, http } accept
|
|
||||||
|
|
||||||
ct state established accept
|
|
||||||
}
|
|
||||||
chain forward {
|
|
||||||
type filter hook forward priority filter;
|
|
||||||
}
|
|
||||||
chain output {
|
|
||||||
type filter hook output priority filter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Then execute `nft -f /etc/nftables.conf` as root.
|
|
||||||
|
|
||||||
This will ensure users will not connect to the new server
|
|
||||||
and mails will not be delivered to the new server
|
|
||||||
before you finish the setup.
|
|
||||||
|
|
||||||
Port 22 is needed for SSH access
|
|
||||||
and port 80 is needed to get a TLS certificate.
|
|
||||||
They are not used by Delta Chat
|
|
||||||
or by other email servers trying to deliver the messages.
|
|
||||||
|
|
||||||
2. Point DNS to the new IP addresses.
|
|
||||||
|
|
||||||
You can already remove the old IP addresses from DNS.
|
|
||||||
Existing Delta Chat users will still be able to connect
|
|
||||||
to the old server, send and receive messages,
|
|
||||||
but new users will fail to create new profiles
|
|
||||||
with your chatmail server.
|
|
||||||
|
|
||||||
3. Setup the new server with `cmdeploy`.
|
|
||||||
|
|
||||||
This step is similar to initial setup.
|
|
||||||
However, because ports Delta Chat uses are blocked,
|
|
||||||
new server will not become usable immediately.
|
|
||||||
If other servers try to deliver messages to your new server they will fail,
|
|
||||||
but normally email servers will retry delivering messages
|
|
||||||
for at least a week, so messages will not be lost.
|
|
||||||
|
|
||||||
4. Firewall all ports except `ssh` (22) on the old server.
|
|
||||||
Existing users will not be able to connect from now on
|
|
||||||
and no more messages will be delivered to your old chatmail server.
|
|
||||||
|
|
||||||
Blocking users from connecting to the new server
|
|
||||||
until mailboxes are migrated is needed to avoid UID validity change.
|
|
||||||
If Delta Chat connects to the new server before it is fully set up,
|
|
||||||
it will lose track of the IMAP message UID
|
|
||||||
and miss messages that arrived during migration.
|
|
||||||
|
|
||||||
Same for SMTP port 25, you want it blocked during migration so no new mails arrive
|
|
||||||
while the server is moving.
|
|
||||||
|
|
||||||
5. Use `rsync -avz` over SSH to copy /home/vmail/mail from the old server to the new one
|
|
||||||
preserving file permissions and timestamps.
|
|
||||||
|
|
||||||
6. Unblock ports used by Delta Chat and SMTP message exchange.
|
|
||||||
For that you can modify `/etc/nftables.conf` as follows:
|
|
||||||
```
|
|
||||||
#!/usr/sbin/nft -f
|
|
||||||
|
|
||||||
flush ruleset
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
tcp dport { ssh, smtp, http, https, imap, imaps, submission, submissions } accept
|
|
||||||
|
|
||||||
ct state established accept
|
|
||||||
}
|
|
||||||
chain forward {
|
|
||||||
type filter hook forward priority filter;
|
|
||||||
}
|
|
||||||
chain output {
|
|
||||||
type filter hook output priority filter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Execute `nft -f /etc/nftables.conf` as root to apply the changes.
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ max_message_size = 31457280
|
|||||||
# days after which mails are unconditionally deleted
|
# days after which mails are unconditionally deleted
|
||||||
delete_mails_after = 20
|
delete_mails_after = 20
|
||||||
|
|
||||||
# days after which users without a successful login are deleted (database and mails)
|
# days after which users without a login are deleted (database and mails)
|
||||||
delete_inactive_users_after = 90
|
delete_inactive_users_after = 100
|
||||||
|
|
||||||
# minimum length a username must have
|
# minimum length a username must have
|
||||||
username_min_length = 9
|
username_min_length = 9
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ www.{{ mail_domain }}. CNAME {{ mail_domain }}.
|
|||||||
_dmarc.{{ mail_domain }}. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
_dmarc.{{ mail_domain }}. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
||||||
|
|
||||||
{% if acme_account_url %}
|
{% if acme_account_url %}
|
||||||
{{ mail_domain }}. CAA 0 issue "letsencrypt.org;accounturi={{ acme_account_url }}"
|
{{ mail_domain }}. CAA 128 issue "letsencrypt.org;accounturi={{ acme_account_url }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
_adsp._domainkey.{{ mail_domain }}. TXT "dkim=discardable"
|
_adsp._domainkey.{{ mail_domain }}. TXT "dkim=discardable"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from chatmaild.config import read_config, write_initial_config
|
|||||||
from packaging import version
|
from packaging import version
|
||||||
from termcolor import colored
|
from termcolor import colored
|
||||||
|
|
||||||
from . import dns, remote
|
from . import dns, remote_funcs
|
||||||
from .sshexec import SSHExec
|
from .sshexec import SSHExec
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -132,7 +132,7 @@ def status_cmd(args, out):
|
|||||||
else:
|
else:
|
||||||
out.red("no privacy settings")
|
out.red("no privacy settings")
|
||||||
|
|
||||||
for line in sshexec(remote.rshell.get_systemd_running):
|
for line in sshexec(remote_funcs.get_systemd_running):
|
||||||
print(line)
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ def main(args=None):
|
|||||||
|
|
||||||
def get_sshexec():
|
def get_sshexec():
|
||||||
print(f"[ssh] login to {args.config.mail_domain}")
|
print(f"[ssh] login to {args.config.mail_domain}")
|
||||||
return SSHExec(args.config.mail_domain, verbose=args.verbose)
|
return SSHExec(args.config.mail_domain, remote_funcs, verbose=args.verbose)
|
||||||
|
|
||||||
args.get_sshexec = get_sshexec
|
args.get_sshexec = get_sshexec
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import importlib
|
|||||||
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
|
||||||
from . import remote
|
from . import remote_funcs
|
||||||
|
|
||||||
|
|
||||||
def get_initial_remote_data(sshexec, mail_domain):
|
def get_initial_remote_data(sshexec, mail_domain):
|
||||||
return sshexec.logged(
|
return sshexec.logged(
|
||||||
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
call=remote_funcs.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -16,12 +16,9 @@ def check_initial_remote_data(remote_data, print=print):
|
|||||||
mail_domain = remote_data["mail_domain"]
|
mail_domain = remote_data["mail_domain"]
|
||||||
if not remote_data["A"] and not remote_data["AAAA"]:
|
if not remote_data["A"] and not remote_data["AAAA"]:
|
||||||
print(f"Missing A and/or AAAA DNS records for {mail_domain}!")
|
print(f"Missing A and/or AAAA DNS records for {mail_domain}!")
|
||||||
elif remote_data["MTA_STS"] != f"{mail_domain}.":
|
elif not remote_data["MTA_STS"]:
|
||||||
print("Missing MTA-STS CNAME record:")
|
print("Missing MTA-STS CNAME record:")
|
||||||
print(f"mta-sts.{mail_domain}. CNAME {mail_domain}.")
|
print(f"mta-sts.{mail_domain}. CNAME {mail_domain}")
|
||||||
elif remote_data["WWW"] != f"{mail_domain}.":
|
|
||||||
print("Missing www CNAME record:")
|
|
||||||
print(f"www.{mail_domain}. CNAME {mail_domain}.")
|
|
||||||
else:
|
else:
|
||||||
return remote_data
|
return remote_data
|
||||||
|
|
||||||
@@ -45,8 +42,7 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
|
|||||||
and return (exitcode, remote_data) tuple."""
|
and return (exitcode, remote_data) tuple."""
|
||||||
|
|
||||||
required_diff, recommended_diff = sshexec.logged(
|
required_diff, recommended_diff = sshexec.logged(
|
||||||
remote.rdns.check_zonefile,
|
remote_funcs.check_zonefile, kwargs=dict(zonefile=zonefile)
|
||||||
kwargs=dict(zonefile=zonefile, mail_domain=remote_data["mail_domain"]),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if required_diff:
|
if required_diff:
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ mail_server_comment = Chatmail server
|
|||||||
# <https://doc.dovecot.org/configuration_manual/quota_plugin/>
|
# <https://doc.dovecot.org/configuration_manual/quota_plugin/>
|
||||||
mail_plugins = zlib quota
|
mail_plugins = zlib quota
|
||||||
|
|
||||||
imap_capability = +XDELTAPUSH XCHATMAIL
|
# these are the capabilities Delta Chat cares about actually
|
||||||
|
# so let's keep the network overhead per login small
|
||||||
|
# https://github.com/deltachat/deltachat-core-rust/blob/master/src/imap/capabilities.rs
|
||||||
|
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY METADATA XDELTAPUSH XCHATMAIL
|
||||||
|
|
||||||
|
|
||||||
# Authentication for system users.
|
# Authentication for system users.
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
<domain>{{ config.domain_name }}</domain>
|
<domain>{{ config.domain_name }}</domain>
|
||||||
<displayName>{{ config.domain_name }} chatmail</displayName>
|
<displayName>{{ config.domain_name }} chatmail</displayName>
|
||||||
<displayShortName>{{ config.domain_name }}</displayShortName>
|
<displayShortName>{{ config.domain_name }}</displayShortName>
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>{{ config.domain_name }}</hostname>
|
||||||
|
<port>443</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</incomingServer>
|
||||||
<incomingServer type="imap">
|
<incomingServer type="imap">
|
||||||
<hostname>{{ config.domain_name }}</hostname>
|
<hostname>{{ config.domain_name }}</hostname>
|
||||||
<port>993</port>
|
<port>993</port>
|
||||||
@@ -19,13 +26,13 @@
|
|||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
</incomingServer>
|
</incomingServer>
|
||||||
<incomingServer type="imap">
|
<outgoingServer type="smtp">
|
||||||
<hostname>{{ config.domain_name }}</hostname>
|
<hostname>{{ config.domain_name }}</hostname>
|
||||||
<port>443</port>
|
<port>443</port>
|
||||||
<socketType>SSL</socketType>
|
<socketType>SSL</socketType>
|
||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
</incomingServer>
|
</outgoingServer>
|
||||||
<outgoingServer type="smtp">
|
<outgoingServer type="smtp">
|
||||||
<hostname>{{ config.domain_name }}</hostname>
|
<hostname>{{ config.domain_name }}</hostname>
|
||||||
<port>465</port>
|
<port>465</port>
|
||||||
@@ -40,12 +47,5 @@
|
|||||||
<authentication>password-cleartext</authentication>
|
<authentication>password-cleartext</authentication>
|
||||||
<username>%EMAILADDRESS%</username>
|
<username>%EMAILADDRESS%</username>
|
||||||
</outgoingServer>
|
</outgoingServer>
|
||||||
<outgoingServer type="smtp">
|
|
||||||
<hostname>{{ config.domain_name }}</hostname>
|
|
||||||
<port>443</port>
|
|
||||||
<socketType>SSL</socketType>
|
|
||||||
<authentication>password-cleartext</authentication>
|
|
||||||
<username>%EMAILADDRESS%</username>
|
|
||||||
</outgoingServer>
|
|
||||||
</emailProvider>
|
</emailProvider>
|
||||||
</clientConfig>
|
</clientConfig>
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
"""
|
|
||||||
|
|
||||||
The 'cmdeploy.remote' sub package contains modules with remotely executing functions.
|
|
||||||
|
|
||||||
Its "_sshexec_bootstrap" module is executed remotely through `SSHExec`
|
|
||||||
and its main() loop there stays connected via a command channel,
|
|
||||||
ready to receive function invocations ("command") and return results.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from . import rdns, rshell
|
|
||||||
|
|
||||||
__all__ = ["rdns", "rshell"]
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import builtins
|
|
||||||
import importlib
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
## Function Execution server
|
|
||||||
|
|
||||||
|
|
||||||
def _run_loop(cmd_channel):
|
|
||||||
while cmd := cmd_channel.receive():
|
|
||||||
cmd_channel.send(_handle_one_request(cmd))
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_one_request(cmd):
|
|
||||||
pymod_path, func_name, kwargs = cmd
|
|
||||||
try:
|
|
||||||
mod = importlib.import_module(pymod_path)
|
|
||||||
func = getattr(mod, func_name)
|
|
||||||
res = func(**kwargs)
|
|
||||||
return ("finish", res)
|
|
||||||
except:
|
|
||||||
data = traceback.format_exc()
|
|
||||||
return ("error", data)
|
|
||||||
|
|
||||||
|
|
||||||
def main(channel):
|
|
||||||
# enable simple "print" logging
|
|
||||||
|
|
||||||
builtins.print = lambda x="": channel.send(("log", x))
|
|
||||||
|
|
||||||
_run_loop(channel)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from subprocess import CalledProcessError, check_output
|
|
||||||
|
|
||||||
|
|
||||||
def shell(command, fail_ok=False):
|
|
||||||
print(f"$ {command}")
|
|
||||||
try:
|
|
||||||
return check_output(command, shell=True).decode().rstrip()
|
|
||||||
except CalledProcessError:
|
|
||||||
if not fail_ok:
|
|
||||||
raise
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def get_systemd_running():
|
|
||||||
lines = shell("systemctl --type=service --state=running").split("\n")
|
|
||||||
return [line for line in lines if line.startswith(" ")]
|
|
||||||
@@ -11,26 +11,40 @@ All functions of this module
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import traceback
|
||||||
|
from subprocess import CalledProcessError, check_output
|
||||||
|
|
||||||
from .rshell import CalledProcessError, shell
|
|
||||||
|
def shell(command, fail_ok=False):
|
||||||
|
print(f"$ {command}")
|
||||||
|
try:
|
||||||
|
return check_output(command, shell=True).decode().rstrip()
|
||||||
|
except CalledProcessError:
|
||||||
|
if not fail_ok:
|
||||||
|
raise
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_systemd_running():
|
||||||
|
lines = shell("systemctl --type=service --state=running").split("\n")
|
||||||
|
return [line for line in lines if line.startswith(" ")]
|
||||||
|
|
||||||
|
|
||||||
def perform_initial_checks(mail_domain):
|
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):
|
|
||||||
shell("apt-get install -y dnsutils")
|
|
||||||
shell(f"unbound-control flush_zone {mail_domain}", fail_ok=True)
|
|
||||||
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}")
|
|
||||||
|
|
||||||
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)
|
||||||
if not MTA_STS or not WWW or (not A and not AAAA):
|
if not MTA_STS or (not A and not AAAA):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
res["acme_account_url"] = shell("acmetool account-url", fail_ok=True)
|
res["acme_account_url"] = shell("acmetool account-url", fail_ok=True)
|
||||||
|
if not shell("dig", fail_ok=True):
|
||||||
|
shell("apt-get install -y dnsutils")
|
||||||
|
shell(f"unbound-control flush_zone {mail_domain}", fail_ok=True)
|
||||||
res["dkim_entry"] = get_dkim_entry(mail_domain, dkim_selector="opendkim")
|
res["dkim_entry"] = get_dkim_entry(mail_domain, dkim_selector="opendkim")
|
||||||
|
|
||||||
# parse out sts-id if exists, example: "v=STSv1; id=2090123"
|
# parse out sts-id if exists, example: "v=STSv1; id=2090123"
|
||||||
@@ -60,9 +74,8 @@ def query_dns(typ, domain):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_zonefile(zonefile, mail_domain):
|
def check_zonefile(zonefile):
|
||||||
"""Check expected zone file entries."""
|
"""Check expected zone file entries."""
|
||||||
shell(f"unbound-control flush_zone {mail_domain}", fail_ok=True)
|
|
||||||
required = True
|
required = True
|
||||||
required_diff = []
|
required_diff = []
|
||||||
recommended_diff = []
|
recommended_diff = []
|
||||||
@@ -86,3 +99,37 @@ def check_zonefile(zonefile, mail_domain):
|
|||||||
recommended_diff.append(zf_line)
|
recommended_diff.append(zf_line)
|
||||||
|
|
||||||
return required_diff, recommended_diff
|
return required_diff, recommended_diff
|
||||||
|
|
||||||
|
|
||||||
|
## Function Execution server
|
||||||
|
|
||||||
|
|
||||||
|
def _run_loop(cmd_channel):
|
||||||
|
while 1:
|
||||||
|
cmd = cmd_channel.receive()
|
||||||
|
if cmd is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
cmd_channel.send(_handle_one_request(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_one_request(cmd):
|
||||||
|
func_name, kwargs = cmd
|
||||||
|
try:
|
||||||
|
res = globals()[func_name](**kwargs)
|
||||||
|
return ("finish", res)
|
||||||
|
except:
|
||||||
|
data = traceback.format_exc()
|
||||||
|
return ("error", data)
|
||||||
|
|
||||||
|
|
||||||
|
# check if this module is executed remotely
|
||||||
|
# and setup a simple serialized function-execution loop
|
||||||
|
|
||||||
|
if __name__ == "__channelexec__":
|
||||||
|
channel = channel # noqa (channel object gets injected)
|
||||||
|
|
||||||
|
# enable simple "print" logging for anyone changing this module
|
||||||
|
globals()["print"] = lambda x="": channel.send(("log", x))
|
||||||
|
|
||||||
|
_run_loop(channel)
|
||||||
@@ -1,45 +1,12 @@
|
|||||||
import inspect
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from queue import Queue
|
|
||||||
|
|
||||||
import execnet
|
import execnet
|
||||||
|
|
||||||
from . import remote
|
|
||||||
|
|
||||||
|
|
||||||
class FuncError(Exception):
|
class FuncError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def bootstrap_remote(gateway, remote=remote):
|
|
||||||
"""Return a command channel which can execute remote functions."""
|
|
||||||
source_init_path = inspect.getfile(remote)
|
|
||||||
basedir = os.path.dirname(source_init_path)
|
|
||||||
name = os.path.basename(basedir)
|
|
||||||
|
|
||||||
# rsync sourcedir to remote host
|
|
||||||
remote_pkg_path = f"/root/from-cmdeploy/{name}"
|
|
||||||
q = Queue()
|
|
||||||
finish = lambda: q.put(None)
|
|
||||||
rsync = execnet.RSync(sourcedir=basedir, verbose=False)
|
|
||||||
rsync.add_target(gateway, remote_pkg_path, finishedcallback=finish, delete=True)
|
|
||||||
rsync.send()
|
|
||||||
q.get()
|
|
||||||
|
|
||||||
# start sshexec bootstrap and return its command channel
|
|
||||||
remote_sys_path = os.path.dirname(remote_pkg_path)
|
|
||||||
channel = gateway.remote_exec(
|
|
||||||
f"""
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, {remote_sys_path!r})
|
|
||||||
from remote._sshexec_bootstrap import main
|
|
||||||
main(channel)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
return channel
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -48,18 +15,16 @@ class SSHExec:
|
|||||||
RemoteError = execnet.RemoteError
|
RemoteError = execnet.RemoteError
|
||||||
FuncError = FuncError
|
FuncError = FuncError
|
||||||
|
|
||||||
def __init__(self, host, verbose=False, python="python3", timeout=60):
|
def __init__(self, host, remote_funcs, verbose=False, python="python3", timeout=60):
|
||||||
self.gateway = execnet.makegateway(f"ssh=root@{host}//python={python}")
|
self.gateway = execnet.makegateway(f"ssh=root@{host}//python={python}")
|
||||||
self._remote_cmdloop_channel = bootstrap_remote(self.gateway, remote)
|
self._remote_cmdloop_channel = self.gateway.remote_exec(remote_funcs)
|
||||||
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):
|
||||||
if kwargs is None:
|
if kwargs is None:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
assert call.__module__.startswith("cmdeploy.remote")
|
self._remote_cmdloop_channel.send((call.__name__, kwargs))
|
||||||
modname = call.__module__.replace("cmdeploy.", "")
|
|
||||||
self._remote_cmdloop_channel.send((modname, call.__name__, kwargs))
|
|
||||||
while 1:
|
while 1:
|
||||||
code, data = self._remote_cmdloop_channel.receive(timeout=self.timeout)
|
code, data = self._remote_cmdloop_channel.receive(timeout=self.timeout)
|
||||||
if log_callback is not None and code == "log":
|
if log_callback is not None and code == "log":
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ _submission._tcp.zftest.testrun.org. SRV 0 1 587 zftest.testrun.org.
|
|||||||
_submissions._tcp.zftest.testrun.org. SRV 0 1 465 zftest.testrun.org.
|
_submissions._tcp.zftest.testrun.org. SRV 0 1 465 zftest.testrun.org.
|
||||||
_imap._tcp.zftest.testrun.org. SRV 0 1 143 zftest.testrun.org.
|
_imap._tcp.zftest.testrun.org. SRV 0 1 143 zftest.testrun.org.
|
||||||
_imaps._tcp.zftest.testrun.org. SRV 0 1 993 zftest.testrun.org.
|
_imaps._tcp.zftest.testrun.org. SRV 0 1 993 zftest.testrun.org.
|
||||||
zftest.testrun.org. CAA 0 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1371472956"
|
zftest.testrun.org. CAA 128 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1371472956"
|
||||||
zftest.testrun.org. TXT "v=spf1 a:zftest.testrun.org ~all"
|
zftest.testrun.org. TXT "v=spf1 a:zftest.testrun.org ~all"
|
||||||
_dmarc.zftest.testrun.org. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
_dmarc.zftest.testrun.org. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
||||||
_adsp._domainkey.zftest.testrun.org. TXT "dkim=discardable"
|
_adsp._domainkey.zftest.testrun.org. TXT "dkim=discardable"
|
||||||
|
|||||||
@@ -2,29 +2,29 @@ import smtplib
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cmdeploy import remote
|
from cmdeploy import remote_funcs
|
||||||
from cmdeploy.sshexec import SSHExec
|
from cmdeploy.sshexec import SSHExec
|
||||||
|
|
||||||
|
|
||||||
class TestSSHExecutor:
|
class TestSSHExecutor:
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def sshexec(self, sshdomain):
|
def sshexec(self, sshdomain):
|
||||||
return SSHExec(sshdomain)
|
return SSHExec(sshdomain, remote_funcs)
|
||||||
|
|
||||||
def test_ls(self, sshexec):
|
def test_ls(self, sshexec):
|
||||||
out = sshexec(call=remote.rdns.shell, kwargs=dict(command="ls"))
|
out = sshexec(call=remote_funcs.shell, kwargs=dict(command="ls"))
|
||||||
out2 = sshexec(call=remote.rdns.shell, kwargs=dict(command="ls"))
|
out2 = sshexec(call=remote_funcs.shell, kwargs=dict(command="ls"))
|
||||||
assert out == out2
|
assert out == out2
|
||||||
|
|
||||||
def test_perform_initial(self, sshexec, maildomain):
|
def test_perform_initial(self, sshexec, maildomain):
|
||||||
res = sshexec(
|
res = sshexec(
|
||||||
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
remote_funcs.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
||||||
)
|
)
|
||||||
assert res["A"] or res["AAAA"]
|
assert res["A"] or res["AAAA"]
|
||||||
|
|
||||||
def test_logged(self, sshexec, maildomain, capsys):
|
def test_logged(self, sshexec, maildomain, capsys):
|
||||||
sshexec.logged(
|
sshexec.logged(
|
||||||
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
remote_funcs.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
||||||
)
|
)
|
||||||
out, err = capsys.readouterr()
|
out, err = capsys.readouterr()
|
||||||
assert err.startswith("Collecting")
|
assert err.startswith("Collecting")
|
||||||
@@ -33,21 +33,21 @@ class TestSSHExecutor:
|
|||||||
|
|
||||||
sshexec.verbose = True
|
sshexec.verbose = True
|
||||||
sshexec.logged(
|
sshexec.logged(
|
||||||
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
remote_funcs.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
|
||||||
)
|
)
|
||||||
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_funcs.perform_initial_checks.__doc__ in lines[0]
|
||||||
|
|
||||||
def test_exception(self, sshexec, capsys):
|
def test_exception(self, sshexec, capsys):
|
||||||
try:
|
try:
|
||||||
sshexec.logged(
|
sshexec.logged(
|
||||||
remote.rdns.perform_initial_checks,
|
remote_funcs.perform_initial_checks,
|
||||||
kwargs=dict(mail_domain=None),
|
kwargs=dict(mail_domain=None),
|
||||||
)
|
)
|
||||||
except sshexec.FuncError as e:
|
except sshexec.FuncError as e:
|
||||||
assert "rdns.py" in str(e)
|
assert "remote_funcs.py" in str(e)
|
||||||
assert "AssertionError" in str(e)
|
assert "AssertionError" in str(e)
|
||||||
else:
|
else:
|
||||||
pytest.fail("didn't raise exception")
|
pytest.fail("didn't raise exception")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cmdeploy import remote
|
from cmdeploy import remote_funcs
|
||||||
from cmdeploy.dns import check_full_zone, check_initial_remote_data
|
from cmdeploy.dns import check_full_zone, check_initial_remote_data
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ def mockdns_base(monkeypatch):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
monkeypatch.setattr(remote.rdns, query_dns.__name__, query_dns)
|
monkeypatch.setattr(remote_funcs, query_dns.__name__, query_dns)
|
||||||
return qdict
|
return qdict
|
||||||
|
|
||||||
|
|
||||||
@@ -24,10 +24,7 @@ def mockdns(mockdns_base):
|
|||||||
{
|
{
|
||||||
"A": {"some.domain": "1.1.1.1"},
|
"A": {"some.domain": "1.1.1.1"},
|
||||||
"AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"},
|
"AAAA": {"some.domain": "fde5:cd7a:9e1c:3240:5a99:936f:cdac:53ae"},
|
||||||
"CNAME": {
|
"CNAME": {"mta-sts.some.domain": "some.domain"},
|
||||||
"mta-sts.some.domain": "some.domain.",
|
|
||||||
"www.some.domain": "some.domain.",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return mockdns_base
|
return mockdns_base
|
||||||
@@ -35,16 +32,14 @@ def mockdns(mockdns_base):
|
|||||||
|
|
||||||
class TestPerformInitialChecks:
|
class TestPerformInitialChecks:
|
||||||
def test_perform_initial_checks_ok1(self, mockdns):
|
def test_perform_initial_checks_ok1(self, mockdns):
|
||||||
remote_data = remote.rdns.perform_initial_checks("some.domain")
|
remote_data = remote_funcs.perform_initial_checks("some.domain")
|
||||||
assert remote_data["A"] == mockdns["A"]["some.domain"]
|
assert len(remote_data) == 7
|
||||||
assert remote_data["AAAA"] == mockdns["AAAA"]["some.domain"]
|
|
||||||
assert remote_data["MTA_STS"] == mockdns["CNAME"]["mta-sts.some.domain"]
|
|
||||||
assert remote_data["WWW"] == mockdns["CNAME"]["www.some.domain"]
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("drop", ["A", "AAAA"])
|
@pytest.mark.parametrize("drop", ["A", "AAAA"])
|
||||||
def test_perform_initial_checks_with_one_of_A_AAAA(self, mockdns, drop):
|
def test_perform_initial_checks_with_one_of_A_AAAA(self, mockdns, drop):
|
||||||
del mockdns[drop]
|
del mockdns[drop]
|
||||||
remote_data = remote.rdns.perform_initial_checks("some.domain")
|
remote_data = remote_funcs.perform_initial_checks("some.domain")
|
||||||
|
assert len(remote_data) == 7
|
||||||
assert not remote_data[drop]
|
assert not remote_data[drop]
|
||||||
|
|
||||||
l = []
|
l = []
|
||||||
@@ -53,8 +48,9 @@ class TestPerformInitialChecks:
|
|||||||
assert not l
|
assert not l
|
||||||
|
|
||||||
def test_perform_initial_checks_no_mta_sts(self, mockdns):
|
def test_perform_initial_checks_no_mta_sts(self, mockdns):
|
||||||
del mockdns["CNAME"]["mta-sts.some.domain"]
|
del mockdns["CNAME"]
|
||||||
remote_data = remote.rdns.perform_initial_checks("some.domain")
|
remote_data = remote_funcs.perform_initial_checks("some.domain")
|
||||||
|
assert len(remote_data) == 4
|
||||||
assert not remote_data["MTA_STS"]
|
assert not remote_data["MTA_STS"]
|
||||||
|
|
||||||
l = []
|
l = []
|
||||||
@@ -89,18 +85,14 @@ class TestZonefileChecks:
|
|||||||
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
def test_check_zonefile_all_ok(self, cm_data, mockdns_base):
|
||||||
zonefile = cm_data.get("zftest.zone")
|
zonefile = cm_data.get("zftest.zone")
|
||||||
parse_zonefile_into_dict(zonefile, mockdns_base)
|
parse_zonefile_into_dict(zonefile, mockdns_base)
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
required_diff, recommended_diff = remote_funcs.check_zonefile(zonefile)
|
||||||
zonefile, "some.domain"
|
|
||||||
)
|
|
||||||
assert not required_diff and not recommended_diff
|
assert not required_diff and not recommended_diff
|
||||||
|
|
||||||
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
def test_check_zonefile_recommended_not_set(self, cm_data, mockdns_base):
|
||||||
zonefile = cm_data.get("zftest.zone")
|
zonefile = cm_data.get("zftest.zone")
|
||||||
zonefile_mocked = zonefile.split("; Recommended")[0]
|
zonefile_mocked = zonefile.split("; Recommended")[0]
|
||||||
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
parse_zonefile_into_dict(zonefile_mocked, mockdns_base)
|
||||||
required_diff, recommended_diff = remote.rdns.check_zonefile(
|
required_diff, recommended_diff = remote_funcs.check_zonefile(zonefile)
|
||||||
zonefile, "some.domain"
|
|
||||||
)
|
|
||||||
assert not required_diff
|
assert not required_diff
|
||||||
assert len(recommended_diff) == 8
|
assert len(recommended_diff) == 8
|
||||||
|
|
||||||
@@ -109,7 +101,6 @@ class TestZonefileChecks:
|
|||||||
zonefile_mocked = zonefile.split("; Recommended")[0]
|
zonefile_mocked = zonefile.split("; Recommended")[0]
|
||||||
parse_zonefile_into_dict(zonefile_mocked, mockdns_base, only_required=True)
|
parse_zonefile_into_dict(zonefile_mocked, mockdns_base, only_required=True)
|
||||||
mssh = MockSSHExec()
|
mssh = MockSSHExec()
|
||||||
mockdns_base["mail_domain"] = "some.domain"
|
|
||||||
res = check_full_zone(mssh, mockdns_base, out=mockout, zonefile=zonefile)
|
res = check_full_zone(mssh, mockdns_base, out=mockout, zonefile=zonefile)
|
||||||
assert res == 0
|
assert res == 0
|
||||||
assert "WARNING" in mockout.captured_plain[0]
|
assert "WARNING" in mockout.captured_plain[0]
|
||||||
@@ -119,7 +110,6 @@ class TestZonefileChecks:
|
|||||||
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)
|
||||||
mssh = MockSSHExec()
|
mssh = MockSSHExec()
|
||||||
mockdns_base["mail_domain"] = "some.domain"
|
|
||||||
res = check_full_zone(mssh, mockdns_base, out=mockout, zonefile=zonefile)
|
res = check_full_zone(mssh, mockdns_base, out=mockout, zonefile=zonefile)
|
||||||
assert res == 0
|
assert res == 0
|
||||||
assert not mockout.captured_red
|
assert not mockout.captured_red
|
||||||
|
|||||||
@@ -43,20 +43,6 @@ The first login sets your password.
|
|||||||
- 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).
|
- 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).
|
||||||
|
|
||||||
|
|
||||||
### <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?
|
### Who are the operators? Which software is running?
|
||||||
|
|
||||||
This chatmail provider is run by a small voluntary group of devs and sysadmins,
|
This chatmail provider is run by a small voluntary group of devs and sysadmins,
|
||||||
|
|||||||
Reference in New Issue
Block a user