Mail servers (MTAs) like Postfix and Sendmail can 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 in your MTA's Postfix'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.
Purepythonmilter aims 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.
Install Purepythonmilter, e.g. using pip
:
$ pip install purepythonmilter
Self-descriptive example Milter app:
import purepythonmilter as ppm
async def on_mail_from(cmd: ppm.MailFrom) -> ppm.VerdictOrContinue:
if cmd.address.lower().endswith("@example.com"):
return ppm.RejectWithCode(primary_code=(5, 7, 1), text="not allowed here!")
return ppm.Continue()
mymilter = ppm.PurePythonMilter(name="mymilter", hook_on_mail_from=on_mail_from)
mymilter.run_server(host="127.0.0.1", port=9000)
- Start your Milter application or run one of the examples directly — see
examples/
. - Start a Postfix instance with a configuration like
smtpd_milters = inet:127.0.0.1:9000
(replace IP address and port number accordingly).
Described here 👉 examples/README.md
.
- 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 used OpenDKIM Milter and want to implement your own DKIM signer/verifier.
Purepythonmilter was written as an alternative to, and, out of frustration with it. PyMilter is not type annotated (mypy), has signal handling issues (for me), the dependency on a hand-crafted Python-C-extension linking 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)?1 🙈
So, yeah, that's the short version of why I started this project. 🤓
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 guidelines
- Any 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).
This project is very new and feedback is very much welcome! Please don't hesitate to file an issue, drop an idea or ask a question in the discussions.
Ideas & Feature Requests are in there too. 💡
Alternatively, just drop me a message at [email protected]
. 📬
If 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 probably not what 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 the socketmap protocol or policy delegation with the much simpler policy delegation protocol. Most of the email's and connection's metadata is available there too. For example, the postfix-mta-sts-resolver uses the former and the SPF policy daemon pypolicyd-spf uses the latter. Sometimes the use of a Milter may still be considered; for example, the SPF verification filter spf-milter is implemented using the Milter protocol.
For content inspection, there's Postfix's Content 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 are possible to implement using a Milter, but what could also be accomplished using alternative — likely simpler — ways:
- Inject custom headers to add information on which
smtpd
instance the email was received for routing/classifications later. This would typically be done using Postfix's policy delegation returningPREPEND headername: headertext
as 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/ClueGetter is such an application using the Milter protocol for a part of the functionality.
Most Python alternatives appear to be unmaintained and no longer actively supported for years.
- Kilter: A new project (2022) and shares some similarities features; it's written in pure async Python.
- python-libmilter: marked as 'no longer supporting', as of late 2022.
- PpyMilter: Python 2-only (last commit 2015).
- yatxmilter: Python 2-only (last commit 2015).
- python-milter-tools: Python 2-only (last commit 2011).
Alternatives in other programming languages without a dependency on Sendmail's libmilter are:
- indymilter: an asynchronous Milter library written in Rust.
- Sendmail::PMilter: a pure-Perl implementation (last release 2011).
- emersion/go-milter: a Milter library written in Go (in active development).
- phalaaxx/milter: another Milter library written in Go (last commit 2020).
- andybalholm/milter: a simple framework for writing milters written in Go (last commit 2016).
- nightcode/jmilter: a Milter library written in Java.
- sendmail-jilter: another Milter library written in Java (last release 2011).
- milterjs: a Milter library written in Javascript (last release 2018).
Other relevant projects (not really reusable libraries): phalaaxx/ratemilter, phalaaxx/pf-milters, mschneider82/milterclient, andybalholm/grayland, Freeaqingme/ClueGetter.
The major part of the project is Apache 2.0 licensed.
Files deemed insignificant in terms of copyright such as configuration files are licensed under the public domain "no rights reserved" CC0 license.
The repositoy is REUSE compliant.
Footnotes
-
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.