Postfix milter: ضبط Reply-To بشكل موثوق دون الإضرار بتسليم الرسائل
لماذا نستخدم milter
يستطيع cleanup وheader_checks في Postfix إعادة كتابة الترويسات، لكن الـ milter يمنحك تحكماً دقيقاً وقابلاً للبرمجة أثناء جلسة SMTP، ويمكنه تعديل الترويسات أو إضافتها بأمان قبل تشغيل مراحل التوقيع والتسليم. ويتيح لك milter مكتوب بلغة Python فحص الترويسات الواردة كما هي تماماً، واحتساب عنوان Reply-To موحّد، وضمان سلوك متسق بين الرسائل المرسلة عبر SMTP والرسائل المحلية.
باستخدام كود Python التالي، سنقرأ ترويسة From الأصلية ونضيفها إلى Reply-To
#!/usr/bin/env python3
import sys, os, re, email
import logging
from email.parser import BytesParser
from email.policy import default
#from Milter import Milter
import Milter
BaseClass = getattr(Milter, 'Base', getattr(Milter, 'Milter', None))
assert BaseClass is not None, "pymilter not installed (no Base/Milter class)"
BRAND = 'brandmail'
FROM_SENDER = 'mail@brandmail.io' # must be approved by your relay
LOG_LEVEL = os.environ.get('IM_LOG_LEVEL', 'INFO').upper()
logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO), format='%(asctime)s %(levelname)s %(message)s')
addr_spec_re = re.compile(r'([^<,]+@[^>,\s]+)')
class BrandMilter(BaseClass):
def __init__(self):
super().__init__()
self.msg_headers = []
self.raw_from = None
def header(self, name, value):
# Record headers; capture first From we see
if name.lower() == 'from' and self.raw_from is None:
self.raw_from = value
# Keep all headers for later rewrite
self.msg_headers.append((name, value))
return Milter.CONTINUE
def _id(self):
try:
return self.idstr()
except Exception:
return "unknown-id"
def eom(self):
try:
# Build minimal header block for parsing
hdr_bytes = b''.join([f"{k}: {v}\r\n".encode('utf-8') for k, v in self.msg_headers])
raw = hdr_bytes + b"\r\n"
msg = BytesParser(policy=default).parsebytes(raw) or {}
# Extract first mailbox from original From
reply_to_addr = (self.raw_from or (msg.get('From') if hasattr(msg, 'get') else '') or '')
# Force approved From
final_from = f'"{BRAND}" <{FROM_SENDER}>'
# Ensure single Reply-To if available
if reply_to_addr:
if hasattr(msg, 'get') and msg.get('Reply-To'):
combined = f"{msg.get('Reply-To')}, {reply_to_addr}"
msg.replace_header('Reply-To', combined)
else:
msg['Reply-To'] = reply_to_addr
# Force approved From
if hasattr(msg, 'get') and msg.get('From'):
# Don't replace From because we will do it in the header_checks
# msg.replace_header('From', final_from)
pass
else:
msg['From'] = final_from
# Remove existing headers once per unique name
seen = set()
removed_count = 0
for k, _ in self.msg_headers:
kl = k.lower()
if kl in seen:
continue
seen.add(kl)
try:
self.chgheader(k, 0, None)
removed_count += 1
except Exception as ex:
logging.warning("%s failed to remove header %s: %r", self._id(), k, ex)
# Re-add normalized headers
added_count = 0
for k, v in (msg.items() if hasattr(msg, 'items') else []):
try:
self.addheader(k, str(v))
added_count += 1
except Exception as ex:
pass
return Milter.CONTINUE
except Exception as e:
# Never propagate exceptions to Postfix; log and continue
logging.error("%s eom exception: %r", self._id(), e)
# Utility to get body bytes accumulated by libmilter (may be empty)
def getbody(self):
# Milter.Base doesn’t buffer body by default; for header-only edits,
# returning empty body is fine; many MTAs provide headers before body.
return b''
def main():
sock_dir = "/var/spool/postfix/var/run/pymilter"
sock_path = f"{sock_dir}/brandmail.sock"
os.makedirs(sock_dir, mode=0o750, exist_ok=True)
try:
os.unlink(sock_path)
except FileNotFoundError:
pass
except OSError as e:
logging.error("Could not unlink socket %s: %s", SOCK_PATH, e)
raise
# Register class and run milter
Milter.factory = BrandMilter
logging.info("Starting milter on unix:%s", sock_path)
Milter.runmilter("brandmail-milter", f"unix:{sock_path}", 10)
if __name__ == "__main__":
main()
يمكنك حفظ الملف في /opt/pymilter/milter.py أو في أي مكان آخر تفضّله (مع منحه صلاحية التنفيذ عبر chmod +x)
سيضمن ذلك أن ينشئ Python مقبس socket في المسار /var/spool/postfix/var/run/pymilter (تأكد من وجود المجلد)، والآن أنشئ الخدمة بحسب نظام التشغيل لديك، والخدمة أدناه مخصّصة لـ Ubuntu 20
[Unit]
Description=Postfix Python Milter
After=network.target
[Service]
Type=simple
User=postfix
Group=postfix
ExecStart=/opt/pymilter/venv/bin/python /opt/pymilter/milter.py
RuntimeDirectory=pymilter
RuntimeDirectoryMode=0750
UMask=0027
Restart=on-failure
[Install]
WantedBy=multi-user.target
الآن وجّه إعداد smtpd_milters في Postfix إلى مسار المقبس:
milter_default_action = accept
smtpd_milters = unix:/var/run/pymilter/brandmail.sock
non_smtpd_milters = unix:/var/run/pymilter/brandmail.sock
لم نضف "/var/spool/postfix" إلى المسار لأن Postfix يعمل داخل بيئة chroot هناك بحسب نظام التشغيل، فإذا ظهر تحذير من Postfix بشأن مسار المقبس، فأضف "/var/spool/postfix" في بدايته
أعد تحميل الخدمة وPostfix وستكون جاهزاً تماماً! بالتوفيق!