making it work

This commit is contained in:
holger krekel
2023-12-09 15:15:40 +01:00
parent 6285283b02
commit 81c4a6170f
10 changed files with 287 additions and 128 deletions

View File

@@ -36,7 +36,7 @@ DNS domain name (FQDN), for example `chat.example.org`.
3. Create chatmail configuration file `chatmail.ini`: 3. Create chatmail configuration file `chatmail.ini`:
``` ```
cmdeploy genconfig chatmail.ini CHATMAIL_DOMAIN cmdeploy init chatmail.ini CHATMAIL_DOMAIN
``` ```
4. Deploy to the remote chatmail server, pointing to the chatmail config file: 4. Deploy to the remote chatmail server, pointing to the chatmail config file:

View File

@@ -1,41 +1,25 @@
from pathlib import Path from pathlib import Path
from fnmatch import fnmatch from fnmatch import fnmatch
import iniconfig import iniconfig
system_mailname_path = Path("/etc/mailname")
def read_config(inipath):
def read_config(inipath, mailname=None): cfg = iniconfig.IniConfig(inipath)
if mailname is None: return Config(inipath, params=cfg.sections["params"])
with open(system_mailname_path) as f:
mailname = f.read().strip()
ini = iniconfig.IniConfig(inipath)
privacy = {}
for section in ini:
if section.name.startswith("privacy:"):
domain = section["domain"]
if fnmatch(mailname, domain):
privacy = section
break
return Config(inipath, mailname, privacy, params=ini.sections["params"])
class Config: class Config:
def __init__(self, inipath, mailname, privacy, params): def __init__(self, inipath, params):
self._inipath = inipath self._inipath = inipath
self.mailname = mailname self.mailname = params["mailname"]
self.privacy_postal = privacy.get("privacy_postal")
self.privacy_mail = privacy.get("privacy_mail")
self.privacy_pdo = privacy.get("privacy_pdo")
self.privacy_supervisor = privacy.get("privacy_supervisor")
self.max_user_send_per_minute = int(params["max_user_send_per_minute"]) self.max_user_send_per_minute = int(params["max_user_send_per_minute"])
self.filtermail_smtp_port = int(params["filtermail_smtp_port"]) self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.postfix_reinject_port = int(params["postfix_reinject_port"]) self.postfix_reinject_port = int(params["postfix_reinject_port"])
self.passthrough_recipients = params["passthrough_recipients"].split() self.passthrough_recipients = params["passthrough_recipients"].split()
self.privacy_postal = params.get("privacy_postal")
self.privacy_mail = params.get("privacy_mail")
self.privacy_pdo = params.get("privacy_pdo")
self.privacy_supervisor = params.get("privacy_supervisor")
def _getbytefile(self): def _getbytefile(self):
return open(self._inipath, "rb") return open(self._inipath, "rb")

View File

@@ -127,7 +127,10 @@ class BeforeQueueHandler:
is_outgoing = recipient_domain != envelope_from_domain is_outgoing = recipient_domain != envelope_from_domain
if is_outgoing and not mail_encrypted: if is_outgoing and not mail_encrypted:
is_securejoin = message.get("secure-join") in ["vc-request", "vg-request"] is_securejoin = message.get("secure-join") in [
"vc-request",
"vg-request",
]
if not is_securejoin: if not is_securejoin:
return f"500 Invalid unencrypted mail to <{recipient}>" return f"500 Invalid unencrypted mail to <{recipient}>"

View File

@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools>=45"] requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@@ -12,9 +12,13 @@ dependencies = [
"markdown", "markdown",
"pytest", "pytest",
"setuptools>=68", "setuptools>=68",
"termcolor",
"tox", "tox",
] ]
[project.scripts]
cmdeploy = "deploy_chatmail.cmdeploy:main"
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-v -ra --strict-markers" addopts = "-v -ra --strict-markers"

View File

