all tests pass

This commit is contained in:
holger krekel
2023-10-19 00:07:22 +02:00
parent bbd2773506
commit 0c148006e7
7 changed files with 68 additions and 43 deletions

View File

@@ -1,8 +1,8 @@
[Unit] [Unit]
Description=Email filter for chatmail servers Description=Chatmail Postfix AfterQueue filter
[Service] [Service]
ExecStart=/usr/local/bin/filtermail afterqueue /var/spool/postfix/private/filtermail ExecStart=/usr/local/bin/filtermail afterqueue /var/spool/postfix/private/filtermail-afterqueue
Restart=always Restart=always
RestartSec=30 RestartSec=30

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Chatmail Postfix BeforeQeue filter
[Service]
ExecStart=/usr/local/bin/filtermail beforequeue 10080
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

View File

@@ -9,7 +9,7 @@ from email.utils import parseaddr
from aiosmtpd.lmtp import LMTP from aiosmtpd.lmtp import LMTP
from aiosmtpd.smtp import SMTP from aiosmtpd.smtp import SMTP
from aiosmtpd.controller import UnixSocketController from aiosmtpd.controller import UnixSocketController, Controller
from smtplib import SMTP as SMTPClient from smtplib import SMTP as SMTPClient
@@ -35,10 +35,7 @@ def check_encrypted(message):
return True return True
class BeforeQueueHandler: class BeforeQueueHandler:
transport_class = SMTP
def __init__(self): def __init__(self):
self.send_rate_limiter = SendRateLimiter() self.send_rate_limiter = SendRateLimiter()
@@ -47,7 +44,13 @@ class BeforeQueueHandler:
if self.send_rate_limiter.is_sending_allowed(address): if self.send_rate_limiter.is_sending_allowed(address):
envelope.mail_from = address envelope.mail_from = address
return "250 OK" return "250 OK"
return "400 per-user ratelimit exceeded" return f"450 4.7.1: Too much mail from {address}"
async def handle_DATA(self, server, session, envelope):
logging.info("handle_DATA before-queue: re-injecting the mail")
client = SMTPClient("localhost", "10026")
client.sendmail(envelope.mail_from, envelope.rcpt_tos, envelope.content)
return "250 OK"
class SendRateLimiter: class SendRateLimiter:
@@ -67,8 +70,6 @@ class SendRateLimiter:
class AfterQueueHandler: class AfterQueueHandler:
transport_class = LMTP
async def handle_RCPT(self, server, session, envelope, address, rcpt_options): async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
envelope.rcpt_tos.append(address) envelope.rcpt_tos.append(address)
return "250 OK" return "250 OK"
@@ -77,8 +78,8 @@ class AfterQueueHandler:
valid_recipients, res = lmtp_handle_DATA(envelope) valid_recipients, res = lmtp_handle_DATA(envelope)
# Reinject the mail back into Postfix. # Reinject the mail back into Postfix.
if valid_recipients: if valid_recipients:
logging.info("Reinjecting the mail") logging.info("afterqueue: re-injecting the mail")
client = SMTPClient("localhost", "10026") client = SMTPClient("localhost", "10027")
client.sendmail(envelope.mail_from, valid_recipients, envelope.content) client.sendmail(envelope.mail_from, valid_recipients, envelope.content)
else: else:
logging.info("no valid recipients, ignoring mail") logging.info("no valid recipients, ignoring mail")
@@ -137,25 +138,35 @@ def lmtp_handle_DATA(envelope):
return valid_recipients, res return valid_recipients, res
class Controller(UnixSocketController): class UnixController(UnixSocketController):
def factory(self): def factory(self):
return self.handler.transport_class(self.handler, **self.SMTP_kwargs) return LMTP(self.handler, **self.SMTP_kwargs)
async def asyncmain(loop, handler, unix_socket_fn): class SMTPController(Controller):
Controller(handler, unix_socket=unix_socket_fn).start() def factory(self):
return SMTP(self.handler, **self.SMTP_kwargs)
name2Handler = {"beforequeue": BeforeQueueHandler, "afterqueue": AfterQueueHandler} async def asyncmain_afterqueue(loop, unix_socket_fn):
UnixController(AfterQueueHandler(), unix_socket=unix_socket_fn).start()
async def asyncmain_beforequeue(loop, port):
Controller(BeforeQueueHandler(), hostname="127.0.0.1", port=port).start()
def main(): def main():
args = sys.argv[1:] args = sys.argv[1:]
assert len(args) == 2 assert len(args) == 2
handler = name2Handler[args[0]]()
unix_socket_fn = args[1]
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop.create_task(asyncmain(loop, handler, unix_socket_fn)) if args[0] == "afterqueue":
task = asyncmain_afterqueue(loop, args[1])
elif args[0] == "beforequeue":
task = asyncmain_beforequeue(loop, port=int(args[1]))
else:
raise SystemExit(1)
loop.create_task(task)
loop.run_forever() loop.run_forever()

