Compare commits

...

9 Commits

Author SHA1 Message Date
missytake
aaed93ba78 filtermail: only remove one linebreak 2025-10-19 14:49:15 +02:00
missytake
e961ca2efb filtermail: fix lint 2025-10-19 14:49:15 +02:00
missytake
e665a6f432 filtermail: drop one more newline directly 2025-10-19 14:49:15 +02:00
missytake
75f61683fc filtermail: better performance for removing Version 2025-10-19 14:49:15 +02:00
missytake
7db26f33d9 nginx: be more specific with the server name (#636) 2025-10-19 14:02:41 +02:00
link2xt
2b90f7db37 filtermail: run CPU-intensive handle_DATA in a thread pool executor
See
<https://docs.python.org/3/library/asyncio-eventloop.html#executing-code-in-thread-or-process-pools>
for the documentation.

This should avoid processing of large messages from hogging asyncio
thread and delaying async operations like accepting new connections.
2025-10-19 10:43:11 +00:00
holger krekel
e37dd5153a remove logging and just print to sys.stderr 2025-10-18 19:50:13 +00:00
missytake
f21e4ff55b opendkim: increase DNSTimeout from 5 (default) to 60
fix #667
2025-10-17 11:27:18 +02:00
cliffmccarthy
21258a267a test: Handle Git errors in test_deployed_state()
- This is a counterpart to pull request #607.  Revised
  test_deployed_state() to perform the same error-handling on Git
  commands that cmdeploy does.  If 'git rev-parse' returns an error,
  the value "unknown" is used.  If 'git diff' returns an error, the
  null string is used.
- This fixes failures in environments where Git is not installed or
  where the .git subdirectory is not present (as long as the server
  was deployed in the same way).
2025-10-16 16:15:35 +02:00
5 changed files with 44 additions and 15 deletions

View File

@@ -2,6 +2,15 @@
## untagged ## untagged
- filtermail: run CPU-intensive handle_DATA in a thread pool executor
([#676](https://github.com/chatmail/relay/pull/676))
- don't use the complicated logging module in filtermail to exclude a potential source of errors.
([#674](https://github.com/chatmail/relay/pull/674))
- Specify nginx.conf to only handle `mail_domain`, www, and mta-sts domains
([#636](https://github.com/chatmail/relay/pull/636))
- Setup TURN server - Setup TURN server
([#621](https://github.com/chatmail/relay/pull/621)) ([#621](https://github.com/chatmail/relay/pull/621))
@@ -17,6 +26,9 @@
- Ignore all RCPT TO: parameters - Ignore all RCPT TO: parameters
([#651](https://github.com/chatmail/relay/pull/651)) ([#651](https://github.com/chatmail/relay/pull/651))
- Increase opendkim DNS Timeout from 5 to 60 seconds
([#672](https://github.com/chatmail/relay/pull/672))
- Add config parameter for Let's Encrypt ACME email - Add config parameter for Let's Encrypt ACME email
([#663](https://github.com/chatmail/relay/pull/663)) ([#663](https://github.com/chatmail/relay/pull/663))

View File

@@ -2,7 +2,6 @@
import asyncio import asyncio
import base64 import base64
import binascii import binascii
import logging
import sys import sys
import time import time
from email import policy from email import policy
@@ -105,8 +104,8 @@ def check_armored_payload(payload: str, outgoing: bool):
# Disallow comments in outgoing messages # Disallow comments in outgoing messages
version_comment = "Version: " version_comment = "Version: "
if payload.startswith(version_comment): if payload.startswith(version_comment):
version_line = payload.splitlines()[0] splitindex = payload.find("\r\n") + 2
payload = payload.removeprefix(version_line) payload = payload[splitindex:]
if outgoing: if outgoing:
return False return False
@@ -229,7 +228,7 @@ class OutgoingBeforeQueueHandler:
self.send_rate_limiter = SendRateLimiter() self.send_rate_limiter = SendRateLimiter()
async def handle_MAIL(self, server, session, envelope, address, mail_options): async def handle_MAIL(self, server, session, envelope, address, mail_options):
logging.info(f"handle_MAIL from {address}") log_info(f"handle_MAIL from {address}")
envelope.mail_from = address envelope.mail_from = address
max_sent = self.config.max_user_send_per_minute max_sent = self.config.max_user_send_per_minute
if not self.send_rate_limiter.is_sending_allowed(address, max_sent): if not self.send_rate_limiter.is_sending_allowed(address, max_sent):
@@ -242,11 +241,15 @@ class OutgoingBeforeQueueHandler:
return "250 OK" return "250 OK"
async def handle_DATA(self, server, session, envelope): async def handle_DATA(self, server, session, envelope):
logging.info("handle_DATA before-queue") loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, self.sync_handle_DATA, envelope)
def sync_handle_DATA(self, envelope):
log_info("handle_DATA before-queue")
error = self.check_DATA(envelope) error = self.check_DATA(envelope)
if error: if error:
return error return error
logging.info("re-injecting the mail that passed checks") log_info("re-injecting the mail that passed checks")
client = SMTPClient("localhost", self.config.postfix_reinject_port) client = SMTPClient("localhost", self.config.postfix_reinject_port)
client.sendmail( client.sendmail(
envelope.mail_from, envelope.rcpt_tos, envelope.original_content envelope.mail_from, envelope.rcpt_tos, envelope.original_content
@@ -255,7 +258,7 @@ class OutgoingBeforeQueueHandler:
def check_DATA(self, envelope): def check_DATA(self, envelope):
"""the central filtering function for e-mails.""" """the central filtering function for e-mails."""
logging.info(f"Processing DATA message from {envelope.mail_from}") log_info(f"Processing DATA message from {envelope.mail_from}")
message = BytesParser(policy=policy.default).parsebytes(envelope.content) message = BytesParser(policy=policy.default).parsebytes(envelope.content)
mail_encrypted = check_encrypted(message, outgoing=True) mail_encrypted = check_encrypted(message, outgoing=True)
@@ -295,11 +298,15 @@ class IncomingBeforeQueueHandler:
self.config = config self.config = config
async def handle_DATA(self, server, session, envelope): async def handle_DATA(self, server, session, envelope):
logging.info("handle_DATA before-queue") loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, self.sync_handle_DATA, envelope)
def sync_handle_DATA(self, envelope):
log_info("handle_DATA before-queue")
error = self.check_DATA(envelope) error = self.check_DATA(envelope)
if error: if error:
return error return error
logging.info("re-injecting the mail that passed checks") log_info("re-injecting the mail that passed checks")
# the smtp daemon on reinject_port_incoming gives it to dkim milter # the smtp daemon on reinject_port_incoming gives it to dkim milter
# which looks at source address to determine whether to verify or sign # which looks at source address to determine whether to verify or sign
@@ -315,7 +322,7 @@ class IncomingBeforeQueueHandler:
def check_DATA(self, envelope): def check_DATA(self, envelope):
"""the central filtering function for e-mails.""" """the central filtering function for e-mails."""
logging.info(f"Processing DATA message from {envelope.mail_from}") log_info(f"Processing DATA message from {envelope.mail_from}")
message = BytesParser(policy=policy.default).parsebytes(envelope.content) message = BytesParser(policy=policy.default).parsebytes(envelope.content)
mail_encrypted = check_encrypted(message, outgoing=False) mail_encrypted = check_encrypted(message, outgoing=False)
@@ -357,16 +364,19 @@ class SendRateLimiter:
return False return False
def log_info(msg):
print(msg, file=sys.stderr)
def main(): def main():
args = sys.argv[1:] args = sys.argv[1:]
assert len(args) == 2 assert len(args) == 2
config = read_config(args[0]) config = read_config(args[0])
mode = args[1] mode = args[1]
logging.basicConfig(level=logging.WARN)
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
assert mode in ["incoming", "outgoing"] assert mode in ["incoming", "outgoing"]
task = asyncmain_beforequeue(config, mode) task = asyncmain_beforequeue(config, mode)
loop.create_task(task) loop.create_task(task)
logging.info("entering serving loop") log_info("entering serving loop")
loop.run_forever() loop.run_forever()

View File

@@ -66,7 +66,7 @@ http {
index index.html index.htm; index index.html index.htm;
server_name _; server_name {{ config.domain_name }} www.{{ config.domain_name }} mta-sts.{{ config.domain_name }};
access_log syslog:server=unix:/dev/log,facility=local7; access_log syslog:server=unix:/dev/log,facility=local7;

View File

@@ -13,6 +13,7 @@ OversignHeaders From
On-BadSignature reject On-BadSignature reject
On-KeyNotFound reject On-KeyNotFound reject
On-NoSignature reject On-NoSignature reject
DNSTimeout 60
# Signing domain, selector, and key (required). For example, perform signing # Signing domain, selector, and key (required). For example, perform signing
# for domain "example.com" with selector "2020" (2020._domainkey.example.com), # for domain "example.com" with selector "2020" (2020._domainkey.example.com),

View File

@@ -223,8 +223,14 @@ def test_expunged(remote, chatmail_config):
def test_deployed_state(remote): def test_deployed_state(remote):
try:
git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode() git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode()
except Exception:
git_hash = "unknown\n"
try:
git_diff = subprocess.check_output(["git", "diff"]).decode() git_diff = subprocess.check_output(["git", "diff"]).decode()
except Exception:
git_diff = ""
git_status = [git_hash.strip()] git_status = [git_hash.strip()]
for line in git_diff.splitlines(): for line in git_diff.splitlines():
git_status.append(line.strip().lower()) git_status.append(line.strip().lower())