Compare commits

..

50 Commits

Author SHA1 Message Date
link2xt ec22cb3202 Document ports 80 and 443 and add more hyperlinks 2023-12-21 15:56:51 +00:00
missytake 2cf950e901 echo: fail if configure doesn't work 2023-12-21 01:06:23 +01:00
missytake 46d5dbb07d DNS: nicer output for reverse DNS/PTR records. fixes #143 2023-12-20 19:26:50 +01:00
missytake d2e0d1fecc DNS: flush_zone before validating DNS entries. fixes #140 2023-12-20 19:26:50 +01:00
missytake d333cfdd5a lint: fix 1 issue 2023-12-20 19:26:50 +01:00
missytake 32238e99ab tests: testing cmdeploy init only makes sense with a staging server as well now 2023-12-20 19:26:50 +01:00
missytake 40a3a2cc86 tests: make test init work with reachable chatmail_domain 2023-12-20 19:26:50 +01:00
missytake fe978a1971 DNS: increase SSH command's timeout to 10 seconds (the default) 2023-12-20 19:26:50 +01:00
missytake b426c2e7ff DNS: error if can't connect with SSH. fixes #144 2023-12-20 19:26:50 +01:00
missytake b626464453 cmdeploy: fail init and run if SSH doesn't connect 2023-12-20 19:26:50 +01:00
missytake 76c3316f02 cmdeploy init: make output green if DNS is correct 2023-12-19 19:39:49 +01:00
missytake a6a9406228 DNS: making CLI output slightly prettier 2023-12-19 19:39:49 +01:00
missytake 7921f5dd0b DNS: fix some crashes in cmdeploy dns 2023-12-19 19:39:49 +01:00
missytake 39fc9d628f cmdeploy: only run cmdeploy dns after cmdeploy run 2023-12-19 19:39:49 +01:00
link2xt 85a9183b61 Do not call show_dns with run args 2023-12-19 19:39:49 +01:00
missytake 36a4381484 DNS: use local dig if ssh fails 2023-12-19 19:39:49 +01:00
missytake 5ff98a571c DNS: commit hpk's suggestion 2023-12-19 19:39:49 +01:00
missytake 0a91aeb4a3 cmdeploy: simplify check_necessary_dns output 2023-12-19 19:39:49 +01:00
missytake c38f1d7e54 DNS: fix reverse DNS checking 2023-12-19 19:39:49 +01:00
missytake 42bba52f66 README: move cmdeploy dns to additional commands 2023-12-19 19:39:49 +01:00
missytake 03aab4043c DNS: fix CNAME resolving, don't print ssh commands for DNS requests 2023-12-19 19:39:49 +01:00
missytake 146def2f06 cmdeploy: show DNS info at begin and end of cmdeploy run 2023-12-19 19:39:49 +01:00
missytake d642224a73 DNS: flush cache in the beginning 2023-12-19 19:39:49 +01:00
missytake 0238437ce7 DNS: get DNS records with server-side dig 2023-12-19 19:39:49 +01:00
missytake 7ed59ea8bc DNS: move getting IPs to dns.py 2023-12-19 19:39:49 +01:00
missytake 49d0a0bbb0 DNS: fix parsing 2023-12-19 19:39:49 +01:00
missytake 330a034329 DNS: ignore DNS resolvers which don't give us JSON 2023-12-19 19:39:49 +01:00
missytake aee18215fc DNS: Also check A and CNAME entries 2023-12-19 19:39:49 +01:00
missytake 336f87770d cmdeploy: write --zonefile to file 2023-12-19 19:39:49 +01:00
missytake 4199e04ab3 cmdeploy: fixing DNS CLI output 2023-12-19 19:39:49 +01:00
missytake 50922fb1d2 docs: dns doesn't just output a zone file anymore 2023-12-19 19:39:49 +01:00
missytake d2fe417715 DNS: try other resolvers if the first doesn't have it 2023-12-19 19:39:49 +01:00
missytake 2b731bf909 DNS: also add IPv4 entry to zonefile 2023-12-19 19:39:49 +01:00
missytake 2669babb53 DNS: added checks for PTR records 2023-12-19 19:39:49 +01:00
missytake fe675a9a72 cmdeploy: dns --zonefile subcommand to just print the zonefile 2023-12-19 19:39:49 +01:00
missytake 79f766b28e tests: mark test as xfail until we can test for CLI output 2023-12-19 19:39:49 +01:00
missytake 0eeb692c4b DNS: re-use HTTP session to reduce query time by 7 seconds 2023-12-19 19:39:49 +01:00
missytake 6c401173db DNS: also generate AAAA entry 2023-12-19 19:39:49 +01:00
missytake b474b86e7b cmdeploy: only output DNS entries which are not correct yet 2023-12-19 19:39:49 +01:00
missytake 6a9beb8ff7 DNS: ensure mta-sts.@ is also pointing to @ 2023-12-19 19:39:49 +01:00
missytake d0f5d08443 cmdeploy run: don't run if crucial DNS entries are missing 2023-12-19 19:39:49 +01:00
missytake 49848ec01e cmdeploy init: show DNS entries required for deployment if not set 2023-12-19 19:39:49 +01:00
missytake 0ffe4d4996 Revert "pyinfra: only install unbound-anchor on Debian systems"
This reverts commit c1d3de926e.
2023-12-19 17:45:00 +01:00
missytake 7a2a889585 pyinfra: only install unbound-anchor on Debian systems 2023-12-19 17:45:00 +01:00
missytake 1e4b776de5 unbound: generate root.key manually if it doesn't exist 2023-12-19 17:45:00 +01:00
link2xt 3d00ca1672 doveauth: add support for Dovecot 2.3.16 2023-12-18 19:44:11 +00:00
link2xt 485bbb9cbd Let acmetool manage port 80
This avoids circular dependency with nginx.
nginx needs a certificate to start
and getting a certificate requires someone
listening on port 80.
2023-12-18 16:36:36 +01:00
holger krekel 359c195419 count ci accounts correctly 2023-12-16 17:06:13 +01:00
holger krekel 1b9e822ff6 strike this weird CHATMAIL_DOMAIN variable 2023-12-16 16:36:56 +01:00
holger krekel 9f6c00d62c strike last mentins of "instance" in readme 2023-12-16 16:36:56 +01:00
13 changed files with 329 additions and 87 deletions
-3
View File
@@ -33,8 +33,5 @@ jobs:
- name: run deploy-chatmail offline tests - name: run deploy-chatmail offline tests
run: pytest --pyargs cmdeploy run: pytest --pyargs cmdeploy
- name: initialize with chatmail domain
run: cmdeploy init chat.example.org
# all other cmdeploy commands require a staging server # all other cmdeploy commands require a staging server
# see https://github.com/deltachat/chatmail/issues/100 # see https://github.com/deltachat/chatmail/issues/100
+22 -18
View File
@@ -15,8 +15,8 @@ after which the initially specified password is required for using them.
## Deploying your own chatmail server ## Deploying your own chatmail server
We subsequently use `CHATMAIL_DOMAIN` as a placeholder for your fully qualified We use `chat.example.org` as the chatmail domain in the following steps.
DNS domain name (FQDN), for example `chat.example.org`. Please substitute it with your own domain.
1. Install the `cmdeploy` command in a virtualenv 1. Install the `cmdeploy` command in a virtualenv
@@ -29,15 +29,15 @@ DNS domain name (FQDN), for example `chat.example.org`.
2. Create chatmail configuration file `chatmail.ini`: 2. Create chatmail configuration file `chatmail.ini`:
``` ```
scripts/cmdeploy init CHATMAIL_DOMAIN scripts/cmdeploy init chat.example.org # <-- use your 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`. according to the hints provided by `cmdeploy init`.
Verify that SSH root login works: Verify that SSH root login works:
``` ```
ssh root@CHATMAIL_DOMAIN ssh root@chat.example.org # <-- use your domain
``` ```
4. Deploy to the remote chatmail server: 4. Deploy to the remote chatmail server:
@@ -45,13 +45,9 @@ DNS domain name (FQDN), for example `chat.example.org`.
``` ```
scripts/cmdeploy run scripts/cmdeploy run
``` ```
This script will also show you additional DNS records
5. To output a DNS zone file from which you can transfer DNS records which you should configure at your DNS provider
to your DNS provider: (it can take some time until they are public).
```
scripts/cmdeploy dns
```
### Other helpful commands: ### Other helpful commands:
@@ -61,6 +57,12 @@ To check the status of your remotely running chatmail service:
scripts/cmdeploy status scripts/cmdeploy status
``` ```
To check whether your DNS records are correct:
```
scripts/cmdeploy dns
```
To test whether your chatmail service is working correctly: To test whether your chatmail service is working correctly:
``` ```
@@ -75,7 +77,7 @@ scripts/cmdeploy bench
## Overview of this repository ## Overview of this repository
This repository drives the development of "chatmail instances", This repository drives the development of chatmail services,
comprised of minimal setups of comprised of minimal setups of
- [postfix smtp server](https://www.postfix.org) - [postfix smtp server](https://www.postfix.org)
@@ -91,7 +93,7 @@ as well as custom services that are integrated with these two:
to send mails for them. to send mails for them.
- `chatmaild/src/chatmaild/filtermail.py` prevents - `chatmaild/src/chatmaild/filtermail.py` prevents
unencrypted e-mail from leaving the chatmail instance unencrypted e-mail from leaving the chatmail service
and is integrated into postfix's outbound mail pipelines. and is integrated into postfix's outbound mail pipelines.
There is also the `cmdeploy/src/cmdeploy/cmdeploy.py` command line tool There is also the `cmdeploy/src/cmdeploy/cmdeploy.py` command line tool
@@ -104,7 +106,7 @@ to automatically install all chatmail components on a server.
### Home page and getting started for users ### Home page and getting started for users
`cmdeploy run` also creates default static Web pages and deploys them `cmdeploy run` also creates default static Web pages and deploys them
to an nginx web server under `https://CHATMAIL_DOMAIN`. to a nginx web server with:
- a default `index.html` along with a QR code that users can click to - a default `index.html` along with a QR code that users can click to
create accounts on your chatmail provider, create accounts on your chatmail provider,
@@ -149,10 +151,12 @@ While this file is present, account creation will be blocked.
### Ports ### Ports
Postfix listens on ports 25 (smtp) and 587 (submission) and 465 (submissions). [Postfix](http://www.postfix.org/) listens on ports 25 (smtp) and 587 (submission) and 465 (submissions).
Dovecot listens on ports 143(imap) and 993 (imaps). [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).
Delta Chat apps will, however, discover all ports and configurations Delta Chat apps will, however, discover all ports and configurations
automatically by reading the `autoconfig.xml` file from the chatmail service. automatically by reading the [autoconfig XML file](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) from the chatmail service.
+7 -1
View File
@@ -102,12 +102,17 @@ def handle_dovecot_request(msg, db, config: Config):
short_command = msg[0] short_command = msg[0]
if short_command == "L": # LOOKUP if short_command == "L": # LOOKUP
parts = msg[1:].split("\t") parts = msg[1:].split("\t")
keyname, user = parts[:2]
# 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("/") namespace, type, *args = keyname.split("/")
reply_command = "F" reply_command = "F"
res = "" res = ""
if namespace == "shared": if namespace == "shared":
if type == "userdb": if type == "userdb":
user = args[0]
if user.endswith(f"@{config.mail_domain}"): if user.endswith(f"@{config.mail_domain}"):
res = lookup_userdb(db, user) res = lookup_userdb(db, user)
if res: if res:
@@ -115,6 +120,7 @@ def handle_dovecot_request(msg, db, config: Config):
else: else:
reply_command = "N" reply_command = "N"
elif type == "passdb": elif type == "passdb":
user = args[1]
if user.endswith(f"@{config.mail_domain}"): if user.endswith(f"@{config.mail_domain}"):
res = lookup_passdb(db, config, user, cleartext_password=args[0]) res = lookup_passdb(db, config, user, cleartext_password=args[0])
if res: if res:
+1 -5
View File
@@ -6,7 +6,6 @@ it will echo back any message that has non-empty text and also supports the /hel
import logging import logging
import os import os
import sys import sys
from threading import Thread
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
@@ -76,10 +75,7 @@ def main():
config = read_config(sys.argv[1]) config = read_config(sys.argv[1])
password = create_newemail_dict(config).get("password") password = create_newemail_dict(config).get("password")
email = "echo@" + config.mail_domain email = "echo@" + config.mail_domain
configure_thread = Thread( bot.configure(email, password)
target=bot.configure, kwargs={"email": email, "password": password}
)
configure_thread.start()
bot.run_forever() bot.run_forever()
+5 -1
View File
@@ -399,7 +399,11 @@ def deploy_chatmail(config_path: Path) -> None:
# to use 127.0.0.1 as the resolver. # to use 127.0.0.1 as the resolver.
apt.packages( apt.packages(
name="Install unbound", name="Install unbound",
packages="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"],
) )
systemd.service( systemd.service(
name="Start and enable unbound", name="Start and enable unbound",
+18 -1
View File
@@ -1,6 +1,6 @@
import importlib.resources import importlib.resources
from pyinfra.operations import apt, files, server from pyinfra.operations import apt, files, systemd, server
def deploy_acmetool(nginx_hook=False, email="", domains=[]): def deploy_acmetool(nginx_hook=False, email="", domains=[]):
@@ -46,6 +46,23 @@ def deploy_acmetool(nginx_hook=False, email="", domains=[]):
mode="644", 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",
)
systemd.service(
name="Setup acmetool-redirector service",
service="acmetool-redirector.service",
running=True,
enabled=True,
restarted=service_file.changed,
)
server.shell( server.shell(
name=f"Request certificate for: { ', '.join(domains) }", name=f"Request certificate for: { ', '.join(domains) }",
commands=[f"acmetool want { ' '.join(domains)}"], commands=[f"acmetool want { ' '.join(domains)}"],
@@ -0,0 +1,11 @@
[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
+6 -4
View File
@@ -1,12 +1,14 @@
{chatmail_domain}. A {ipv4}
{chatmail_domain}. AAAA {ipv6}
{chatmail_domain}. MX 10 {chatmail_domain}. {chatmail_domain}. MX 10 {chatmail_domain}.
_submission._tcp.{chatmail_domain}. SRV 0 1 587 {chatmail_domain}. _submission._tcp.{chatmail_domain}. SRV 0 1 587 {chatmail_domain}.
_submissions._tcp.{chatmail_domain}. SRV 0 1 465 {chatmail_domain}. _submissions._tcp.{chatmail_domain}. SRV 0 1 465 {chatmail_domain}.
_imap._tcp.{chatmail_domain}. SRV 0 1 143 {chatmail_domain}. _imap._tcp.{chatmail_domain}. SRV 0 1 143 {chatmail_domain}.
_imaps._tcp.{chatmail_domain}. SRV 0 1 993 {chatmail_domain}. _imaps._tcp.{chatmail_domain}. SRV 0 1 993 {chatmail_domain}.
{chatmail_domain}. IN CAA 128 issue "letsencrypt.org;accounturi={acme_account_url}" {chatmail_domain}. CAA 128 issue "letsencrypt.org;accounturi={acme_account_url}"
{chatmail_domain}. TXT "v=spf1 a:{chatmail_domain} -all" {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" _dmarc.{chatmail_domain}. TXT "v=DMARC1;p=reject;rua=mailto:{email};ruf=mailto:{email};fo=1;adkim=r;aspf=r"
_mta-sts.{chatmail_domain}. IN TXT "v=STSv1; id={sts_id}" _mta-sts.{chatmail_domain}. TXT "v=STSv1; id={sts_id}"
mta-sts.{chatmail_domain}. IN CNAME {chatmail_domain}. mta-sts.{chatmail_domain}. CNAME {chatmail_domain}.
_smtp._tls.{chatmail_domain}. IN TXT "v=TLSRPTv1;rua=mailto:{email}" _smtp._tls.{chatmail_domain}. TXT "v=TLSRPTv1;rua=mailto:{email}"
{dkim_entry} {dkim_entry}
+35 -37
View File
@@ -3,7 +3,6 @@ Provides the `cmdeploy` entry point function,
along with command line option and subcommand parsing. along with command line option and subcommand parsing.
""" """
import argparse import argparse
import datetime
import shutil import shutil
import subprocess import subprocess
import importlib.resources import importlib.resources
@@ -15,6 +14,7 @@ from pathlib import Path
from termcolor import colored from termcolor import colored
from chatmaild.config import read_config, write_initial_config from chatmaild.config import read_config, write_initial_config
from cmdeploy.dns import show_dns, check_necessary_dns
# #
@@ -32,11 +32,16 @@ def init_cmd_options(parser):
def init_cmd(args, out): def init_cmd(args, out):
"""Initialize chatmail config file.""" """Initialize chatmail config file."""
mail_domain = args.chatmail_domain
if args.inipath.exists(): if args.inipath.exists():
out.red(f"Path exists, not modifying: {args.inipath}") print(f"Path exists, not modifying: {args.inipath}")
raise SystemExit(1) else:
write_initial_config(args.inipath, args.chatmail_domain) write_initial_config(args.inipath, mail_domain)
out.green(f"created config file for {args.chatmail_domain} in {args.inipath}") out.green(f"created config file for {mail_domain} in {args.inipath}")
check_necessary_dns(
out,
mail_domain,
)
def run_cmd_options(parser): def run_cmd_options(parser):
@@ -50,47 +55,34 @@ def run_cmd_options(parser):
def run_cmd(args, out): def run_cmd(args, out):
"""Deploy chatmail services on the remote server.""" """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 = os.environ.copy()
env["CHATMAIL_INI"] = args.inipath env["CHATMAIL_INI"] = args.inipath
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"
cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}" cmd = f"{pyinf} --ssh-user root {args.config.mail_domain} {deploy_path}"
out.check_call(cmd, env=env) 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): def dns_cmd(args, out):
"""Generate dns zone file.""" """Generate dns zone file."""
template = importlib.resources.files(__package__).joinpath("chatmail.zone.f") show_dns(args, out)
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): def status_cmd(args, out):
@@ -219,9 +211,15 @@ class Out:
color = "red" if red else ("green" if green else None) color = "red" if red else ("green" if green else None)
print(colored(msg, color), file=file) print(colored(msg, color), file=file)
def shell_output(self, arg): def shell_output(self, arg, no_print=False, timeout=10):
if not no_print:
self(f"[$ {arg}]", file=sys.stderr) self(f"[$ {arg}]", file=sys.stderr)
return subprocess.check_output(arg, shell=True).decode() output = subprocess.STDOUT
else:
output = subprocess.DEVNULL
return subprocess.check_output(
arg, shell=True, timeout=timeout, stderr=output
).decode()
def check_call(self, arg, env=None, quiet=False): def check_call(self, arg, env=None, quiet=False):
if not quiet: if not quiet:
+211
View File
@@ -0,0 +1,211 @@
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 {typ} {domain}")
line_num = 0
for line in dig_result.splitlines():
line_num += 1
if line.strip() == ";; ANSWER SECTION:":
return dig_result.splitlines()[line_num].split("\t")[-1]
def check_ptr_record(self, ip: str, mail_domain) -> str:
"""Check the PTR record for an IPv4 or IPv6 address."""
result = self.get("-x", ip)
if result:
if ip_address(ip).version == 6:
result = result.split()[-1]
if result[:-1] == mail_domain:
return result
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
@@ -48,12 +48,4 @@ http {
# add cgi-bin support # add cgi-bin support
include /usr/share/doc/fcgiwrap/examples/nginx.conf; 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;
}
} }
@@ -2,6 +2,16 @@ import pytest
import threading import threading
import queue 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): def test_login_basic_functioning(imap_or_smtp, gencreds, lp):
"""Test a) that an initial login creates a user automatically """Test a) that an initial login creates a user automatically
+1 -7
View File
@@ -2,7 +2,6 @@ import os
import pytest import pytest
from cmdeploy.cmdeploy import get_parser, main from cmdeploy.cmdeploy import get_parser, main
from chatmaild.config import read_config
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@@ -21,12 +20,7 @@ class TestCmdline:
run = parser.parse_args(["run"]) run = parser.parse_args(["run"])
assert init and run assert init and run
def test_init(self, tmp_path): @pytest.mark.xfail(reason="init doesn't exit anymore, check for CLI output instead")
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): def test_init_not_overwrite(self):
main(["init", "chat.example.org"]) main(["init", "chat.example.org"])
with pytest.raises(SystemExit): with pytest.raises(SystemExit):