Skip to main content

A Milter library and app framework in pure-Python with asyncio

Project description

A modern pure-Python Milter framework

Python 3.10+ Checked with mypy Code style: black Ruff Imports: isort Hadolint ShellCheck License: Apache 2.0 REUSE compliant

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.

Getting started 🚀

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)

Configuration with Postfix

  1. Start your Milter application or run one of the examples directly — see examples/.
  2. Start a Postfix instance with a configuration like smtpd_milters = inet:127.0.0.1:9000 (replace IP address and port number accordingly).

Run an example Milter app with Postfix in Docker

Described here 👉 examples/README.md.

Example use 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 used OpenDKIM Milter and want to implement your own DKIM signer/verifier.

What about PyMilter?

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)?[^sendmail-relnotes-kr-c-deprecation] 🙈

So, yeah, that's the short version of why I started this project. 🤓

Documentation 📖

Limitations

  • 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).

Feedback 💬

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 github@gertvandijk.nl. 📬

When not to use a Milter

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 returning PREPEND 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.

Alternatives to Purepythonmilter

Python alternatives appear to be unmaintained and no longer actively supported for years.

Alternatives in other programming languages without a dependency on Sendmail's libmilter are:

Other relevant projects (not really reusable libraries): phalaaxx/ratemilter, phalaaxx/pf-milters, mschneider82/milterclient, andybalholm/grayland, Freeaqingme/ClueGetter.

License

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.

[^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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

purepythonmilter-0.0.1.tar.gz (82.9 kB view details)

Uploaded Source

Built Distribution

purepythonmilter-0.0.1-py3-none-any.whl (49.2 kB view details)

Uploaded Python 3

File details

Details for the file purepythonmilter-0.0.1.tar.gz.

File metadata

  • Download URL: purepythonmilter-0.0.1.tar.gz
  • Upload date:
  • Size: 82.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.10.9

File hashes

Hashes for purepythonmilter-0.0.1.tar.gz
Algorithm Hash digest
SHA256 1e08698ed556c3044f89ab09bbbd67e30a5402342e55ef8232e4396f5bac1790
MD5 b248f9c8ab86f364209233ef8b608821
BLAKE2b-256 ad9ed5b481a8466ad7359197d44016063de5a00153822d7c0d523f6516cb1dee

See more details on using hashes here.

File details

Details for the file purepythonmilter-0.0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for purepythonmilter-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 57272f9589f6de95ba4cf643ba86f17a309d19a3a720d30fb95b128915d6517e
MD5 94571ecff1f17106e2b1b1326b42a9ae
BLAKE2b-256 a259c5b8d01a0e1350be27c57e7793d07ab4f9c61235f471da48c09a91d5f524

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page