mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
Compare commits
1 Commits
link2xt/no
...
fix_metric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a994a1b950 |
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -33,5 +33,8 @@ jobs:
|
||||
- name: run deploy-chatmail offline tests
|
||||
run: pytest --pyargs cmdeploy
|
||||
|
||||
- name: initialize with chatmail domain
|
||||
run: cmdeploy init chat.example.org
|
||||
|
||||
# all other cmdeploy commands require a staging server
|
||||
# see https://github.com/deltachat/chatmail/issues/100
|
||||
|
||||
42
README.md
42
README.md
@@ -15,8 +15,8 @@ after which the initially specified password is required for using them.
|
||||
|
||||
## Deploying your own chatmail server
|
||||
|
||||
We use `chat.example.org` as the chatmail domain in the following steps.
|
||||
Please substitute it with your own domain.
|
||||
We subsequently use `CHATMAIL_DOMAIN` as a placeholder for your fully qualified
|
||||
DNS domain name (FQDN), for example `chat.example.org`.
|
||||
|
||||
1. Install the `cmdeploy` command in a virtualenv
|
||||
|
||||
@@ -25,19 +25,19 @@ Please substitute it with your own domain.
|
||||
cd chatmail
|
||||
scripts/initenv.sh
|
||||
```
|
||||
|
||||
|
||||
2. Create chatmail configuration file `chatmail.ini`:
|
||||
|
||||
```
|
||||
scripts/cmdeploy init chat.example.org # <-- use your domain
|
||||
scripts/cmdeploy init CHATMAIL_DOMAIN
|
||||
```
|
||||
|
||||
3. Setup first DNS records for your chatmail domain,
|
||||
3. Setup first DNS records for your `CHATMAIL_DOMAIN`,
|
||||
according to the hints provided by `cmdeploy init`.
|
||||
Verify that SSH root login works:
|
||||
|
||||
```
|
||||
ssh root@chat.example.org # <-- use your domain
|
||||
ssh root@CHATMAIL_DOMAIN
|
||||
```
|
||||
|
||||
4. Deploy to the remote chatmail server:
|
||||
@@ -45,9 +45,13 @@ Please substitute it with your own domain.
|
||||
```
|
||||
scripts/cmdeploy run
|
||||
```
|
||||
This script will also show you additional DNS records
|
||||
which you should configure at your DNS provider
|
||||
(it can take some time until they are public).
|
||||
|
||||
5. To output a DNS zone file from which you can transfer DNS records
|
||||
to your DNS provider:
|
||||
|
||||
```
|
||||
scripts/cmdeploy dns
|
||||
```
|
||||
|
||||
### Other helpful commands:
|
||||
|
||||
@@ -57,12 +61,6 @@ To check the status of your remotely running chatmail service:
|
||||
scripts/cmdeploy status
|
||||
```
|
||||
|
||||
To check whether your DNS records are correct:
|
||||
|
||||
```
|
||||
scripts/cmdeploy dns
|
||||
```
|
||||
|
||||
To test whether your chatmail service is working correctly:
|
||||
|
||||
```
|
||||
@@ -77,7 +75,7 @@ scripts/cmdeploy bench
|
||||
|
||||
## Overview of this repository
|
||||
|
||||
This repository drives the development of chatmail services,
|
||||
This repository drives the development of "chatmail instances",
|
||||
comprised of minimal setups of
|
||||
|
||||
- [postfix smtp server](https://www.postfix.org)
|
||||
@@ -93,7 +91,7 @@ as well as custom services that are integrated with these two:
|
||||
to send mails for them.
|
||||
|
||||
- `chatmaild/src/chatmaild/filtermail.py` prevents
|
||||
unencrypted e-mail from leaving the chatmail service
|
||||
unencrypted e-mail from leaving the chatmail instance
|
||||
and is integrated into postfix's outbound mail pipelines.
|
||||
|
||||
There is also the `cmdeploy/src/cmdeploy/cmdeploy.py` command line tool
|
||||
@@ -106,7 +104,7 @@ to automatically install all chatmail components on a server.
|
||||
### Home page and getting started for users
|
||||
|
||||
`cmdeploy run` also creates default static Web pages and deploys them
|
||||
to a nginx web server with:
|
||||
to an nginx web server under `https://CHATMAIL_DOMAIN`.
|
||||
|
||||
- a default `index.html` along with a QR code that users can click to
|
||||
create accounts on your chatmail provider,
|
||||
@@ -151,12 +149,10 @@ While this file is present, account creation will be blocked.
|
||||
|
||||
### Ports
|
||||
|
||||
[Postfix](http://www.postfix.org/) listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
|
||||
[Dovecot](https://www.dovecot.org/) listens on ports 143 (imap) and 993 (imaps).
|
||||
[nginx](https://www.nginx.com/) listens on port 443 (https).
|
||||
[acmetool](https://hlandau.github.io/acmetool/) listens on port 80 (http).
|
||||
Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
|
||||
Dovecot listens on ports 143(imap) and 993 (imaps).
|
||||
|
||||
Delta Chat apps will, however, discover all ports and configurations
|
||||
automatically by reading the [autoconfig XML file](https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html) from the chatmail service.
|
||||
automatically by reading the `autoconfig.xml` file from the chatmail service.
|
||||
|
||||
|
||||
|
||||
@@ -91,56 +91,23 @@ def lookup_passdb(db, config: Config, user, cleartext_password):
|
||||
VALUES (?, ?, ?)"""
|
||||
conn.execute(q, (user, encrypted_password, int(time.time())))
|
||||
return dict(
|
||||
home=f"/home/vmail/mail/{config.mail_domain}/{user}",
|
||||
home=f"/home/vmail/{user}",
|
||||
uid="vmail",
|
||||
gid="vmail",
|
||||
password=encrypted_password,
|
||||
)
|
||||
|
||||
|
||||
def split_and_unescape(s):
|
||||
"""Split strings using double quote as a separator and backslash as escape character
|
||||
into parts."""
|
||||
|
||||
out = ""
|
||||
i = 0
|
||||
while i < len(s):
|
||||
c = s[i]
|
||||
if c == "\\":
|
||||
# Skip escape character.
|
||||
i += 1
|
||||
|
||||
# This will raise IndexError if there is no character
|
||||
# after escape character. This is expected
|
||||
# as this is an invalid input.
|
||||
out += s[i]
|
||||
elif c == '"':
|
||||
# Separator
|
||||
yield out
|
||||
out = ""
|
||||
else:
|
||||
out += c
|
||||
i += 1
|
||||
yield out
|
||||
|
||||
|
||||
def handle_dovecot_request(msg, db, config: Config):
|
||||
short_command = msg[0]
|
||||
if short_command == "L": # LOOKUP
|
||||
parts = msg[1:].split("\t")
|
||||
|
||||
# Dovecot <2.3.17 has only one part,
|
||||
# do not attempt to read any other parts for compatibility.
|
||||
keyname = parts[0]
|
||||
|
||||
namespace, type, args = keyname.split("/", 2)
|
||||
args = list(split_and_unescape(args))
|
||||
|
||||
keyname, user = parts[:2]
|
||||
namespace, type, *args = keyname.split("/")
|
||||
reply_command = "F"
|
||||
res = ""
|
||||
if namespace == "shared":
|
||||
if type == "userdb":
|
||||
user = args[0]
|
||||
if user.endswith(f"@{config.mail_domain}"):
|
||||
res = lookup_userdb(db, user)
|
||||
if res:
|
||||
@@ -148,7 +115,6 @@ def handle_dovecot_request(msg, db, config: Config):
|
||||
else:
|
||||
reply_command = "N"
|
||||
elif type == "passdb":
|
||||
user = args[1]
|
||||
if user.endswith(f"@{config.mail_domain}"):
|
||||
res = lookup_passdb(db, config, user, cleartext_password=args[0])
|
||||
if res:
|
||||
|
||||
@@ -6,6 +6,7 @@ it will echo back any message that has non-empty text and also supports the /hel
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||
|
||||
@@ -75,7 +76,10 @@ def main():
|
||||
config = read_config(sys.argv[1])
|
||||
password = create_newemail_dict(config).get("password")
|
||||
email = "echo@" + config.mail_domain
|
||||
bot.configure(email, password)
|
||||
configure_thread = Thread(
|
||||
target=bot.configure, kwargs={"email": email, "password": password}
|
||||
)
|
||||
configure_thread.start()
|
||||
bot.run_forever()
|
||||
|
||||
|
||||
|
||||
@@ -52,19 +52,15 @@ def test_too_high_db_version(db):
|
||||
|
||||
|
||||
def test_handle_dovecot_request(db, example_config):
|
||||
# Test that password can contain ", ', \ and /
|
||||
msg = (
|
||||
'Lshared/passdb/laksjdlaksjdlak\\\\sjdlk\\"12j\\\'3l1/k2j3123"'
|
||||
"Lshared/passdb/laksjdlaksjdlaksjdlk12j3l1k2j3123/"
|
||||
"some42123@chat.example.org\tsome42123@chat.example.org"
|
||||
)
|
||||
res = handle_dovecot_request(msg, db, example_config)
|
||||
assert res
|
||||
assert res[0] == "O" and res.endswith("\n")
|
||||
userdata = json.loads(res[1:].strip())
|
||||
assert (
|
||||
userdata["home"]
|
||||
== "/home/vmail/mail/chat.example.org/some42123@chat.example.org"
|
||||
)
|
||||
assert userdata["home"] == "/home/vmail/some42123@chat.example.org"
|
||||
assert userdata["uid"] == userdata["gid"] == "vmail"
|
||||
assert userdata["password"].startswith("{SHA512-CRYPT}")
|
||||
|
||||
|
||||
@@ -254,15 +254,6 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
|
||||
)
|
||||
need_restart |= master_config.changed
|
||||
|
||||
header_cleanup = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("postfix/submission_header_cleanup"),
|
||||
dest="/etc/postfix/submission_header_cleanup",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= header_cleanup.changed
|
||||
|
||||
return need_restart
|
||||
|
||||
|
||||
@@ -408,11 +399,7 @@ def deploy_chatmail(config_path: Path) -> None:
|
||||
# to use 127.0.0.1 as the resolver.
|
||||
apt.packages(
|
||||
name="Install unbound",
|
||||
packages=["unbound", "unbound-anchor", "dnsutils"],
|
||||
)
|
||||
server.shell(
|
||||
name="Generate root keys for validating DNSSEC",
|
||||
commands=["unbound-anchor -a /var/lib/unbound/root.key || true"],
|
||||
packages="unbound",
|
||||
)
|
||||
systemd.service(
|
||||
name="Start and enable unbound",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import importlib.resources
|
||||
|
||||
from pyinfra.operations import apt, files, systemd, server
|
||||
from pyinfra import host
|
||||
from pyinfra.facts.systemd import SystemdStatus
|
||||
from pyinfra.operations import apt, files, server
|
||||
|
||||
|
||||
def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
||||
@@ -48,30 +46,6 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
|
||||
mode="644",
|
||||
)
|
||||
|
||||
service_file = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"acmetool-redirector.service"
|
||||
),
|
||||
dest="/etc/systemd/system/acmetool-redirector.service",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
if host.get_fact(SystemdStatus).get("nginx.service"):
|
||||
systemd.service(
|
||||
name="Stop nginx service to free port 80",
|
||||
service="nginx",
|
||||
running=False,
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
name="Setup acmetool-redirector service",
|
||||
service="acmetool-redirector.service",
|
||||
running=True,
|
||||
enabled=True,
|
||||
restarted=service_file.changed,
|
||||
)
|
||||
|
||||
server.shell(
|
||||
name=f"Request certificate for: { ', '.join(domains) }",
|
||||
commands=[f"acmetool want { ' '.join(domains)}"],
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[Unit]
|
||||
Description=acmetool HTTP redirector
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/acmetool redirector --service.uid=daemon
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,14 +1,12 @@
|
||||
{chatmail_domain}. A {ipv4}
|
||||
{chatmail_domain}. AAAA {ipv6}
|
||||
{chatmail_domain}. MX 10 {chatmail_domain}.
|
||||
_submission._tcp.{chatmail_domain}. SRV 0 1 587 {chatmail_domain}.
|
||||
_submissions._tcp.{chatmail_domain}. SRV 0 1 465 {chatmail_domain}.
|
||||
_imap._tcp.{chatmail_domain}. SRV 0 1 143 {chatmail_domain}.
|
||||
_imaps._tcp.{chatmail_domain}. SRV 0 1 993 {chatmail_domain}.
|
||||
{chatmail_domain}. CAA 128 issue "letsencrypt.org;accounturi={acme_account_url}"
|
||||
{chatmail_domain}. IN CAA 128 issue "letsencrypt.org;accounturi={acme_account_url}"
|
||||
{chatmail_domain}. TXT "v=spf1 a:{chatmail_domain} -all"
|
||||
_dmarc.{chatmail_domain}. TXT "v=DMARC1;p=reject;rua=mailto:{email};ruf=mailto:{email};fo=1;adkim=r;aspf=r"
|
||||
_mta-sts.{chatmail_domain}. TXT "v=STSv1; id={sts_id}"
|
||||
mta-sts.{chatmail_domain}. CNAME {chatmail_domain}.
|
||||
_smtp._tls.{chatmail_domain}. TXT "v=TLSRPTv1;rua=mailto:{email}"
|
||||
_mta-sts.{chatmail_domain}. IN TXT "v=STSv1; id={sts_id}"
|
||||
mta-sts.{chatmail_domain}. IN CNAME {chatmail_domain}.
|
||||
_smtp._tls.{chatmail_domain}. IN TXT "v=TLSRPTv1;rua=mailto:{email}"
|
||||
{dkim_entry}
|
||||
|
||||
@@ -3,6 +3,7 @@ Provides the `cmdeploy` entry point function,
|
||||
along with command line option and subcommand parsing.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import shutil
|
||||
import subprocess
|
||||
import importlib.resources
|
||||
@@ -14,7 +15,6 @@ from pathlib import Path
|
||||
|
||||
from termcolor import colored
|
||||
from chatmaild.config import read_config, write_initial_config
|
||||
from cmdeploy.dns import show_dns, check_necessary_dns
|
||||
|
||||
|
||||
#
|
||||
@@ -32,16 +32,11 @@ def init_cmd_options(parser):
|
||||
|
||||
def init_cmd(args, out):
|
||||
"""Initialize chatmail config file."""
|
||||
mail_domain = args.chatmail_domain
|
||||
if args.inipath.exists():
|
||||
print(f"Path exists, not modifying: {args.inipath}")
|
||||
else:
|
||||
write_initial_config(args.inipath, mail_domain)
|
||||
out.green(f"created config file for {mail_domain} in {args.inipath}")
|
||||
check_necessary_dns(
|
||||
out,
|
||||
mail_domain,
|
||||
)
|
||||
out.red(f"Path exists, not modifying: {args.inipath}")
|
||||
raise SystemExit(1)
|
||||
write_initial_config(args.inipath, args.chatmail_domain)
|
||||
out.green(f"created config file for {args.chatmail_domain} in {args.inipath}")
|
||||
|
||||
|
||||
def run_cmd_options(parser):
|
||||
@@ -55,34 +50,47 @@ def run_cmd_options(parser):
|
||||
|
||||
def run_cmd(args, out):
|
||||
"""Deploy chatmail services on the remote server."""
|
||||
mail_domain = args.config.mail_domain
|
||||
if not check_necessary_dns(
|
||||
out,
|
||||
mail_domain,
|
||||
):
|
||||
sys.exit(1)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["CHATMAIL_INI"] = args.inipath
|
||||
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
|
||||
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
|
||||
cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}"
|
||||
|
||||
out.check_call(cmd, env=env)
|
||||
print("Deploy completed, call `cmdeploy dns` next.")
|
||||
|
||||
|
||||
def dns_cmd_options(parser):
|
||||
parser.add_argument(
|
||||
"--zonefile",
|
||||
dest="zonefile",
|
||||
help="print the whole zonefile for deploying directly",
|
||||
)
|
||||
|
||||
|
||||
def dns_cmd(args, out):
|
||||
"""Generate dns zone file."""
|
||||
show_dns(args, out)
|
||||
template = importlib.resources.files(__package__).joinpath("chatmail.zone.f")
|
||||
ssh = f"ssh root@{args.config.mail_domain}"
|
||||
|
||||
def read_dkim_entries(entry):
|
||||
lines = []
|
||||
for line in entry.split("\n"):
|
||||
if line.startswith(";") or not line.strip():
|
||||
continue
|
||||
line = line.replace("\t", " ")
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
acme_account_url = out.shell_output(f"{ssh} -- acmetool account-url")
|
||||
dkim_entry = read_dkim_entries(out.shell_output(f"{ssh} -- opendkim-genzone -F"))
|
||||
|
||||
out(
|
||||
f"[writing {args.config.mail_domain} zone data (using space as separator) to stdout output]",
|
||||
green=True,
|
||||
)
|
||||
print(
|
||||
template.read_text()
|
||||
.format(
|
||||
acme_account_url=acme_account_url,
|
||||
email=f"root@{args.config.mail_domain}",
|
||||
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
|
||||
chatmail_domain=args.config.mail_domain,
|
||||
dkim_entry=dkim_entry,
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
|
||||
|
||||
def status_cmd(args, out):
|
||||
@@ -211,15 +219,9 @@ class Out:
|
||||
color = "red" if red else ("green" if green else None)
|
||||
print(colored(msg, color), file=file)
|
||||
|
||||
def shell_output(self, arg, no_print=False, timeout=10):
|
||||
if not no_print:
|
||||
self(f"[$ {arg}]", file=sys.stderr)
|
||||
output = subprocess.STDOUT
|
||||
else:
|
||||
output = subprocess.DEVNULL
|
||||
return subprocess.check_output(
|
||||
arg, shell=True, timeout=timeout, stderr=output
|
||||
).decode()
|
||||
def shell_output(self, arg):
|
||||
self(f"[$ {arg}]", file=sys.stderr)
|
||||
return subprocess.check_output(arg, shell=True).decode()
|
||||
|
||||
def check_call(self, arg, env=None, quiet=False):
|
||||
if not quiet:
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
import sys
|
||||
|
||||
import requests
|
||||
import importlib
|
||||
import subprocess
|
||||
import datetime
|
||||
from ipaddress import ip_address
|
||||
|
||||
|
||||
class DNS:
|
||||
def __init__(self, out, mail_domain):
|
||||
self.session = requests.Session()
|
||||
self.out = out
|
||||
self.ssh = f"ssh root@{mail_domain} -- "
|
||||
try:
|
||||
self.shell(f"unbound-control flush_zone {mail_domain}")
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
def shell(self, cmd):
|
||||
try:
|
||||
return self.out.shell_output(f"{self.ssh}{cmd}", no_print=True)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
if "exit status 255" in str(e) or "timed out" in str(e):
|
||||
self.out.red(f"Error: can't reach the server with: {self.ssh[:-4]}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise
|
||||
|
||||
def get_ipv4(self):
|
||||
cmd = "ip a | grep 'inet ' | grep 'scope global' | grep -oE '[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}' | head -1"
|
||||
return self.shell(cmd).strip()
|
||||
|
||||
def get_ipv6(self):
|
||||
cmd = "ip a | grep inet6 | grep 'scope global' | sed -e 's#/64 scope global##' | sed -e 's#inet6##'"
|
||||
return self.shell(cmd).strip()
|
||||
|
||||
def get(self, typ: str, domain: str) -> str | None:
|
||||
"""Get a DNS entry"""
|
||||
dig_result = self.shell(f"dig -r -q {domain} -t {typ} +short")
|
||||
line = dig_result.partition("\n")[0]
|
||||
if line:
|
||||
return line
|
||||
|
||||
def check_ptr_record(self, ip: str, mail_domain) -> bool:
|
||||
"""Check the PTR record for an IPv4 or IPv6 address."""
|
||||
result = self.shell(f"dig -r -x {ip} +short").rstrip()
|
||||
return result == f"{mail_domain}."
|
||||
|
||||
|
||||
def show_dns(args, out):
|
||||
template = importlib.resources.files(__package__).joinpath("chatmail.zone.f")
|
||||
mail_domain = args.config.mail_domain
|
||||
ssh = f"ssh root@{mail_domain}"
|
||||
dns = DNS(out, mail_domain)
|
||||
|
||||
def read_dkim_entries(entry):
|
||||
lines = []
|
||||
for line in entry.split("\n"):
|
||||
if line.startswith(";") or not line.strip():
|
||||
continue
|
||||
line = line.replace("\t", " ")
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
print("Checking your DKIM keys and DNS entries...")
|
||||
try:
|
||||
acme_account_url = out.shell_output(f"{ssh} -- acmetool account-url")
|
||||
except subprocess.CalledProcessError:
|
||||
print("Please run `cmdeploy run` first.")
|
||||
return
|
||||
dkim_entry = read_dkim_entries(out.shell_output(f"{ssh} -- opendkim-genzone -F"))
|
||||
|
||||
ipv6 = dns.get_ipv6()
|
||||
reverse_ipv6 = dns.check_ptr_record(ipv6, mail_domain)
|
||||
ipv4 = dns.get_ipv4()
|
||||
reverse_ipv4 = dns.check_ptr_record(ipv4, mail_domain)
|
||||
to_print = []
|
||||
|
||||
with open(template, "r") as f:
|
||||
zonefile = (
|
||||
f.read()
|
||||
.format(
|
||||
acme_account_url=acme_account_url,
|
||||
email=f"root@{args.config.mail_domain}",
|
||||
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
|
||||
chatmail_domain=args.config.mail_domain,
|
||||
dkim_entry=dkim_entry,
|
||||
ipv6=ipv6,
|
||||
ipv4=ipv4,
|
||||
)
|
||||
.strip()
|
||||
)
|
||||
try:
|
||||
with open(args.zonefile, "w+") as zf:
|
||||
zf.write(zonefile)
|
||||
print(f"DNS records successfully written to: {args.zonefile}")
|
||||
return
|
||||
except TypeError:
|
||||
pass
|
||||
started_dkim_parsing = False
|
||||
for line in zonefile.splitlines():
|
||||
line = line.format(
|
||||
acme_account_url=acme_account_url,
|
||||
email=f"root@{args.config.mail_domain}",
|
||||
sts_id=datetime.datetime.now().strftime("%Y%m%d%H%M"),
|
||||
chatmail_domain=args.config.mail_domain,
|
||||
dkim_entry=dkim_entry,
|
||||
ipv6=ipv6,
|
||||
).strip()
|
||||
for typ in ["A", "AAAA", "CNAME", "CAA"]:
|
||||
if f" {typ} " in line:
|
||||
domain, value = line.split(f" {typ} ")
|
||||
current = dns.get(typ, domain.strip()[:-1])
|
||||
if current != value.strip():
|
||||
to_print.append(line)
|
||||
if " MX " in line:
|
||||
domain, typ, prio, value = line.split()
|
||||
current = dns.get(typ, domain[:-1])
|
||||
if not current:
|
||||
to_print.append(line)
|
||||
elif current.split()[1] != value:
|
||||
print(line.replace(prio, str(int(current[0]) + 1)))
|
||||
if " SRV " in line:
|
||||
domain, typ, prio, weight, port, value = line.split()
|
||||
current = dns.get("SRV", domain[:-1])
|
||||
if current != f"{prio} {weight} {port} {value}":
|
||||
to_print.append(line)
|
||||
if " TXT " in line:
|
||||
domain, value = line.split(" TXT ")
|
||||
current = dns.get("TXT", domain.strip()[:-1])
|
||||
if domain.startswith("_mta-sts."):
|
||||
if current:
|
||||
if current.split("id=")[0] == value.split("id=")[0]:
|
||||
continue
|
||||
if current != value:
|
||||
to_print.append(line)
|
||||
if " IN TXT ( " in line:
|
||||
started_dkim_parsing = True
|
||||
dkim_lines = [line]
|
||||
if started_dkim_parsing and line.startswith('"'):
|
||||
dkim_lines.append(" " + line)
|
||||
domain, data = "\n".join(dkim_lines).split(" IN TXT ")
|
||||
current = dns.get("TXT", domain.strip()[:-1])
|
||||
if current:
|
||||
current = "( %s )" % (current.replace('" "', '"\n "'))
|
||||
if current.replace(";", "\\;") != data:
|
||||
to_print.append(dkim_entry)
|
||||
else:
|
||||
to_print.append(dkim_entry)
|
||||
|
||||
if to_print:
|
||||
to_print.insert(
|
||||
0, "You should configure the following DNS entries at your provider:\n"
|
||||
)
|
||||
to_print.append(
|
||||
"\nIf you already configured the DNS entries, wait a bit until the DNS entries propagate to the Internet."
|
||||
)
|
||||
print("\n".join(to_print))
|
||||
else:
|
||||
out.green("Great! All your DNS entries are correct.")
|
||||
|
||||
to_print = []
|
||||
if not reverse_ipv4:
|
||||
to_print.append(f"\tIPv4:\t{ipv4}\t{args.config.mail_domain}")
|
||||
if not reverse_ipv6:
|
||||
to_print.append(f"\tIPv6:\t{ipv6}\t{args.config.mail_domain}")
|
||||
if len(to_print) > 0:
|
||||
if len(to_print) == 1:
|
||||
warning = "You should add the following PTR/reverse DNS entry:"
|
||||
else:
|
||||
warning = "You should add the following PTR/reverse DNS entries:"
|
||||
out.red(warning)
|
||||
for entry in to_print:
|
||||
print(entry)
|
||||
print(
|
||||
"You can do so at your hosting provider (maybe this isn't your DNS provider)."
|
||||
)
|
||||
|
||||
|
||||
def check_necessary_dns(out, mail_domain):
|
||||
"""Check whether $mail_domain and mta-sts.$mail_domain resolve."""
|
||||
dns = DNS(out, mail_domain)
|
||||
ipv4 = dns.get("A", mail_domain)
|
||||
ipv6 = dns.get("AAAA", mail_domain)
|
||||
mta_entry = dns.get("CNAME", "mta-sts." + mail_domain)
|
||||
mta_ip = dns.get("A", mta_entry)
|
||||
if not mta_ip:
|
||||
mta_ip = dns.get("AAAA", mta_entry)
|
||||
to_print = []
|
||||
if not (ipv4 or ipv6):
|
||||
to_print.append(f"\t{mail_domain}.\t\t\tA<your server's IPv4 address>")
|
||||
if not mta_ip or not (mta_ip == ipv4 or mta_ip == ipv6):
|
||||
to_print.append(f"\tmta-sts.{mail_domain}.\tCNAME\t{mail_domain}.")
|
||||
if to_print:
|
||||
to_print.insert(
|
||||
0,
|
||||
"\nFor chatmail to work, you need to configure this at your DNS provider:\n",
|
||||
)
|
||||
for line in to_print:
|
||||
print(line)
|
||||
print()
|
||||
else:
|
||||
dns.out.green("\nAll necessary DNS entries seem to be set.")
|
||||
return True
|
||||
@@ -1,10 +1,5 @@
|
||||
uri = proxy:/run/dovecot/doveauth.socket:auth
|
||||
iterate_disable = yes
|
||||
default_pass_scheme = plain
|
||||
# %E escapes characters " (double quote), ' (single quote) and \ (backslash) with \ (backslash).
|
||||
# See <https://doc.dovecot.org/configuration_manual/config_file/config_variables/#modifiers>
|
||||
# for documentation.
|
||||
#
|
||||
# We escape user-provided input and use double quote as a separator.
|
||||
password_key = passdb/%Ew"%Eu
|
||||
user_key = userdb/%Eu
|
||||
password_key = passdb/%w/%u
|
||||
user_key = userdb/%u
|
||||
|
||||
@@ -48,4 +48,12 @@ http {
|
||||
# add cgi-bin support
|
||||
include /usr/share/doc/fcgiwrap/examples/nginx.conf;
|
||||
}
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ append_dot_mydomain = no
|
||||
|
||||
readme_directory = no
|
||||
|
||||
# See http://www.postfix.org/COMPATIBILITY_README.html
|
||||
compatibility_level = 3.6
|
||||
# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 2 on
|
||||
# fresh installs.
|
||||
compatibility_level = 2
|
||||
|
||||
# TLS parameters
|
||||
smtpd_tls_cert_file=/var/lib/acme/live/{{ config.mail_domain }}/fullchain
|
||||
@@ -48,5 +49,3 @@ virtual_mailbox_domains = {{ config.mail_domain }}
|
||||
|
||||
smtpd_milters = unix:opendkim/opendkim.sock
|
||||
non_smtpd_milters = $smtpd_milters
|
||||
|
||||
header_checks = regexp:/etc/postfix/submission_header_cleanup
|
||||
|
||||
@@ -14,6 +14,10 @@ smtp inet n - y - - smtpd -v
|
||||
{% else %}
|
||||
smtp inet n - y - - smtpd
|
||||
{% endif %}
|
||||
#smtp inet n - y - 1 postscreen
|
||||
#smtpd pass - - y - - smtpd
|
||||
#dnsblog unix - - y - 0 dnsblog
|
||||
#tlsproxy unix - - y - 0 tlsproxy
|
||||
submission inet n - y - - smtpd
|
||||
-o syslog_name=postfix/submission
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/^Received:/ IGNORE
|
||||
/^X-Originating-IP:/ IGNORE
|
||||
/^X-Mailer:/ IGNORE
|
||||
/^User-Agent:/ IGNORE
|
||||
@@ -2,16 +2,6 @@ import pytest
|
||||
import threading
|
||||
import queue
|
||||
|
||||
from chatmaild.config import read_config
|
||||
from cmdeploy.cmdeploy import main
|
||||
|
||||
|
||||
def test_init(tmp_path, maildomain):
|
||||
inipath = tmp_path.joinpath("chatmail.ini")
|
||||
main(["init", "--config", str(inipath), maildomain])
|
||||
config = read_config(inipath)
|
||||
assert config.mail_domain == maildomain
|
||||
|
||||
|
||||
def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
|
||||
"""Test a) that an initial login creates a user automatically
|
||||
|
||||
@@ -14,12 +14,3 @@ def test_fastcgi_working(maildomain, chatmail_config):
|
||||
res = requests.post(url)
|
||||
assert maildomain in res.json().get("email")
|
||||
assert len(res.json().get("password")) > chatmail_config.password_min_length
|
||||
|
||||
|
||||
def test_newemail_configure(maildomain, rpc):
|
||||
"""Test configuring accounts by scanning a QR code works."""
|
||||
url = f"DCACCOUNT:https://{maildomain}/cgi-bin/newemail.py"
|
||||
for i in range(3):
|
||||
account_id = rpc.add_account()
|
||||
rpc.set_config_from_qr(account_id, url)
|
||||
rpc.configure(account_id)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import time
|
||||
import re
|
||||
import random
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import ipaddress
|
||||
|
||||
|
||||
class TestEndToEndDeltaChat:
|
||||
@@ -122,17 +119,3 @@ class TestEndToEndDeltaChat:
|
||||
for msg in msgs:
|
||||
assert "error" not in m.get_message_info()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_hide_senders_ip_address(cmfactory):
|
||||
public_ip = requests.get("http://icanhazip.com").content.decode().strip()
|
||||
assert ipaddress.ip_address(public_ip)
|
||||
|
||||
user1, user2 = cmfactory.get_online_accounts(2)
|
||||
chat = cmfactory.get_accepted_chat(user1, user2)
|
||||
|
||||
chat.send_text("testing submission header cleanup")
|
||||
user2.wait_next_incoming_message()
|
||||
user2.direct_imap.select_folder("Inbox")
|
||||
msg = user2.direct_imap.get_all_messages()[0]
|
||||
assert public_ip not in msg.obj.as_string()
|
||||
|
||||
@@ -2,6 +2,7 @@ import os
|
||||
|
||||
import pytest
|
||||
from cmdeploy.cmdeploy import get_parser, main
|
||||
from chatmaild.config import read_config
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -20,7 +21,12 @@ class TestCmdline:
|
||||
run = parser.parse_args(["run"])
|
||||
assert init and run
|
||||
|
||||
@pytest.mark.xfail(reason="init doesn't exit anymore, check for CLI output instead")
|
||||
def test_init(self, tmp_path):
|
||||
main(["init", "chat.example.org"])
|
||||
inipath = tmp_path.joinpath("chatmail.ini")
|
||||
config = read_config(inipath)
|
||||
assert config.mail_domain == "chat.example.org"
|
||||
|
||||
def test_init_not_overwrite(self):
|
||||
main(["init", "chat.example.org"])
|
||||
with pytest.raises(SystemExit):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
## More information
|
||||
|
||||
{{ config.mail_domain }} provides a low-maintenance, resource efficient and
|
||||
`nine.testrun.org` 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.
|
||||
|
||||
Reference in New Issue
Block a user