mirror of
https://github.com/chatmail/relay.git
synced 2026-05-19 04:18:09 +00:00
Add filtermail
This commit is contained in:
@@ -23,10 +23,6 @@ def _install_doveauth() -> None:
|
|||||||
src=doveauth_path.open("rb"),
|
src=doveauth_path.open("rb"),
|
||||||
dest=remote_path,
|
dest=remote_path,
|
||||||
)
|
)
|
||||||
apt.packages(
|
|
||||||
name="apt install python3-pip",
|
|
||||||
packages="python3-pip",
|
|
||||||
)
|
|
||||||
# Maybe if we introduce dependencies to the doveauth package at some point, we should not install doveauth
|
# Maybe if we introduce dependencies to the doveauth package at some point, we should not install doveauth
|
||||||
# system-wide anymore. For now it's fine though.
|
# system-wide anymore. For now it's fine though.
|
||||||
server.shell(
|
server.shell(
|
||||||
@@ -35,6 +31,48 @@ def _install_doveauth() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_filtermail() -> None:
|
||||||
|
"""Setup filtermail."""
|
||||||
|
filtermail_filename = "filtermail-0.1.tar.gz"
|
||||||
|
filtermail_path = importlib.resources.files(__package__).joinpath(
|
||||||
|
f"../../../filtermail/dist/{filtermail_filename}"
|
||||||
|
)
|
||||||
|
remote_path = f"/tmp/{filtermail_filename}"
|
||||||
|
if Path(str(filtermail_path)).exists():
|
||||||
|
files.put(
|
||||||
|
name="upload local filtermail build",
|
||||||
|
src=filtermail_path.open("rb"),
|
||||||
|
dest=remote_path,
|
||||||
|
)
|
||||||
|
apt.packages(
|
||||||
|
name="apt install python3-aiosmtpd",
|
||||||
|
packages="python3-aiosmtpd",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --no-deps because aiosmtplib is installed with `apt`.
|
||||||
|
server.shell(
|
||||||
|
name="install local doveauth build with pip",
|
||||||
|
commands=[f"pip install --break-system-packages --no-deps {remote_path}"],
|
||||||
|
)
|
||||||
|
|
||||||
|
files.put(
|
||||||
|
src=importlib.resources.files(__package__)
|
||||||
|
.joinpath("../../../filtermail/filtermail.service")
|
||||||
|
.open("rb"),
|
||||||
|
dest="/etc/systemd/system/filtermail.service",
|
||||||
|
user="root",
|
||||||
|
group="root",
|
||||||
|
mode="644",
|
||||||
|
)
|
||||||
|
systemd.service(
|
||||||
|
name="Setup filtermail service",
|
||||||
|
service="filtermail.service",
|
||||||
|
running=True,
|
||||||
|
enabled=True,
|
||||||
|
restarted=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
|
def _configure_opendkim(domain: str, dkim_selector: str) -> bool:
|
||||||
"""Configures OpenDKIM"""
|
"""Configures OpenDKIM"""
|
||||||
need_restart = False
|
need_restart = False
|
||||||
@@ -172,7 +210,12 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
apt.packages(
|
||||||
|
name="apt install python3-pip",
|
||||||
|
packages="python3-pip",
|
||||||
|
)
|
||||||
_install_doveauth()
|
_install_doveauth()
|
||||||
|
_install_filtermail()
|
||||||
dovecot_need_restart = _configure_dovecot(mail_server)
|
dovecot_need_restart = _configure_dovecot(mail_server)
|
||||||
postfix_need_restart = _configure_postfix(mail_domain)
|
postfix_need_restart = _configure_postfix(mail_domain)
|
||||||
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
|
opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ submission inet n - y - - smtpd
|
|||||||
-o smtpd_recipient_restrictions=
|
-o smtpd_recipient_restrictions=
|
||||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||||
-o milter_macro_daemon_name=ORIGINATING
|
-o milter_macro_daemon_name=ORIGINATING
|
||||||
|
-o content_filter=filter:unix:private/filtemail
|
||||||
smtps inet n - y - - smtpd
|
smtps inet n - y - - smtpd
|
||||||
-o syslog_name=postfix/smtps
|
-o syslog_name=postfix/smtps
|
||||||
-o smtpd_tls_wrappermode=yes
|
-o smtpd_tls_wrappermode=yes
|
||||||
@@ -42,6 +43,7 @@ smtps inet n - y - - smtpd
|
|||||||
-o smtpd_recipient_restrictions=
|
-o smtpd_recipient_restrictions=
|
||||||
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
|
||||||
-o milter_macro_daemon_name=ORIGINATING
|
-o milter_macro_daemon_name=ORIGINATING
|
||||||
|
-o content_filter=filter:unix:private/filtermail
|
||||||
#628 inet n - y - - qmqpd
|
#628 inet n - y - - qmqpd
|
||||||
pickup unix n - y 60 1 pickup
|
pickup unix n - y 60 1 pickup
|
||||||
cleanup unix n - y - 0 cleanup
|
cleanup unix n - y - 0 cleanup
|
||||||
@@ -70,3 +72,7 @@ lmtp unix - - y - - lmtp
|
|||||||
anvil unix - - y - 1 anvil
|
anvil unix - - y - 1 anvil
|
||||||
scache unix - - y - 1 scache
|
scache unix - - y - 1 scache
|
||||||
postlog unix-dgram n - n - 1 postlogd
|
postlog unix-dgram n - n - 1 postlogd
|
||||||
|
filter unix - n n - - lmtp
|
||||||
|
# Local SMTP server for reinjecting filered mail.
|
||||||
|
localhost:10026 inet n - n - 10 smtpd
|
||||||
|
-o content_filter=
|
||||||
|
|||||||
10
filtermail/filtermail.service
Normal file
10
filtermail/filtermail.service
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Email filter for chatmail servers
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/filtermail
|
||||||
|
Restart=always
|
||||||
|
RestartSec=30
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
33
filtermail/pyproject.toml
Normal file
33
filtermail/pyproject.toml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "filtermail"
|
||||||
|
version = "0.1"
|
||||||
|
dependencies = [
|
||||||
|
"aiosmtpd"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
filtermail = "filtermail.filtermail:main"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = "-v -ra --strict-markers"
|
||||||
|
|
||||||
|
[tool.tox]
|
||||||
|
legacy_tox_ini = """
|
||||||
|
[tox]
|
||||||
|
isolated_build = true
|
||||||
|
envlist = lint
|
||||||
|
|
||||||
|
[testenv:lint]
|
||||||
|
skipdist = True
|
||||||
|
skip_install = True
|
||||||
|
deps =
|
||||||
|
ruff
|
||||||
|
black
|
||||||
|
commands =
|
||||||
|
black --quiet --check --diff src/
|
||||||
|
ruff src/
|
||||||
|
"""
|
||||||
0
filtermail/src/filtermail/__init__.py
Normal file
0
filtermail/src/filtermail/__init__.py
Normal file
85
filtermail/src/filtermail/filtermail.py
Normal file
85
filtermail/src/filtermail/filtermail.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiosmtpd.lmtp import LMTP
|
||||||
|
from aiosmtpd.controller import UnixSocketController
|
||||||
|
from smtplib import SMTP as SMTPClient
|
||||||
|
|
||||||
|
|
||||||
|
def check_encrypted(envelope):
|
||||||
|
"""https://xkcd.com/1181/"""
|
||||||
|
return "-----BEGIN PGP MESSAGE-----" in envelope.content.decode(
|
||||||
|
"utf8", errors="replace"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleController(UnixSocketController):
|
||||||
|
def factory(self):
|
||||||
|
return LMTP(self.handler, **self.SMTP_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleHandler:
|
||||||
|
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
|
||||||
|
envelope.rcpt_tos.append(address)
|
||||||
|
return "250 OK"
|
||||||
|
|
||||||
|
async def handle_DATA(self, server, session, envelope):
|
||||||
|
logging.info("Processing DATA message from %s", envelope.mail_from)
|
||||||
|
|
||||||
|
valid_recipients = []
|
||||||
|
|
||||||
|
mail_encrypted = check_encrypted(envelope)
|
||||||
|
|
||||||
|
res = []
|
||||||
|
for recipient in envelope.rcpt_tos:
|
||||||
|
my_local_domain = envelope.mail_from.split("@")
|
||||||
|
if len(my_local_domain) != 2:
|
||||||
|
res += [f"500 Invalid from address <{envelope.mail_from}>"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if envelope.mail_from == recipient:
|
||||||
|
# Always allow sending emails to self.
|
||||||
|
valid_recipients += [recipient]
|
||||||
|
res += ["250 OK"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
recipient_local_domain = recipient.split("@")
|
||||||
|
if len(recipient_local_domain) != 2:
|
||||||
|
res += [f"500 Invalid address <{recipient}>"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_outgoing = recipient_local_domain[1] != my_local_domain[1]
|
||||||
|
if is_outgoing and not mail_encrypted:
|
||||||
|
res += ["500 Outgoing mail must be encrypted"]
|
||||||
|
continue
|
||||||
|
|
||||||
|
valid_recipients += [recipient]
|
||||||
|
res += ["250 OK"]
|
||||||
|
|
||||||
|
# Reinject the mail back into Postfix.
|
||||||
|
if valid_recipients:
|
||||||
|
logging.info("Reinjecting the mail")
|
||||||
|
client = SMTPClient("localhost", "10026")
|
||||||
|
client.sendmail(envelope.mail_from, valid_recipients, envelope.content)
|
||||||
|
|
||||||
|
return "\r\n".join(res)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncmain(loop):
|
||||||
|
controller = ExampleController(
|
||||||
|
ExampleHandler(), unix_socket="/var/spool/postfix/private/filtermail"
|
||||||
|
)
|
||||||
|
controller.start()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
loop.create_task(asyncmain(loop=loop))
|
||||||
|
loop.run_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
10
plan.txt
10
plan.txt
@@ -11,14 +11,12 @@
|
|||||||
4. automatic expiry of users that haven't logged in for N days
|
4. automatic expiry of users that haven't logged in for N days
|
||||||
|
|
||||||
|
|
||||||
## Postfix goals/steps
|
## Filtermail
|
||||||
|
|
||||||
1. block all outgoing mails with our own LMTP program
|
1. Only allow (outgoing) mails if secure-join or autocrypt-pgp-encrypted format.
|
||||||
|
Currently only checks for "-----BEGIN PGP MESSAGE-----".
|
||||||
|
|
||||||
2. only allow (outgoing) mails if secure-join or autocrypt-pgp-encrypted format
|
2. basic outgoing send rate/limits (depending on "account-rating")
|
||||||
(probably via an lmtp service)
|
|
||||||
|
|
||||||
3. basic outgoing send rate/limits (depending on "account-rating")
|
|
||||||
|
|
||||||
|
|
||||||
## online tests (first with plain python/pytest)
|
## online tests (first with plain python/pytest)
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
|
: ${CHATMAIL_DOMAIN:=c1.testrun.org}
|
||||||
export CHATMAIL_DOMAIN
|
export CHATMAIL_DOMAIN
|
||||||
cd doveauth
|
|
||||||
|
pushd doveauth
|
||||||
venv/bin/python3 -m build
|
venv/bin/python3 -m build
|
||||||
../chatmail-pyinfra/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" ../deploy.py
|
popd
|
||||||
rm -r dist/
|
|
||||||
|
pushd filtermail
|
||||||
|
venv/bin/python3 -m build
|
||||||
|
popd
|
||||||
|
|
||||||
|
chatmail-pyinfra/venv/bin/pyinfra --ssh-user root "$CHATMAIL_DOMAIN" deploy.py
|
||||||
|
|
||||||
|
#rm -r doveauth/dist/
|
||||||
|
#rm -r filtermail/dist/
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ doveauth/venv/bin/pip install -e doveauth
|
|||||||
|
|
||||||
python3 -m venv online-tests/venv
|
python3 -m venv online-tests/venv
|
||||||
online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat
|
online-tests/venv/bin/pip install pytest pytest-timeout pdbpp deltachat
|
||||||
|
|
||||||
|
python3 -m venv filtermail/venv
|
||||||
|
filtermail/venv/bin/pip install build
|
||||||
|
filtermail/venv/bin/pip install -e filtermail
|
||||||
|
|||||||
Reference in New Issue
Block a user