@@ -0,0 +1,132 @@
"""
Provides the `cmdeploy` entry point function,
along with command line option and subcommand parsing.
"""
import importlib.resources
import argparse
from pathlib import Path
import iniconfig
from termcolor import colored
from chatmaild.config import read_config
class Out:
"""Convenience print output printer providing coloring."""
def red(self, msg):
print(colored(msg, "red"))
def green(self, msg):
print(colored(msg, "green"))
def __call__(self, msg, red=False, green=False):
color = "red" if red else ("green" if green else None)
print(colored(msg, color))
description = """\
Setup your chatmail server configuration and
deploy it via SSH to your remote location.
"""
def add_config_option(parser):
parser.add_argument(
"--config",
dest="chatmail_ini",
action="store",
default=Path("chatmail.ini"),
type=Path,
help="path to the chatmail.ini file",
)
def add_subcommand(subparsers, func):
name = func.__name__
assert name.endswith("_cmd")
name = name[:-4]
doc = func.__doc__.strip()
p = subparsers.add_parser(name, description=doc, help=doc)
p.set_defaults(func=func)
return p
def get_parser():
"""Return an ArgumentParser for the 'cmdeploy' CLI."""
parser = argparse.ArgumentParser(description=description)
subparsers = parser.add_subparsers(
title="subcommands",
)
init_parser = add_subcommand(subparsers, init_cmd)
add_config_option(init_parser)
init_parser.add_argument(
"chatmail_domain",
action="store",
help="fully qualified DNS domain name for your chatmail instance",
)
install_parser = add_subcommand(subparsers, install_cmd)
add_config_option(install_parser)
return parser
def write_initial_config(inipath, mailname, out):
inidir = importlib.resources.files(__package__).joinpath("ini")
content = inidir.joinpath("chatmail.ini.f").read_text().format(mailname=mailname)
if mailname.endswith(".testrun.org"):
override_inipath = inidir.joinpath("override-testrun.ini")
privacy = iniconfig.IniConfig(override_inipath)["privacy"]
lines = []
for line in content.split("\n"):
for key, value in privacy.items():
value_lines = value.strip().split("\n")
if not line.startswith(f"{key} =") or not value_lines:
continue
if len(value_lines) == 1:
lines.append(f"{key} = {value}")
else:
lines.append(f"{key} =")
for vl in value_lines:
lines.append(f" {vl}")
break
else:
lines.append(line)
content = "\n".join(lines)
inipath.write_text(content)
out(f"written {inipath} for chatmail domain {mailname}")
def init_cmd(args, out):
"""Initialize chatmail config file."""
if args.chatmail_ini.exists():
out.red(f"Path exists, not modifying: {args.xdcget_ini}")
raise SystemExit(1)
write_initial_config(args.chatmail_ini, args.chatmail_domain, out)
def install_cmd(args, out):
"""Install or update chatmail services on the remote server. """
try:
config = read_config(args.chatmail_ini)
except Exception as ex:
out.red(ex)
raise SystemExit(1)
XXX
def main(args=None):
"""Provide main entry point for 'xdcget' CLI invocation."""
parser = get_parser()
args = parser.parse_args(args=args)
if not hasattr(args, "func"):
return parser.parse_args(["-h"])
out = Out()
args.func(args, out)
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,8 @@
[params]
# mail domain (MUST be set to fully qualified chat mail domain) # mail domain (MUST be set to fully qualified chat mail domain)
domain = {mailname} mailname = {mailname}
# #
# If you only do private test deploys, you don't need to modify any settings below # If you only do private test deploys, you don't need to modify any settings below

View File

@@ -1,16 +1,13 @@
mailname = {mailname} [privacy]
max_user_send_per_minute = 60
passthrough_recipients = privacy@testrun.org xstore@testrun.org
filtermail_smtp_port = 10080 passthrough_recipients = privacy@testrun.org
postfix_reinject_port = 10025
privacy_postal = privacy_postal =
Merlinux GmbH, Represented by the managing director H. Krekel, Merlinux GmbH, Represented by the managing director H. Krekel,
Reichgrafen Str. 20, 79102 Freiburg, Germany Reichgrafen Str. 20, 79102 Freiburg, Germany
privacy_mail = delta-privacy@merlinux.eu privacy_mail = privacy@testrun.org
privacy_pdo = privacy_pdo =
Prof. Dr. Fabian Schmieder, lexICT UG (limited), Ostfeldstr. 49, 30559 Hannover. Prof. Dr. Fabian Schmieder, lexICT UG (limited), Ostfeldstr. 49, 30559 Hannover.
You can contact him at *delta-privacy@merlinux.eu* (Keyword: DPO) You can contact him at *delta-privacy@merlinux.eu* (Keyword: DPO)

