Package: purepythonmilter
A modern pure-Python Milter frameworkMail servers (MTAs) likePostfixandSendmailcan connect to an external filter process, called a
'Milter', for actions to take during an incoming SMTP transaction.
You may consider it like a plugin on the mail server software using callbacks over a TCP
or UNIX socket.A Milter can have any custom condition to reject/tempfail/discard a message, manipulate
headers and/or body and more.
This can be useful if you require custom validations or manupulative actions before mail
is accepted and that is unavailable inyour MTA'sPostfix's built-in features.
The use of a Milter would typically be the right choice when it comes to complex
decision making on accepting mail 'before queue' with conditions on headers or the
message body.Purepythonmilteraims to be a modern, Postfix-first, high-quality, strictly typed
framework and library.
And then all of that with an easy to use API and a high-performance asynchronous
embedded server.Getting started 🚀Install Purepythonmilter, e.g. usingpip:$pipinstallpurepythonmilterSelf-descriptive example Milter app:importpurepythonmilterasppmasyncdefon_mail_from(cmd:ppm.MailFrom)->ppm.VerdictOrContinue:ifcmd.address.lower().endswith("@example.com"):returnppm.RejectWithCode(primary_code=(5,7,1),text="not allowed here!")returnppm.Continue()mymilter=ppm.PurePythonMilter(name="mymilter",hook_on_mail_from=on_mail_from)mymilter.run_server(host="127.0.0.1",port=9000)Configuration with PostfixStart your Milter application or run one of the examples directly — seeexamples/.Start a Postfix instance with a configuration likesmtpd_milters = inet:127.0.0.1:9000(replace IP address and port number
accordingly).Run an example Milter app with Postfix in DockerDescribed here 👉examples/README.md.Exampleuse cases for a Milter app 💡From-header and envelope sender (Return-Path) alignment validation, for compliance
with DMARC (RFC7489 section 3.1) or reasons of preventing abuse
(impersonation).
Pevent sending such messages out by rejecting non-compliant messages on submission
time already and incude a descriptive error message to the user.Encrypt sensitive connection/account information and attach that in a custom header
value for outbound mail.
In case of abuse, the information can be decrypted by an operator from the raw mails
concerned and eliminates the need to store this data centrally for all mail.Cryptographically sign outgoing email or verify signatures of incoming email with some
custom scheme.In case you don't like the existing commonly usedOpenDKIM Milterand want to implement your own DKIM signer/verifier.What about PyMilter?Purepythonmilter was written as an alternative to, and, out of frustration with it.PyMilteris not type annotated (mypy), has signal handling issues (for me), the
dependency on ahand-crafted Python-C-extensionlinking to
Sendmail's libmilter and no offering of a binary package (wheel) to ease
installation. 😥By the way, did you know that Sendmail is — yes even in 2023 — written in K&R C
(predating ANSI-C)?[^sendmail-relnotes-kr-c-deprecation]🙈So, yeah, that's the short version of why I started this project. 🤓Documentation 📖docs/design.md— design and intents of this project. 🧠docs/api.md— API documentationdocs/milter-protocol.md— raw protocol notes. ✍️CONTRIBUTING.md— for development setup and contribution
guidelinesLimitationsAny functionality requiring intermediary responses (such as 'progress') is not yet
implemented in the API.Any functionality that requires carrying state over phases is not yet supported in the
API. (e.g. combining input from two different hooks)Mail headers are not 'folded'/'unfolded', but given or written as-is.UNIX domain sockets are not supported for the Milter server to listen on (TCP is).Feedback 💬This project is very new and feedback is very much welcome!
Please don't hesitate tofile an issue, drop an idea or ask a
question in thediscussions.Ideas & Feature Requestsare in there too. 💡Alternatively, just drop me a message atgithub@gertvandijk.nl. 📬Whennotto use a MilterIf you want to accomplish something that could be done using a custom dynamic
lookup in Postfix, such as message routing or policy lookups.
Postfix offers quite some built-in dynamic lookup types and a Milter is probablynotwhat you're looking for.
The Milter protocol is relatively complex and its use may not be required for your use
case.Be sure to also have a look at implementing your own custom dynamic lookup table in
Postfix using thesocketmap protocolor policy delegation
with the much simplerpolicy delegation protocol.
Most of the email's and connection'smetadatais available there too.
For example, thepostfix-mta-sts-resolveruses the former and the SPF policy daemonpypolicyd-spfuses the latter.
Sometimes the use of a Milter may still be considered; for example, the SPF verification
filterspf-milteris implemented using the Milter protocol.For content inspection, there's Postfix'sContent filter, but
beware that it's running 'after queue'.
It takes quite some orchestration to avoid bounces and correctly feed the mail back into
Postfix.Another aspect to consider is MTA support.
While the alternatives for Postfix listed above are still Postfix-specific, other more
generic lookup methods also exist.
For example, a dynamic DNS lookup could be much better adopted when migrating to another
MTA than any of the above.Example use cases which arepossibleto implement using a Milter, but what could also
be accomplished using alternative — likely simpler — ways:Inject custom headers to add information on whichsmtpdinstance the email was
received for routing/classifications later.
This would typically be done using Postfix's policy delegation returningPREPEND headername: headertextas action.Validate sender restrictions for a data backend type not supported by the Postfix,
such as interacting with an HTTP REST API / webhooks.
Again, policy delegation may be much simpler, but if conditions involve mail contents,
then you may need a Milter still.Custom centralized rate limiting and billing in an email hosting platform with several
account tiers.
And similarly for this one, policy delegation is probably much simpler.A read-only Milter that logs in a structured way and perhaps with certain conditions.
This would eliminate parsing Postfix's text log files, well, for incoming connections
at least.Freeaqingme/ClueGetteris such an application using the Milter protocol for a part
of the functionality.Alternatives to PurepythonmilterPython alternatives appear to be unmaintained and no longer actively supported for
years.python-libmilter: marked as'no longer supporting',
as of late 2022.PpyMilter: Python 2-only (last commit 2015).Alternatives in other programming languages without a dependency on Sendmail's libmilter
are:indymilter: an asynchronous Milter library written inRust.Sendmail::PMilter: a pure-Perlimplementation (last release
2011).emersion/go-milter: a Milter library written inGo(in active development).phalaaxx/milter: another Milter library written inGo(last commit 2020).andybalholm/milter: a simple framework for writing milters written inGo(last
commit 2016).nightcode/jmilter: a Milter library written inJava.sendmail-jilter: another Milter library written inJava(last release 2011).milterjs: a Milter library written inJavascript(last release 2018).Other relevant projects (not really reusable libraries):phalaaxx/ratemilter,phalaaxx/pf-milters,mschneider82/milterclient,andybalholm/grayland,Freeaqingme/ClueGetter.LicenseThe major part of the project isApache 2.0licensed.Files deemed insignificant in terms of copyright such as configuration files are
licensed under the public domain "no rights reserved"CC0license.The repositoy isREUSEcompliant.[^sendmail-relnotes-kr-c-deprecation]:Sendmail 8.71.1 Release notes:> 2021/08/17
>
> Deprecation notice: due to compatibility problems with some third party code, we
> plan to finally switch from K&R to ANSI C. If you are using sendmail on a system
> which does not have a compiler for ANSI C contact us with details as soon as
> possible so we can determine how to proceed.