View File

@@ -339,4 +339,3 @@ def test_send_rate_limiter():
else: else:
assert i == SendRateLimiter.MAX_USER_SEND_PER_MINUTE + 1 assert i == SendRateLimiter.MAX_USER_SEND_PER_MINUTE + 1
break break

View File

@@ -53,24 +53,25 @@ def _install_chatmaild() -> None:
daemon_reload=True, daemon_reload=True,
) )
files.put( for fn in ("filtermail-after", "filtermail-before"):
name="upload filtermail.service", files.put(
src=importlib.resources.files("chatmaild") name=f"upload {fn}.service",
.joinpath("filtermail.service") src=importlib.resources.files("chatmaild")
.open("rb"), .joinpath(f"{fn}.service")
dest="/etc/systemd/system/filtermail.service", .open("rb"),
user="root", dest=f"/etc/systemd/system/{fn}.service",
group="root", user="root",
mode="644", group="root",
) mode="644",
systemd.service( )
name="Setup filtermail service", systemd.service(
service="filtermail.service", name=f"Setup {fn} service",
running=True, service=f"{fn}.service",
enabled=True, running=True,
restarted=True, enabled=True,
daemon_reload=True, restarted=True,
) daemon_reload=True,
)
def _configure_opendkim(domain: str, dkim_selector: str) -> bool: def _configure_opendkim(domain: str, dkim_selector: str) -> bool:

View File

@@ -32,7 +32,8 @@ 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/filtermail -o smtpd_proxy_filter=127.0.0.1:10080
-o content_filter=filter:unix:private/filtermail-afterqueue
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
@@ -47,7 +48,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 -o smtpd_proxy_filter=127.0.0.1:10080
#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
@@ -77,4 +78,6 @@ postlog unix-dgram n - n - 1 postlogd
filter unix - n n - - lmtp filter unix - n n - - lmtp
# Local SMTP server for reinjecting filered mail. # Local SMTP server for reinjecting filered mail.
localhost:10026 inet n - n - 10 smtpd localhost:10026 inet n - n - 10 smtpd
-o content_filter=filter:unix:private/filtermail-afterqueue
localhost:10027 inet n - n - 10 smtpd
-o content_filter= -o content_filter=

View File

@@ -46,10 +46,11 @@ def test_exceed_rate_limit(cmsetup, gencreds, mailgen):
print("Sending mail", str(i)) print("Sending mail", str(i))
try: try:
user1.smtp.sendmail(user1.addr, [user2.addr], mail) user1.smtp.sendmail(user1.addr, [user2.addr], mail)
except smtplib.SMTPSenderRefused as e: except smtplib.SMTPException as e:
if i < 80: if i < 80:
pytest.fail(f"rate limit was exceeded too early with msg {i}") pytest.fail(f"rate limit was exceeded too early with msg {i}")
assert e.smtp_code == 450 outcome = e.recipients[user2.addr]
assert b'4.7.1 Error: too much mail from' in e.smtp_error assert outcome[0] == 450
assert b'4.7.1: Too much mail from' in outcome[1]
return return
pytest.fail("Rate limit was not exceeded") pytest.fail("Rate limit was not exceeded")