View File

@@ -2,87 +2,27 @@ from chatmaild.config import read_config
import chatmaild.config import chatmaild.config
def test_read_config_without_mailname(tmp_path, create_ini, monkeypatch): def test_read_config_basic(make_config):
mailname_path = tmp_path.joinpath("mailname") config = make_config("chat.example.org")
mailname_path.write_text("something.example.org") assert config.mailname == "chat.example.org"
monkeypatch.setattr(chatmaild.config, "system_mailname_path", mailname_path) assert not config.privacy_supervisor and not config.privacy_mail
assert not config.privacy_pdo and not config.privacy_postal
inipath = create_ini( inipath = config._inipath
""" inipath.write_text(inipath.read_text().replace("60", "37"))
[params]
max_user_send_per_minute = 40
filtermail_smtp_port = 9875
postfix_reinject_port = 9999
passthrough_recipients =
"""
)
config = read_config(inipath) config = read_config(inipath)
assert config.mailname == "something.example.org" assert config.max_user_send_per_minute == 37
assert config.mailname == "chat.example.org"
def test_read_config_without_privacy_policy(tmp_path, create_ini): def test_read_config_testrun(make_config):
inipath = create_ini( config = make_config("something.testrun.org")
"""
[params]
max_user_send_per_minute = 40
filtermail_smtp_port = 9875
postfix_reinject_port = 9999
passthrough_recipients =
[privacy:testrun]
domain = *.example.org
"""
)
config = read_config(inipath, "something.example.org")
assert config.mailname == "something.example.org"
assert config.max_user_send_per_minute == 40
assert config.filtermail_smtp_port == 9875
assert config.postfix_reinject_port == 9999
assert config.passthrough_recipients == []
assert not config.privacy_postal
assert not config.privacy_mail
assert not config.privacy_pdo
assert not config.privacy_supervisor
def test_read_config(create_ini):
inipath = create_ini(
"""
[params]
max_user_send_per_minute = 40
filtermail_smtp_port = 10080
postfix_reinject_port = 10025
passthrough_recipients = x@example.org y@example.org
[privacy:testrun]
domain = *.testrun.org
privacy_postal =
Postal Ltd
privacy_mail = privacy@merlinux.eu
privacy_pdo =
Postal PDO
You can contact him at *delta-privacy@merlinux.eu* (Keyword: DPO)
privacy_supervisor =
line1
line2 with space
"""
)
config = read_config(inipath, "something.testrun.org")
assert config.mailname == "something.testrun.org" assert config.mailname == "something.testrun.org"
assert len(config.privacy_postal.split("\n")) > 1
assert len(config.privacy_supervisor.split("\n")) > 1
assert len(config.privacy_pdo.split("\n")) > 1
assert config.privacy_mail == "privacy@testrun.org"
assert config.filtermail_smtp_port == 10080 assert config.filtermail_smtp_port == 10080
assert config.postfix_reinject_port == 10025 assert config.postfix_reinject_port == 10025
assert config.passthrough_recipients == ["x@example.org", "y@example.org"] assert config.max_user_send_per_minute == 60
assert config.privacy_postal == "Postal Ltd" assert config.passthrough_recipients
assert config.privacy_mail == "privacy@merlinux.eu"
lines = config.privacy_pdo.split("\n")
assert lines[0] == "Postal PDO"
assert lines[1].startswith("You can ")
lines = config.privacy_supervisor.split("\n")
assert lines[0] == "line1"
assert lines[1] == "line2 with space"

View File

