diff --git a/chatmaild/src/chatmaild/filtermail.service b/chatmaild/src/chatmaild/filtermail-after.service similarity index 65% rename from chatmaild/src/chatmaild/filtermail.service rename to chatmaild/src/chatmaild/filtermail-after.service index 0e6a9a18..ccd2772e 100644 --- a/chatmaild/src/chatmaild/filtermail.service +++ b/chatmaild/src/chatmaild/filtermail-after.service @@ -1,8 +1,8 @@ [Unit] -Description=Email filter for chatmail servers +Description=Chatmail Postfix AfterQueue filter [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 RestartSec=30 diff --git a/chatmaild/src/chatmaild/filtermail-before.service b/chatmaild/src/chatmaild/filtermail-before.service new file mode 100644 index 00000000..0351c0b8 --- /dev/null +++ b/chatmaild/src/chatmaild/filtermail-before.service @@ -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 diff --git a/chatmaild/src/chatmaild/filtermail.py b/chatmaild/src/chatmaild/filtermail.py index 76777f51..15439bfc 100644 --- a/chatmaild/src/chatmaild/filtermail.py +++ b/chatmaild/src/chatmaild/filtermail.py @@ -9,7 +9,7 @@ from email.utils import parseaddr from aiosmtpd.lmtp import LMTP from aiosmtpd.smtp import SMTP -from aiosmtpd.controller import UnixSocketController +from aiosmtpd.controller import UnixSocketController, Controller from smtplib import SMTP as SMTPClient @@ -35,10 +35,7 @@ def check_encrypted(message): return True - class BeforeQueueHandler: - transport_class = SMTP - def __init__(self): self.send_rate_limiter = SendRateLimiter() @@ -47,7 +44,13 @@ class BeforeQueueHandler: if self.send_rate_limiter.is_sending_allowed(address): envelope.mail_from = address 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: @@ -67,8 +70,6 @@ class SendRateLimiter: class AfterQueueHandler: - transport_class = LMTP - async def handle_RCPT(self, server, session, envelope, address, rcpt_options): envelope.rcpt_tos.append(address) return "250 OK" @@ -77,8 +78,8 @@ class AfterQueueHandler: valid_recipients, res = lmtp_handle_DATA(envelope) # Reinject the mail back into Postfix. if valid_recipients: - logging.info("Reinjecting the mail") - client = SMTPClient("localhost", "10026") + logging.info("afterqueue: re-injecting the mail") + client = SMTPClient("localhost", "10027") client.sendmail(envelope.mail_from, valid_recipients, envelope.content) else: logging.info("no valid recipients, ignoring mail") @@ -137,25 +138,35 @@ def lmtp_handle_DATA(envelope): return valid_recipients, res -class Controller(UnixSocketController): +class UnixController(UnixSocketController): 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): - Controller(handler, unix_socket=unix_socket_fn).start() +class SMTPController(Controller): + 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(): args = sys.argv[1:] assert len(args) == 2 - handler = name2Handler[args[0]]() - unix_socket_fn = args[1] logging.basicConfig(level=logging.INFO) loop = asyncio.new_event_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() diff --git a/chatmaild/src/chatmaild/test_filtermail.py b/chatmaild/src/chatmaild/test_filtermail.py index cc86cfa7..32452e94 100644 --- a/chatmaild/src/chatmaild/test_filtermail.py +++ b/chatmaild/src/chatmaild/test_filtermail.py @@ -339,4 +339,3 @@ def test_send_rate_limiter(): else: assert i == SendRateLimiter.MAX_USER_SEND_PER_MINUTE + 1 break - diff --git a/deploy-chatmail/src/deploy_chatmail/__init__.py b/deploy-chatmail/src/deploy_chatmail/__init__.py index 80df1844..5ca476ca 100644 --- a/deploy-chatmail/src/deploy_chatmail/__init__.py +++ b/deploy-chatmail/src/deploy_chatmail/__init__.py @@ -53,24 +53,25 @@ def _install_chatmaild() -> None: daemon_reload=True, ) - files.put( - name="upload filtermail.service", - src=importlib.resources.files("chatmaild") - .joinpath("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, - daemon_reload=True, - ) + for fn in ("filtermail-after", "filtermail-before"): + files.put( + name=f"upload {fn}.service", + src=importlib.resources.files("chatmaild") + .joinpath(f"{fn}.service") + .open("rb"), + dest=f"/etc/systemd/system/{fn}.service", + user="root", + group="root", + mode="644", + ) + systemd.service( + name=f"Setup {fn} service", + service=f"{fn}.service", + running=True, + enabled=True, + restarted=True, + daemon_reload=True, + ) def _configure_opendkim(domain: str, dkim_selector: str) -> bool: diff --git a/deploy-chatmail/src/deploy_chatmail/postfix/master.cf.j2 b/deploy-chatmail/src/deploy_chatmail/postfix/master.cf.j2 index 18345dc3..1dbac100 100644 --- a/deploy-chatmail/src/deploy_chatmail/postfix/master.cf.j2 +++ b/deploy-chatmail/src/deploy_chatmail/postfix/master.cf.j2 @@ -32,7 +32,8 @@ submission inet n - y - - smtpd -o smtpd_recipient_restrictions= -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -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 -o syslog_name=postfix/smtps -o smtpd_tls_wrappermode=yes @@ -47,7 +48,7 @@ smtps inet n - y - - smtpd -o smtpd_recipient_restrictions= -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -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 pickup unix n - y 60 1 pickup cleanup unix n - y - 0 cleanup @@ -77,4 +78,6 @@ 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=filter:unix:private/filtermail-afterqueue +localhost:10027 inet n - n - 10 smtpd -o content_filter= diff --git a/online-tests/test_0_login.py b/online-tests/test_0_login.py index 6c397b2a..e30209c5 100644 --- a/online-tests/test_0_login.py +++ b/online-tests/test_0_login.py @@ -46,10 +46,11 @@ def test_exceed_rate_limit(cmsetup, gencreds, mailgen): print("Sending mail", str(i)) try: user1.smtp.sendmail(user1.addr, [user2.addr], mail) - except smtplib.SMTPSenderRefused as e: + except smtplib.SMTPException as e: if i < 80: pytest.fail(f"rate limit was exceeded too early with msg {i}") - assert e.smtp_code == 450 - assert b'4.7.1 Error: too much mail from' in e.smtp_error + outcome = e.recipients[user2.addr] + assert outcome[0] == 450 + assert b'4.7.1: Too much mail from' in outcome[1] return pytest.fail("Rate limit was not exceeded")