mirror of
https://github.com/chatmail/relay.git
synced 2026-05-10 16:04:37 +00:00
Compare commits
32 Commits
link2xt/ad
...
greeterbot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b9b0f5f44 | ||
|
|
10c671ebda | ||
|
|
f2be32ac6f | ||
|
|
b702848c33 | ||
|
|
a6f2f74520 | ||
|
|
01ec341364 | ||
|
|
998799fe3f | ||
|
|
6186dc5259 | ||
|
|
5880133b5b | ||
|
|
6772bfe630 | ||
|
|
101c3a6b47 | ||
|
|
5ef2100765 | ||
|
|
d49aae365c | ||
|
|
998a185332 | ||
|
|
3e78555ca1 | ||
|
|
01cfd0be19 | ||
|
|
1bdc547479 | ||
|
|
c0b8ba816d | ||
|
|
118ae49674 | ||
|
|
a47df20e22 | ||
|
|
a1d8881887 | ||
|
|
cd7416a0dd | ||
|
|
173e3f6390 | ||
|
|
b8d53242cf | ||
|
|
c65f618fb1 | ||
|
|
42afad0852 | ||
|
|
8bc19439a9 | ||
|
|
cdaddb9b0f | ||
|
|
768bf2b22c | ||
|
|
185e6f7d2a | ||
|
|
90e7169eef | ||
|
|
3db7933d8b |
@@ -1,20 +0,0 @@
|
||||
;; Zone file for staging.testrun.org
|
||||
|
||||
$ORIGIN staging.testrun.org.
|
||||
$TTL 300
|
||||
|
||||
@ IN SOA ns.testrun.org. root.nine.testrun.org (
|
||||
2023010101 ; Serial
|
||||
7200 ; Refresh
|
||||
3600 ; Retry
|
||||
1209600 ; Expire
|
||||
3600 ; Negative response caching TTL
|
||||
)
|
||||
|
||||
;; Nameservers.
|
||||
@ IN NS ns.testrun.org.
|
||||
|
||||
;; DNS records.
|
||||
@ IN A 37.27.37.98
|
||||
mta-sts.staging.testrun.org. CNAME staging.testrun.org.
|
||||
www.staging.testrun.org. CNAME staging.testrun.org.
|
||||
72
.github/workflows/test-and-deploy.yaml
vendored
72
.github/workflows/test-and-deploy.yaml
vendored
@@ -1,72 +0,0 @@
|
||||
name: deploy on staging.testrun.org, and run tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- staging-ci
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: deploy on staging.testrun.org, and run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: prepare SSH
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
echo "${{ secrets.STAGING_SSH_KEY }}" >> ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan staging.testrun.org > ~/.ssh/known_hosts
|
||||
# rsync -avz root@staging.testrun.org:/var/lib/acme . || true
|
||||
# rsync -avz root@staging.testrun.org:/var/lib/rspamd/dkim . || true
|
||||
|
||||
#- name: rebuild staging.testrun.org to have a clean VPS
|
||||
# run: |
|
||||
# curl -X POST \
|
||||
# -H "Authorization: Bearer ${{ secrets.HETZNER_API_TOKEN }}" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d '{"image":"debian-12"}' \
|
||||
# "https://api.hetzner.cloud/v1/servers/${{ secrets.STAGING_SERVER_ID }}/actions/rebuild"
|
||||
|
||||
- run: scripts/initenv.sh
|
||||
|
||||
- name: append venv/bin to PATH
|
||||
run: echo venv/bin >>$GITHUB_PATH
|
||||
|
||||
- name: run formatting checks
|
||||
run: cmdeploy fmt -v
|
||||
|
||||
- name: run deploy-chatmail offline tests
|
||||
run: pytest --pyargs cmdeploy
|
||||
|
||||
#- name: upload TLS cert after rebuilding
|
||||
# run: |
|
||||
# echo " --- wait until staging.testrun.org VPS is rebuilt --- "
|
||||
# rm ~/.ssh/known_hosts
|
||||
# while ! ssh -o ConnectTimeout=180 -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org id -u ; do sleep 1 ; done
|
||||
# ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org id -u
|
||||
# rsync -avz acme root@staging.testrun.org:/var/lib/ || true
|
||||
# rsync -avz dkim root@staging.testrun.org:/var/lib/rspamd/ || true
|
||||
|
||||
- run: cmdeploy init staging.testrun.org
|
||||
|
||||
- run: cmdeploy run
|
||||
|
||||
- name: set DNS entries
|
||||
run: |
|
||||
#ssh -o StrictHostKeyChecking=accept-new -v root@staging.testrun.org chown _rspamd:_rspamd -R /var/lib/rspamd/dkim
|
||||
cmdeploy dns --zonefile staging-generated.zone
|
||||
cat staging-generated.zone >> .github/workflows/staging.testrun.org-default.zone
|
||||
cat .github/workflows/staging.testrun.org-default.zone
|
||||
scp -o StrictHostKeyChecking=accept-new .github/workflows/staging.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging.testrun.org.zone
|
||||
ssh root@ns.testrun.org nsd-checkzone staging.testrun.org /etc/nsd/staging.testrun.org.zone
|
||||
ssh root@ns.testrun.org systemctl reload nsd
|
||||
|
||||
- name: cmdeploy test
|
||||
run: CHATMAIL_DOMAIN2=nine.testrun.org cmdeploy test --slow
|
||||
|
||||
- name: cmdeploy dns (try 3 times)
|
||||
run: cmdeploy dns || cmdeploy dns || cmdeploy dns
|
||||
|
||||
@@ -10,6 +10,10 @@ dependencies = [
|
||||
"iniconfig",
|
||||
"deltachat-rpc-server",
|
||||
"deltachat-rpc-client",
|
||||
"ConfigArgParse",
|
||||
"deltachat",
|
||||
"setuptools>=60",
|
||||
"setuptools-scm>=8",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
@@ -22,6 +26,7 @@ where = ['src']
|
||||
doveauth = "chatmaild.doveauth:main"
|
||||
filtermail = "chatmaild.filtermail:main"
|
||||
echobot = "chatmaild.echo:main"
|
||||
greeterbot = "chatmaild.greeterbot:main"
|
||||
chatmail-metrics = "chatmaild.metrics:main"
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
|
||||
BIN
chatmaild/src/chatmaild/avatar.jpg
Normal file
BIN
chatmaild/src/chatmaild/avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -46,11 +46,17 @@ class Connection:
|
||||
)
|
||||
return result
|
||||
|
||||
def get_user_list(self) -> set[str]:
|
||||
"""Get a set of all users."""
|
||||
q = "SELECT addr from users"
|
||||
return set([tup[0] for tup in self._sqlconn.execute(q).fetchall()])
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, path: str):
|
||||
def __init__(self, path: str, read_only=False):
|
||||
self.path = Path(path)
|
||||
self.ensure_tables()
|
||||
if not read_only:
|
||||
self.ensure_tables()
|
||||
|
||||
def _get_connection(
|
||||
self, write=False, transaction=False, closing=False
|
||||
|
||||
@@ -46,7 +46,7 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool:
|
||||
len(localpart) > config.username_max_length
|
||||
or len(localpart) < config.username_min_length
|
||||
):
|
||||
if localpart != "echo":
|
||||
if localpart not in ("echo", "hello"):
|
||||
logging.warning(
|
||||
"localpart %s has to be between %s and %s chars long",
|
||||
localpart,
|
||||
|
||||
BIN
chatmaild/src/chatmaild/editor.xdc
Normal file
BIN
chatmaild/src/chatmaild/editor.xdc
Normal file
Binary file not shown.
132
chatmaild/src/chatmaild/greeterbot.py
Normal file
132
chatmaild/src/chatmaild/greeterbot.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import time
|
||||
|
||||
import deltachat
|
||||
from deltachat.tracker import ConfigureFailed
|
||||
from time import sleep
|
||||
import tempfile
|
||||
import os
|
||||
import configargparse
|
||||
import pkg_resources
|
||||
import secrets
|
||||
|
||||
from chatmaild.database import Database
|
||||
from chatmaild.config import read_config
|
||||
from chatmaild.newemail import ALPHANUMERIC_PUNCT, CONFIG_PATH
|
||||
|
||||
PASSDB_PATH = "/home/vmail/passdb.sqlite"
|
||||
|
||||
|
||||
def setup_account(data_dir: str, debug: bool) -> deltachat.Account:
|
||||
"""Create a deltachat account with a given addr/password combination.
|
||||
|
||||
:param data_dir: the directory where the data(base) is stored.
|
||||
:param debug: whether to show log messages for the account.
|
||||
:return: the deltachat account object.
|
||||
"""
|
||||
chatmail_config = read_config(CONFIG_PATH)
|
||||
addr = "hello@" + chatmail_config.mail_domain
|
||||
|
||||
try:
|
||||
os.mkdir(os.path.join(data_dir, addr))
|
||||
except FileExistsError:
|
||||
pass
|
||||
db_path = os.path.join(data_dir, addr, "db.sqlite")
|
||||
|
||||
ac = deltachat.Account(db_path)
|
||||
if debug:
|
||||
ac.add_account_plugin(deltachat.events.FFIEventLogger(ac))
|
||||
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
ac.set_config("mdns_enabled", "0")
|
||||
|
||||
if not ac.is_configured():
|
||||
cleartext_password = "".join(
|
||||
secrets.choice(ALPHANUMERIC_PUNCT)
|
||||
for _ in range(chatmail_config.password_min_length + 3)
|
||||
)
|
||||
ac.set_config("mail_pw", cleartext_password)
|
||||
ac.set_config("addr", addr)
|
||||
|
||||
configtracker = ac.configure()
|
||||
try:
|
||||
configtracker.wait_finish()
|
||||
except ConfigureFailed:
|
||||
print(
|
||||
"configuration setup failed for %s with password:\n%s"
|
||||
% (ac.get_config("addr"), ac.get_config("mail_pw"))
|
||||
)
|
||||
raise
|
||||
|
||||
ac.start_io()
|
||||
avatar = pkg_resources.resource_filename(__name__, "avatar.jpg")
|
||||
ac.set_avatar(avatar)
|
||||
ac.set_config("displayname", f"Hello at {chatmail_config.mail_domain}!")
|
||||
return ac
|
||||
|
||||
|
||||
class GreetBot:
|
||||
def __init__(self, passdb, account):
|
||||
self.db = Database(passdb, read_only=True)
|
||||
self.account = account
|
||||
self.domain = account.get_config("addr").split("@")[1]
|
||||
with self.db.read_connection() as conn:
|
||||
self.existing_users = conn.get_user_list()
|
||||
|
||||
def greet_users(self):
|
||||
with self.db.read_connection() as conn:
|
||||
users = conn.get_user_list()
|
||||
new_users = users.difference(self.existing_users)
|
||||
self.existing_users = users
|
||||
time.sleep(20) # wait until Delta is configured on the user side
|
||||
for user in new_users:
|
||||
for ci_prefix in ["ac1_", "ac2_", "ac3_", "ac4_", "ac5_", "ci-"]:
|
||||
if user.startswith(ci_prefix):
|
||||
continue
|
||||
if user not in [c.addr for c in self.account.get_contacts()]:
|
||||
print("Inviting", user)
|
||||
contact = self.account.create_contact(user)
|
||||
chat = contact.create_chat()
|
||||
chat.send_text(
|
||||
"Welcome to %s! Here you can try out Delta Chat." % (self.domain,)
|
||||
)
|
||||
chat.send_text(
|
||||
"I prepared some webxdc apps for you, if you are interested:"
|
||||
)
|
||||
chat.send_file(pkg_resources.resource_filename(__name__, "editor.xdc"))
|
||||
chat.send_file(
|
||||
pkg_resources.resource_filename(__name__, "tower-builder.xdc")
|
||||
)
|
||||
chat.send_text(
|
||||
"You can visit https://webxdc.org/apps to discover more apps! "
|
||||
"Some of these games you can also play with friends, directly in the chat."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = configargparse.ArgumentParser()
|
||||
args.add_argument("--db_path", help="location of the Delta Chat database")
|
||||
args.add_argument(
|
||||
"--passdb", default=PASSDB_PATH, help="location of the chatmail passdb"
|
||||
)
|
||||
args.add_argument("--show-ffi", action="store_true", help="print Delta Chat log")
|
||||
ops = args.parse_args()
|
||||
|
||||
# ensuring account data directory
|
||||
if ops.db_path is None:
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="hellobot")
|
||||
ops.db_path = tempdir.name
|
||||
elif not os.path.exists(ops.db_path):
|
||||
os.mkdir(ops.db_path)
|
||||
|
||||
ac = setup_account(ops.db_path, ops.show_ffi)
|
||||
greeter = GreetBot(ops.passdb, ac)
|
||||
print("waiting for new chatmail users...")
|
||||
while 1:
|
||||
greeter.greet_users()
|
||||
sleep(5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
11
chatmaild/src/chatmaild/greeterbot.service.f
Normal file
11
chatmaild/src/chatmaild/greeterbot.service.f
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=Chatmail greeterbot, a Delta Chat bot to greet new users
|
||||
|
||||
[Service]
|
||||
ExecStart={execpath} --passdb {passdb_path} --db_path /home/vmail/greeterbot/ --show-ffi
|
||||
User=vmail
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -33,7 +33,7 @@ password_min_length = 9
|
||||
passthrough_senders =
|
||||
|
||||
# list of e-mail recipients for which to accept outbound un-encrypted mails
|
||||
passthrough_recipients = xstore@testrun.org groupsbot@hispanilandia.net
|
||||
passthrough_recipients =
|
||||
|
||||
#
|
||||
# Deployment Details
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[privacy]
|
||||
|
||||
passthrough_recipients = privacy@testrun.org xstore@testrun.org groupsbot@hispanilandia.net
|
||||
passthrough_recipients = privacy@testrun.org
|
||||
|
||||
privacy_postal =
|
||||
Merlinux GmbH, Represented by the managing director H. Krekel,
|
||||
|
||||
@@ -28,5 +28,5 @@ def test_read_config_testrun(make_config):
|
||||
assert config.username_min_length == 9
|
||||
assert config.username_max_length == 9
|
||||
assert config.password_min_length == 9
|
||||
assert "privacy@testrun.org" in config.passthrough_recipients
|
||||
assert config.passthrough_recipients == ["privacy@testrun.org"]
|
||||
assert config.passthrough_senders == []
|
||||
|
||||
BIN
chatmaild/src/chatmaild/tower-builder.xdc
Normal file
BIN
chatmaild/src/chatmaild/tower-builder.xdc
Normal file
Binary file not shown.
@@ -101,11 +101,13 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
||||
"doveauth",
|
||||
"filtermail",
|
||||
"echobot",
|
||||
"greeterbot",
|
||||
):
|
||||
params = dict(
|
||||
execpath=f"{remote_venv_dir}/bin/{fn}",
|
||||
config_path=remote_chatmail_inipath,
|
||||
remote_venv_dir=remote_venv_dir,
|
||||
passdb_path="/home/vmail/passdb.sqlite",
|
||||
)
|
||||
source_path = importlib.resources.files("chatmaild").joinpath(f"{fn}.service.f")
|
||||
content = source_path.read_text().format(**params).encode()
|
||||
@@ -126,107 +128,6 @@ def _install_remote_venv_with_chatmaild(config) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
|
||||
"""Configures OpenDKIM"""
|
||||
need_restart = False
|
||||
|
||||
server.group(name="Create opendkim group", group="opendkim", system=True)
|
||||
server.user(
|
||||
name="Create opendkim user",
|
||||
user="opendkim",
|
||||
groups=["opendkim"],
|
||||
system=True,
|
||||
)
|
||||
server.user(
|
||||
name="Add postfix user to opendkim group for socket access",
|
||||
user="postfix",
|
||||
groups=["opendkim"],
|
||||
system=True,
|
||||
)
|
||||
|
||||
main_config = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/opendkim.conf"),
|
||||
dest="/etc/opendkim.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||
)
|
||||
need_restart |= main_config.changed
|
||||
|
||||
screen_script = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/screen.lua"),
|
||||
dest="/etc/opendkim/screen.lua",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= screen_script.changed
|
||||
|
||||
final_script = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/final.lua"),
|
||||
dest="/etc/opendkim/final.lua",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= final_script.changed
|
||||
|
||||
files.directory(
|
||||
name="Add opendkim directory to /etc",
|
||||
path="/etc/opendkim",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="750",
|
||||
present=True,
|
||||
)
|
||||
|
||||
keytable = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/KeyTable"),
|
||||
dest="/etc/dkimkeys/KeyTable",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="644",
|
||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||
)
|
||||
need_restart |= keytable.changed
|
||||
|
||||
signing_table = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath("opendkim/SigningTable"),
|
||||
dest="/etc/dkimkeys/SigningTable",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="644",
|
||||
config={"domain_name": domain, "opendkim_selector": dkim_selector},
|
||||
)
|
||||
need_restart |= signing_table.changed
|
||||
files.directory(
|
||||
name="Add opendkim socket directory to /var/spool/postfix",
|
||||
path="/var/spool/postfix/opendkim",
|
||||
user="opendkim",
|
||||
group="opendkim",
|
||||
mode="750",
|
||||
present=True,
|
||||
)
|
||||
|
||||
apt.packages(
|
||||
name="apt install opendkim opendkim-tools",
|
||||
packages=["opendkim", "opendkim-tools"],
|
||||
)
|
||||
|
||||
if not host.get_fact(File, f"/etc/dkimkeys/{dkim_selector}.private"):
|
||||
server.shell(
|
||||
name="Generate OpenDKIM domain keys",
|
||||
commands=[
|
||||
f"opendkim-genkey -D /etc/dkimkeys -d {domain} -s {dkim_selector}"
|
||||
],
|
||||
_sudo=True,
|
||||
_sudo_user="opendkim",
|
||||
)
|
||||
|
||||
return need_restart
|
||||
|
||||
|
||||
def _install_mta_sts_daemon() -> bool:
|
||||
need_restart = False
|
||||
|
||||
@@ -301,18 +202,6 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
|
||||
)
|
||||
need_restart |= header_cleanup.changed
|
||||
|
||||
# Login map that 1:1 maps email address to login.
|
||||
login_map = files.put(
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"postfix/login_map"
|
||||
),
|
||||
dest="/etc/postfix/login_map",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= login_map.changed
|
||||
|
||||
return need_restart
|
||||
|
||||
|
||||
@@ -418,9 +307,105 @@ def _configure_nginx(domain: str, debug: bool = False) -> bool:
|
||||
return need_restart
|
||||
|
||||
|
||||
def _remove_rspamd() -> None:
|
||||
"""Remove rspamd"""
|
||||
apt.packages(name="Remove rspamd", packages="rspamd", present=False)
|
||||
def remove_opendkim() -> None:
|
||||
"""Remove OpenDKIM, deprecated"""
|
||||
files.file(
|
||||
name="Remove legacy opendkim.conf",
|
||||
path="/etc/opendkim.conf",
|
||||
present=False,
|
||||
)
|
||||
|
||||
files.directory(
|
||||
name="Remove legacy opendkim socket directory from /var/spool/postfix",
|
||||
path="/var/spool/postfix/opendkim",
|
||||
present=False,
|
||||
)
|
||||
|
||||
apt.packages(name="Remove openDKIM", packages="opendkim", present=False)
|
||||
|
||||
|
||||
def _configure_rspamd(dkim_selector: str, mail_domain: str) -> bool:
|
||||
"""Configures rspamd for Rate Limiting."""
|
||||
need_restart = False
|
||||
|
||||
apt.packages(
|
||||
name="apt install rspamd",
|
||||
packages="rspamd",
|
||||
)
|
||||
|
||||
for module in ["phishing", "rbl", "hfilter", "ratelimit"]:
|
||||
disabled_module_conf = files.put(
|
||||
name=f"disable {module} rspamd plugin",
|
||||
src=importlib.resources.files(__package__).joinpath("rspamd/disabled.conf"),
|
||||
dest=f"/etc/rspamd/local.d/{module}.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= disabled_module_conf.changed
|
||||
|
||||
options_inc = files.put(
|
||||
name="disable fuzzy checks",
|
||||
src=importlib.resources.files(__package__).joinpath("rspamd/options.inc"),
|
||||
dest="/etc/rspamd/local.d/options.inc",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= options_inc.changed
|
||||
|
||||
# https://rspamd.com/doc/modules/force_actions.html
|
||||
force_actions_conf = files.put(
|
||||
name="Set up rules to reject on DKIM, SPF and DMARC fails",
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"rspamd/force_actions.conf"
|
||||
),
|
||||
dest="/etc/rspamd/local.d/force_actions.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
)
|
||||
need_restart |= force_actions_conf.changed
|
||||
|
||||
dkim_directory = "/var/lib/rspamd/dkim/"
|
||||
dkim_key_path = f"{dkim_directory}{mail_domain}.{dkim_selector}.key"
|
||||
dkim_dns_file = f"{dkim_directory}{mail_domain}.{dkim_selector}.zone"
|
||||
|
||||
dkim_config = files.template(
|
||||
src=importlib.resources.files(__package__).joinpath(
|
||||
"rspamd/dkim_signing.conf.j2"
|
||||
),
|
||||
dest="/etc/rspamd/local.d/dkim_signing.conf",
|
||||
user="root",
|
||||
group="root",
|
||||
mode="644",
|
||||
config={
|
||||
"dkim_selector": str(dkim_selector),
|
||||
"mail_domain": mail_domain,
|
||||
"dkim_key_path": dkim_key_path,
|
||||
},
|
||||
)
|
||||
need_restart |= dkim_config.changed
|
||||
|
||||
files.directory(
|
||||
name="ensure DKIM key directory exists",
|
||||
path=dkim_directory,
|
||||
present=True,
|
||||
user="_rspamd",
|
||||
group="_rspamd",
|
||||
)
|
||||
|
||||
if not host.get_fact(File, dkim_key_path):
|
||||
server.shell(
|
||||
name="Generate DKIM domain keys with rspamd",
|
||||
commands=[
|
||||
f"rspamadm dkim_keygen -b 2048 -s {dkim_selector} -d {mail_domain} -k {dkim_key_path} > {dkim_dns_file}"
|
||||
],
|
||||
_sudo=True,
|
||||
_sudo_user="_rspamd",
|
||||
)
|
||||
|
||||
return need_restart
|
||||
|
||||
|
||||
def check_config(config):
|
||||
@@ -459,10 +444,7 @@ def deploy_chatmail(config_path: Path) -> None:
|
||||
)
|
||||
server.shell(
|
||||
name="Generate root keys for validating DNSSEC",
|
||||
commands=[
|
||||
"unbound-anchor -a /var/lib/unbound/root.key || true",
|
||||
"systemctl reset-failed unbound.service",
|
||||
],
|
||||
commands=["unbound-anchor -a /var/lib/unbound/root.key || true"],
|
||||
)
|
||||
systemd.service(
|
||||
name="Start and enable unbound",
|
||||
@@ -511,15 +493,15 @@ def deploy_chatmail(config_path: Path) -> None:
|
||||
mta_sts_need_restart = _install_mta_sts_daemon()
|
||||
nginx_need_restart = _configure_nginx(mail_domain)
|
||||
|
||||
_remove_rspamd()
|
||||
opendkim_need_restart = _configure_opendkim(mail_domain, "opendkim")
|
||||
remove_opendkim()
|
||||
rspamd_need_restart = _configure_rspamd("dkim", mail_domain)
|
||||
|
||||
systemd.service(
|
||||
name="Start and enable OpenDKIM",
|
||||
service="opendkim.service",
|
||||
name="Start and enable rspamd",
|
||||
service="rspamd.service",
|
||||
running=True,
|
||||
enabled=True,
|
||||
restarted=opendkim_need_restart,
|
||||
restarted=rspamd_need_restart,
|
||||
)
|
||||
|
||||
systemd.service(
|
||||
|
||||
@@ -7,10 +7,9 @@ _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}. TXT "v=spf1 a:{chatmail_domain} -all"
|
||||
_dmarc.{chatmail_domain}. TXT "v=DMARC1;p=reject;adkim=s;aspf=s"
|
||||
_dmarc.{chatmail_domain}. TXT "v=DMARC1;p=reject;rua=mailto:{email};ruf=mailto:{email};fo=1;adkim=s;aspf=s"
|
||||
_mta-sts.{chatmail_domain}. TXT "v=STSv1; id={sts_id}"
|
||||
mta-sts.{chatmail_domain}. CNAME {chatmail_domain}.
|
||||
www.{chatmail_domain}. CNAME {chatmail_domain}.
|
||||
_smtp._tls.{chatmail_domain}. TXT "v=TLSRPTv1;rua=mailto:{email}"
|
||||
{dkim_entry}
|
||||
_adsp._domainkey.{chatmail_domain}. TXT "dkim=discardable"
|
||||
|
||||
@@ -82,8 +82,7 @@ def dns_cmd_options(parser):
|
||||
|
||||
def dns_cmd(args, out):
|
||||
"""Generate dns zone file."""
|
||||
exit_code = show_dns(args, out)
|
||||
exit(exit_code)
|
||||
show_dns(args, out)
|
||||
|
||||
|
||||
def status_cmd(args, out):
|
||||
|
||||
@@ -47,8 +47,7 @@ class DNS:
|
||||
return result == f"{mail_domain}."
|
||||
|
||||
|
||||
def show_dns(args, out) -> int:
|
||||
"""Check existing DNS records, optionally write them to zone file, return exit code 0 or 1."""
|
||||
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}"
|
||||
@@ -61,6 +60,9 @@ def show_dns(args, out) -> int:
|
||||
continue
|
||||
line = line.replace("\t", " ")
|
||||
lines.append(line)
|
||||
lines[0] = f"dkim._domainkey.{mail_domain}. IN TXT " + lines[0].strip(
|
||||
"dkim._domainkey IN TXT "
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
print("Checking your DKIM keys and DNS entries...")
|
||||
@@ -68,8 +70,10 @@ def show_dns(args, out) -> int:
|
||||
acme_account_url = out.shell_output(f"{ssh} -- acmetool account-url")
|
||||
except subprocess.CalledProcessError:
|
||||
print("Please run `cmdeploy run` first.")
|
||||
return 1
|
||||
dkim_entry = read_dkim_entries(out.shell_output(f"{ssh} -- opendkim-genzone -F"))
|
||||
return
|
||||
dkim_entry = read_dkim_entries(
|
||||
out.shell_output(f"{ssh} -- cat /var/lib/rspamd/dkim/{mail_domain}.dkim.zone")
|
||||
)
|
||||
|
||||
ipv6 = dns.get_ipv6()
|
||||
reverse_ipv6 = dns.check_ptr_record(ipv6, mail_domain)
|
||||
@@ -95,7 +99,7 @@ def show_dns(args, out) -> int:
|
||||
with open(args.zonefile, "w+") as zf:
|
||||
zf.write(zonefile)
|
||||
print(f"DNS records successfully written to: {args.zonefile}")
|
||||
return 0
|
||||
return
|
||||
except TypeError:
|
||||
pass
|
||||
started_dkim_parsing = False
|
||||
@@ -135,7 +139,7 @@ def show_dns(args, out) -> int:
|
||||
continue
|
||||
if current != value:
|
||||
to_print.append(line)
|
||||
if "IN TXT ( " in line:
|
||||
if " IN TXT ( " in line:
|
||||
started_dkim_parsing = True
|
||||
dkim_lines = [line]
|
||||
if started_dkim_parsing and line.startswith('"'):
|
||||
@@ -143,13 +147,12 @@ def show_dns(args, out) -> int:
|
||||
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:
|
||||
current = "( %s" % (current.replace('" "', '"\n "'))
|
||||
if current != data:
|
||||
to_print.append(dkim_entry)
|
||||
else:
|
||||
to_print.append(dkim_entry)
|
||||
|
||||
exit_code = 0
|
||||
if to_print:
|
||||
to_print.insert(
|
||||
0, "You should configure the following DNS entries at your provider:\n"
|
||||
@@ -158,7 +161,6 @@ def show_dns(args, out) -> int:
|
||||
"\nIf you already configured the DNS entries, wait a bit until the DNS entries propagate to the Internet."
|
||||
)
|
||||
print("\n".join(to_print))
|
||||
exit_code = 1
|
||||
else:
|
||||
out.green("Great! All your DNS entries are correct.")
|
||||
|
||||
@@ -178,8 +180,6 @@ def show_dns(args, out) -> int:
|
||||
print(
|
||||
"You can do so at your hosting provider (maybe this isn't your DNS provider)."
|
||||
)
|
||||
exit_code = 1
|
||||
return exit_code
|
||||
|
||||
|
||||
def check_necessary_dns(out, mail_domain):
|
||||
|
||||
@@ -13,15 +13,13 @@ auth_cache_size = 100M
|
||||
mail_debug = yes
|
||||
{% endif %}
|
||||
|
||||
mail_server_admin = mailto:root@{{ config.mail_domain }}
|
||||
mail_server_comment = Chatmail server
|
||||
|
||||
mail_plugins = quota
|
||||
|
||||
# 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
|
||||
imap_capability = IMAP4rev1 IDLE MOVE QUOTA CONDSTORE NOTIFY
|
||||
|
||||
|
||||
# Authentication for system users.
|
||||
@@ -75,7 +73,6 @@ mail_privileged_group = vmail
|
||||
# <https://datatracker.ietf.org/doc/html/rfc4978.html>
|
||||
protocol imap {
|
||||
mail_plugins = $mail_plugins imap_zlib imap_quota
|
||||
imap_metadata = yes
|
||||
}
|
||||
|
||||
protocol lmtp {
|
||||
|
||||
@@ -6,7 +6,7 @@ import io
|
||||
|
||||
|
||||
def gen_qr_png_data(maildomain):
|
||||
url = f"DCACCOUNT:https://{maildomain}/new"
|
||||
url = f"DCACCOUNT:https://{maildomain}/cgi-bin/newemail.py"
|
||||
image = gen_qr(maildomain, url)
|
||||
temp = io.BytesIO()
|
||||
image.save(temp, format="png")
|
||||
|
||||
@@ -45,33 +45,8 @@ http {
|
||||
default_type text/plain;
|
||||
}
|
||||
|
||||
location /new {
|
||||
if ($request_method = GET) {
|
||||
# Redirect to Delta Chat,
|
||||
# which will in turn do a POST request.
|
||||
return 301 dcaccount:https://{{ config.domain_name }}/new;
|
||||
}
|
||||
|
||||
fastcgi_pass unix:/run/fcgiwrap.socket;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py;
|
||||
}
|
||||
|
||||
# Old URL for compatibility with e.g. printed QR codes.
|
||||
#
|
||||
# Copy-paste instead of redirect to /new
|
||||
# because Delta Chat core does not follow redirects.
|
||||
#
|
||||
# Redirects are only for browsers.
|
||||
location /cgi-bin/newemail.py {
|
||||
if ($request_method = GET) {
|
||||
return 301 dcaccount:https://{{ config.domain_name }}/new;
|
||||
}
|
||||
|
||||
fastcgi_pass unix:/run/fcgiwrap.socket;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/lib/cgi-bin/newemail.py;
|
||||
}
|
||||
# add cgi-bin support
|
||||
include /usr/share/doc/fcgiwrap/examples/nginx.conf;
|
||||
}
|
||||
|
||||
# Redirect www. to non-www
|
||||
|
||||
@@ -1 +1 @@
|
||||
{{ config.opendkim_selector }}._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/{{ config.opendkim_selector }}.private
|
||||
dkim._domainkey.{{ config.domain_name }} {{ config.domain_name }}:{{ config.opendkim_selector }}:/etc/dkimkeys/dkim.private
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
if odkim.internal_ip(ctx) == 1 then
|
||||
-- Outgoing message will be signed,
|
||||
-- no need to look for signatures.
|
||||
return nil
|
||||
end
|
||||
|
||||
nsigs = odkim.get_sigcount(ctx)
|
||||
if nsigs == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, nsigs do
|
||||
sig = odkim.get_sighandle(ctx, i - 1)
|
||||
sigres = odkim.sig_result(sig)
|
||||
|
||||
-- All signatures that do not correspond to From:
|
||||
-- were ignored in screen.lua and return sigres -1.
|
||||
--
|
||||
-- Any valid signature that was not ignored like this
|
||||
-- means the message is acceptable.
|
||||
if sigres == 0 then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
odkim.set_reply(ctx, "554", "5.7.1", "No valid DKIM signature found")
|
||||
odkim.set_result(ctx, SMFIS_REJECT)
|
||||
return nil
|
||||
@@ -8,12 +8,10 @@ SyslogSuccess yes
|
||||
# oversigned, because it is often the identity key used by reputation systems
|
||||
# and thus somewhat security sensitive.
|
||||
Canonicalization relaxed/simple
|
||||
#Mode sv
|
||||
#SubDomains no
|
||||
OversignHeaders From
|
||||
|
||||
On-BadSignature reject
|
||||
On-KeyNotFound reject
|
||||
On-NoSignature reject
|
||||
|
||||
# Signing domain, selector, and key (required). For example, perform signing
|
||||
# for domain "example.com" with selector "2020" (2020._domainkey.example.com),
|
||||
# using the private key stored in /etc/dkimkeys/example.private. More granular
|
||||
@@ -24,15 +22,6 @@ KeyFile /etc/dkimkeys/{{ config.opendkim_selector }}.private
|
||||
KeyTable /etc/dkimkeys/KeyTable
|
||||
SigningTable refile:/etc/dkimkeys/SigningTable
|
||||
|
||||
# Sign Autocrypt header in addition to the default specified in RFC 6376.
|
||||
SignHeaders *,+autocrypt
|
||||
|
||||
# Script to ignore signatures that do not correspond to the From: domain.
|
||||
ScreenPolicyScript /etc/opendkim/screen.lua
|
||||
|
||||
# Script to reject mails without a valid DKIM signature.
|
||||
FinalPolicyScript /etc/opendkim/final.lua
|
||||
|
||||
# In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
|
||||
# using a local socket with MTAs that access the socket as a non-privileged
|
||||
# user (for example, Postfix). You may need to add user "postfix" to group
|
||||
@@ -40,10 +29,22 @@ FinalPolicyScript /etc/opendkim/final.lua
|
||||
UserID opendkim
|
||||
UMask 007
|
||||
|
||||
# Socket for the MTA connection (required). If the MTA is inside a chroot jail,
|
||||
# it must be ensured that the socket is accessible. In Debian, Postfix runs in
|
||||
# a chroot in /var/spool/postfix, therefore a Unix socket would have to be
|
||||
# configured as shown on the last line below.
|
||||
#Socket local:/run/opendkim/opendkim.sock
|
||||
#Socket inet:8891@localhost
|
||||
#Socket inet:8891
|
||||
Socket local:/var/spool/postfix/opendkim/opendkim.sock
|
||||
|
||||
PidFile /run/opendkim/opendkim.pid
|
||||
|
||||
# Hosts for which to sign rather than verify, default is 127.0.0.1. See the
|
||||
# OPERATION section of opendkim(8) for more information.
|
||||
#InternalHosts 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12
|
||||
|
||||
# The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided
|
||||
# by the package dns-root-data.
|
||||
TrustAnchorFile /usr/share/dns/root.key
|
||||
#Nameservers 127.0.0.1
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- Ignore signatures that do not correspond to the From: domain.
|
||||
|
||||
from_domain = odkim.get_fromdomain(ctx)
|
||||
if from_domain == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
n = odkim.get_sigcount(ctx)
|
||||
if n == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i = 1, n do
|
||||
sig = odkim.get_sighandle(ctx, i - 1)
|
||||
sig_domain = odkim.sig_getdomain(sig)
|
||||
if from_domain ~= sig_domain then
|
||||
odkim.sig_ignore(sig)
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
@@ -1 +0,0 @@
|
||||
/^(.*)$/ ${1}
|
||||
@@ -46,9 +46,7 @@ inet_protocols = all
|
||||
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||
virtual_mailbox_domains = {{ config.mail_domain }}
|
||||
|
||||
mua_client_restrictions = permit_sasl_authenticated, reject
|
||||
mua_sender_restrictions = reject_sender_login_mismatch, permit_sasl_authenticated, reject
|
||||
mua_helo_restrictions = permit_mynetworks, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, permit
|
||||
smtpd_milters = inet:127.0.0.1:11332
|
||||
non_smtpd_milters = $smtpd_milters
|
||||
|
||||
# 1:1 map MAIL FROM to SASL login name.
|
||||
smtpd_sender_login_maps = regexp:/etc/postfix/login_map
|
||||
header_checks = regexp:/etc/postfix/submission_header_cleanup
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
# ==========================================================================
|
||||
{% if debug == true %}
|
||||
smtp inet n - y - - smtpd -v
|
||||
{%- else %}
|
||||
{% else %}
|
||||
smtp inet n - y - - smtpd
|
||||
{%- endif %}
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
{% 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
|
||||
@@ -31,7 +34,6 @@ submission inet n - y - - smtpd
|
||||
-o milter_macro_daemon_name=ORIGINATING
|
||||
-o smtpd_client_connection_count_limit=1000
|
||||
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
|
||||
-o cleanup_service_name=authclean
|
||||
smtps inet n - y - - smtpd
|
||||
-o syslog_name=postfix/smtps
|
||||
-o smtpd_tls_wrappermode=yes
|
||||
@@ -48,7 +50,6 @@ smtps inet n - y - - smtpd
|
||||
-o smtpd_client_connection_count_limit=1000
|
||||
-o milter_macro_daemon_name=ORIGINATING
|
||||
-o smtpd_proxy_filter=127.0.0.1:{{ config.filtermail_smtp_port }}
|
||||
-o cleanup_service_name=authclean
|
||||
#628 inet n - y - - qmqpd
|
||||
pickup unix n - y 60 1 pickup
|
||||
cleanup unix n - y - 0 cleanup
|
||||
@@ -79,14 +80,3 @@ filter unix - n n - - lmtp
|
||||
# Local SMTP server for reinjecting filered mail.
|
||||
localhost:{{ config.postfix_reinject_port }} inet n - n - 10 smtpd
|
||||
-o syslog_name=postfix/reinject
|
||||
-o smtpd_milters=unix:opendkim/opendkim.sock
|
||||
-o cleanup_service_name=authclean
|
||||
|
||||
# Cleanup `Received` headers for authenticated mail
|
||||
# to avoid leaking client IP.
|
||||
#
|
||||
# We do not do this for received mails
|
||||
# as this will break DKIM signatures
|
||||
# if `Received` header is signed.
|
||||
authclean unix n - - - 0 cleanup
|
||||
-o header_checks=regexp:/etc/postfix/submission_header_cleanup
|
||||
|
||||
1
cmdeploy/src/cmdeploy/rspamd/disabled.conf
Normal file
1
cmdeploy/src/cmdeploy/rspamd/disabled.conf
Normal file
@@ -0,0 +1 @@
|
||||
enabled = false;
|
||||
10
cmdeploy/src/cmdeploy/rspamd/dkim_signing.conf.j2
Normal file
10
cmdeploy/src/cmdeploy/rspamd/dkim_signing.conf.j2
Normal file
@@ -0,0 +1,10 @@
|
||||
selector = {{ config.dkim_selector }}
|
||||
use_esld = false # don't cut c1.testrun.org down to testrun.org
|
||||
domain = {
|
||||
{{ config.mail_domain }} {
|
||||
selectors [
|
||||
selector = {{ config.dkim_selector }}
|
||||
path = {{ config.dkim_key_path }}
|
||||
]
|
||||
}
|
||||
}
|
||||
30
cmdeploy/src/cmdeploy/rspamd/force_actions.conf
Normal file
30
cmdeploy/src/cmdeploy/rspamd/force_actions.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
rules {
|
||||
REJECT_DKIM_SPF {
|
||||
action = "reject";
|
||||
# Reject if
|
||||
# - R_DKIM_RJECT: DKIM reject inserted by `dkim` module.
|
||||
# - R_DKIM_PERMFAIL: permanent failure inserted by `dkim` module e.g. no DKIM DNS record found.
|
||||
# - No DKIM signing (R_DKIM_NA symbol inserted by `dkim` module)
|
||||
#
|
||||
# - SPF failure (R_SPF_FAIL)
|
||||
# - SPF permanent failure, e.g. failed to resolve DNS record referenced from SPF (R_SPF_PERMFAIL)
|
||||
#
|
||||
# - DMARC policy failure (DMARC_POLICY_REJECT)
|
||||
#
|
||||
# Do not reject if:
|
||||
# - R_DKIM_TEMPFAIL, it is a DNS resolution failure
|
||||
# and we do not want to lose messages because of faulty network.
|
||||
#
|
||||
# - R_SPF_SOFTFAIL
|
||||
# - R_SPF_NEUTRAL
|
||||
# - R_SPF_DNSFAIL
|
||||
# - R_SPF_NA
|
||||
#
|
||||
# - DMARC_DNSFAIL
|
||||
# - DMARC_NA
|
||||
# - DMARC_POLICY_SOFTFAIL
|
||||
# - DMARC_POLICY_QUARANTINE
|
||||
# - DMARC_BAD_POLICY
|
||||
expression = "R_DKIM_REJECT | R_DKIM_PERMFAIL | R_DKIM_NA | R_SPF_FAIL | R_SPF_PERMFAIL | DMARC_POLICY_REJECT";
|
||||
}
|
||||
}
|
||||
1
cmdeploy/src/cmdeploy/rspamd/options.inc
Normal file
1
cmdeploy/src/cmdeploy/rspamd/options.inc
Normal file
@@ -0,0 +1 @@
|
||||
filters = "dkim";
|
||||
@@ -9,7 +9,7 @@ def test_gen_qr_png_data(maildomain):
|
||||
|
||||
|
||||
def test_fastcgi_working(maildomain, chatmail_config):
|
||||
url = f"https://{maildomain}/new"
|
||||
url = f"https://{maildomain}/cgi-bin/newemail.py"
|
||||
print(url)
|
||||
res = requests.post(url)
|
||||
assert maildomain in res.json().get("email")
|
||||
@@ -18,7 +18,7 @@ def test_fastcgi_working(maildomain, chatmail_config):
|
||||
|
||||
def test_newemail_configure(maildomain, rpc):
|
||||
"""Test configuring accounts by scanning a QR code works."""
|
||||
url = f"DCACCOUNT:https://{maildomain}/new"
|
||||
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)
|
||||
|
||||
@@ -42,25 +42,13 @@ def test_reject_forged_from(cmsetup, maildata, gencreds, lp, forgeaddr):
|
||||
assert "500" in str(e.value)
|
||||
|
||||
|
||||
def test_authenticated_from(cmsetup, maildata):
|
||||
"""Test that envelope FROM must be the same as login."""
|
||||
user1, user2, user3 = cmsetup.gen_users(3)
|
||||
|
||||
msg = maildata("encrypted.eml", from_addr=user2.addr, to_addr=user3.addr)
|
||||
with pytest.raises(smtplib.SMTPException) as e:
|
||||
user1.smtp.sendmail(
|
||||
from_addr=user2.addr, to_addrs=[user3.addr], msg=msg.as_string()
|
||||
)
|
||||
assert e.value.recipients[user3.addr][0] == 553
|
||||
|
||||
|
||||
@pytest.mark.parametrize("from_addr", ["fake@example.org", "fake@testrun.org"])
|
||||
def test_reject_missing_dkim(cmsetup, maildata, from_addr):
|
||||
"""Test that emails with missing or wrong DMARC, DKIM, and SPF entries are rejected."""
|
||||
recipient = cmsetup.gen_users(1)[0]
|
||||
msg = maildata("plain.eml", from_addr=from_addr, to_addr=recipient.addr).as_string()
|
||||
with smtplib.SMTP(cmsetup.maildomain, 25) as s:
|
||||
with pytest.raises(smtplib.SMTPDataError, match="No valid DKIM signature"):
|
||||
with pytest.raises(smtplib.SMTPDataError, match="Spam message rejected"):
|
||||
s.sendmail(from_addr=from_addr, to_addrs=recipient.addr, msg=msg)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
python3 -m venv --upgrade-deps venv
|
||||
python3 -m venv venv
|
||||
|
||||
venv/bin/pip install -e chatmaild
|
||||
venv/bin/pip install -e cmdeploy
|
||||
|
||||
@@ -7,7 +7,7 @@ Welcome to instant, interoperable and [privacy-preserving](privacy.html) messagi
|
||||
|
||||
👉 **Tap** or scan this QR code to get a random `@{{config.mail_domain}}` e-mail address
|
||||
|
||||
<a href="DCACCOUNT:https://{{ config.mail_domain }}/new">
|
||||
<a href="DCACCOUNT:https://{{ config.mail_domain }}/cgi-bin/newemail.py">
|
||||
<img width=300 style="float: none;" src="qr-chatmail-invite-{{config.mail_domain}}.png" /></a>
|
||||
|
||||
🐣 **Choose** your Avatar and Name
|
||||
|
||||
Reference in New Issue
Block a user