@@ -39,13 +39,6 @@ def pytest_runtest_setup(item):
pytest.skip("skipping slow test, use --slow to run") pytest.skip("skipping slow test, use --slow to run")
@pytest.fixture
def inipath():
dpath = importlib.resources.files("chatmaild")
inipath = dpath.joinpath("../../../chatmail.ini").resolve()
assert inipath.exists()
return inipath
@pytest.fixture @pytest.fixture
def maildomain(): def maildomain():
@@ -415,13 +408,13 @@ class CMUser:
@pytest.fixture @pytest.fixture
def create_ini(tmp_path, inipath): def make_config(tmp_path):
def create_ini_func(source=None): from deploy_chatmail.cmdeploy import main
if source is None: from chatmaild.config import read_config
source = inipath.read_text() inipath = tmp_path.joinpath("chatmail.ini")
p = tmp_path.joinpath("chatmail.ini")
assert not p.exists(), p
p.write_text(textwrap.dedent(source))
return p
return create_ini_func def make_conf(mailname):
main(["init", "--config", str(inipath), mailname])
return read_config(inipath)
return make_conf

104
tests/test_cmdeploy.py Normal file
View File

@@ -0,0 +1,104 @@
import os
import sys
import pytest
from deploy_chatmail.cmdeploy import get_parser, main
from chatmaild.config import read_config
class TestCmdline:
def test_parser(self, capsys):
parser = get_parser()
parser.parse_args([])
init = parser.parse_args(["init", "chat.example.org"])
update = parser.parse_args(["install"])
assert init and update
def test_init(self, tmpdir):
tmpdir.chdir()
main(["init", "chat.example.org"])
inipath = tmpdir.join("chatmail.ini")
config = read_config(inipath.strpath)
assert config.mailname == "chat.example.org"
def test_no_args_description(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main([])
assert excinfo.value.code == 0
out, err = capsys.readouterr()
assert "Collect webxdc" in out
assert " init " in out and "Initialize config" in out
def test_version(self, capsys):
with pytest.raises(SystemExit) as excinfo:
main(["--version"])
assert excinfo.value.code == 0
out, err = capsys.readouterr()
assert out.strip() == xdcget.__version__
def test_init_not_overwrite(self, tmpdir):
tmpdir.chdir()
main(["init"])
with pytest.raises(SystemExit):
main(["init"])
def test_update_from_different_dir(self, config_example1, tmp_path):
p = tmp_path.joinpath("somewhere")
p.mkdir()
os.chdir(p)
main(["--config", "../xdcget.ini", "update"])
def test_prune_index(self, iniconfig):
iniconfig.add_source(
app_id="webxdc-poll",
source_code_url="https://codeberg.org/webxdc/poll",
)
iniconfig.add_lock_entry(
app_id="webxdc-poll",
name="Poll",
tag_name="v1.0.1",
url="https://codeberg.org/attachments/d53543bd-d805-4aba-926d-88eefc7a9eef",
date="2023-07-05T20:30:48Z",
cache_relname="webxdc-poll-v1.0.1.xdc",
)
iniconfig.add_lock_entry(
app_id="webxdc-checklist",
name="Checklist",
tag_name="v0.0.2",
url="https://codeberg.org/attachments/65d05b8d-a97c-4fb6-a534-e308c382f874",
date="2023-07-07T18:05:19Z",
cache_relname="webxdc-checklist-v0.0.2.xdc",
)
config = iniconfig.create()
assert "webxdc-poll" in config.index_path.read_text()
assert "webxdc-checklist" in config.index_path.read_text()
main(["update"])
assert "webxdc-poll" in config.index_path.read_text()
assert "webxdc-checklist" not in config.index_path.read_text()
def test_update_empty(self, iniconfig):
iniconfig.create()
with pytest.raises(SystemExit):
main(["update"])
def test_update_no_network(self, capfd, config_example1, monkeypatch):
main(["update"])
p = config_example1.export_dir.joinpath("xdcget.lock")
assert p.exists()
assert len(p.read_text()) > 50
monkeypatch.delattr(sys.modules["requests"], "get")
main(["update", "--offline"])
def test_export_json(self, capfd, config_example1, monkeypatch):
main(["update"])
p = config_example1.export_dir.joinpath("xdcget-lock.json")
assert p.exists()
with p.open() as f:
app_list = json.load(f)
assert len(app_list) == 2
checklist, poll = app_list
p = config_example1.export_dir.joinpath(checklist["icon_relname"])
assert p.exists() and "icon" in p.name
p = config_example1.export_dir.joinpath(poll["icon_relname"])
assert p.exists() and "icon" in p.name