Skip to main content

flask-inbound-email

CI PyPI License

Give every user of your Flask app their own email address. What they send or forward there reaches your app already read: their note, the forwarded message, PDFs and photos, calendar invites.

alice-x7k2m9p4q1@in.yourapp.com

Users forward an invoice, a booking or a meeting invitation; your app gets:

email.forward.note             # "Pay by Friday"   (what the user wrote on top)
email.forward.original_from    # "Bob Builder <bob@builder.example>"
email.forward.original_subject # "Invoice 42"      (no "Fwd:", "R:", "I:")
email.forward.body             # the forwarded message, without the forward headers
email.files                    # PDFs as they are, photos as JPEGs the right way up
email.skipped                  # what could not be used, and why ("report.docx": unsupported)
email.invite                   # summary, day and time in the user's time zone, video link

It was taken out of a production app, where it reads the emails forwarded to a task manager.

What it takes care of

  • Forwarded emails from Gmail, Apple Mail and Outlook (desktop, web, and the bare "From/Sent/Subject" header block), in English and Italian, also when quoted with >.
  • Attachments: signature logos and icons are ignored, phone photos are rotated and scaled down, broken or oversized files are reported with a reason you can tell the sender.
  • Calendar invites (.ics): new, moved (SEQUENCE) and cancelled events, all-day events, Google Meet / Zoom / Teams links.
  • Trust: only the user's own addresses may write to them; SPF, DKIM, DMARC, spam and virus verdicts are checked; auto-replies, mailing lists and bounces are dropped.
  • Never answering who is rejected: no mail loops, and no way to find out whether an address exists.
  • Delivery on Amazon SES with no public webhook: SES stores the email in S3 and notifies a queue that your app reads. Works the same on a laptop and in production.

Install

pip install flask-inbound-email          # the Flask extension and the email reader
pip install 'flask-inbound-email[aws]'   # plus boto3, to receive through Amazon SES

Python 3.10+, Flask 2.3+.

Use it

from flask_inbound_email import InboundEmail, Result, TemporaryFailure, new_token

app.config["INBOUND_DOMAIN"] = "in.yourapp.com"
inbound = InboundEmail(app)

@inbound.user_loader
def load_user(token):
    # The part before @. Return None for unknown or suspended users.
    return User.query.filter_by(inbound_token=token, active=True).first()

@inbound.trusted_senders
def trusted_senders(user):
    # Only these addresses may write to the user's address. Verify extra ones with a link first.
    return [user.email, *user.verified_emails]

@inbound.handler
def handle(user, email):
    if Note.query.filter_by(message_id=email.message_id).first():
        return Result("ignored", "already received")  # the same email can arrive twice
    try:
        db.session.add(Note(user=user, text=email.forward.note, message_id=email.message_id))
        db.session.commit()
    except OperationalError as e:
        db.session.rollback()
        raise TemporaryFailure() from e  # the email stays in the queue and comes back later
    return Result("handled")

Give each user an address once, and show it to them:

user.inbound_token = new_token(user.email)   # "alice-x7k2m9p4q1"; a new one turns the old address off
inbound.address_for(user.inbound_token)      # "alice-x7k2m9p4q1@in.yourapp.com"

Then receive what is waiting, every minute or two (cron, a systemd timer):

flask inbound poll

To try emails on your machine, without AWS:

flask inbound read forwarded.eml --to alice-x7k2m9p4q1@in.yourapp.com

A complete app in one file is in examples/app.py.

Optional callbacks

Callback Called with Use it to
@inbound.notifier user, email, result answer the sender ("Done: 1 task added"). Never called for rejected emails; its errors are only logged. Send it with the header Auto-Submitted: auto-replied.
@inbound.recorder user or None, sender, subject, result keep a log of every email, rejected ones included. Keep it short (e.g. 30 days).
@inbound.user_context user → context manager run the handler and the notifier in the user's language, e.g. Flask-Babel's force_locale.
@inbound.user_timezone user → zone name or tzinfo read invites in the user's time zone (default: INBOUND_TIMEZONE).

The extension owns no database table and does not commit anything: your callbacks do.

Without Flask

The reading part works on its own:

from zoneinfo import ZoneInfo
from flask_inbound_email import Limits, Verdicts, read_email, rejection

email = read_email(raw_bytes, tz=ZoneInfo("Europe/Rome"), limits=Limits(max_files=3))
reason = rejection(email.message, email.sender, Verdicts(spf="PASS", dkim="PASS"), own_domains=["in.yourapp.com"])

Configuration

Setting Default
INBOUND_DOMAIN — The domain of the users' addresses, e.g. in.yourapp.com.
INBOUND_LEGACY_DOMAINS [] Older domains still accepted after a move.
INBOUND_TIMEZONE UTC Time zone for invites when user_timezone gives none.
INBOUND_QUEUE_URL — The SQS queue (written by the setup command).
INBOUND_AWS_REGION eu-west-1 The SES region that receives the emails.
INBOUND_AWS_ACCESS_KEY_ID, INBOUND_AWS_SECRET_ACCESS_KEY — The app's read-only key. Without them boto3 looks for credentials as usual (environment, ~/.aws, instance role).

Attachment limits are set with InboundEmail(app, limits=Limits(...)): at most 5 files, PDFs up to 10 MB, pictures under 30 KB ignored, pictures scaled to 2000 px.

Amazon SES setup

flowchart LR
    S[Sender] -->|MX in.yourapp.com| SES[Amazon SES]
    SES -->|email| S3[(S3 bucket<br/>deleted after 2 days)]
    SES -->|notification| SNS[SNS topic] --> SQS[SQS queue]
    App[flask inbound poll] -->|reads| SQS
    App -->|reads, then deletes| S3

Run once, with admin credentials (not the app's):

AWS_PROFILE=admin inbound-email-aws-setup --app yourapp --domain in.yourapp.com --env-file .env

It creates the bucket, the topic, the queue, the SES receipt rule and an IAM user for the app that can only read the queue and read or delete the stored emails. It writes the INBOUND_* settings into .env (the secret key is never printed) and prints two DNS records to add: an MX record for the domain and a TXT record that proves it is yours. Running it again skips what already exists.

Good to know:

  • SES receives email only in some regions.
  • A region has a single active receipt rule set: if another app already has one, the rule is added to it.
  • Use a subdomain (in.yourapp.com) so the MX record does not touch your main mailboxes.

The setup command is tested against moto's simulated AWS.

Privacy

The email is deleted from S3 as soon as it is processed (and after 2 days in any case). What you keep is up to your handler: keep what the feature needs, not the whole email. Forwarded emails contain other people's data.

Contributing

Bug reports and pull requests are welcome: see CONTRIBUTING.md. To report a security issue, see SECURITY.md.

License

Apache-2.0. Made by Enjinia.

Metadata

Release files for flask-inbound-email 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flask-inbound-email 0.1.0
File Size Uploaded
flask_inbound_email-0.1.0.tar.gz 31.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flask-inbound-email 0.1.0
File Interpreter ABI Platform
flask_inbound_email-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 59.1 kB

Release files / flask_inbound_email-0.1.0.tar.gz

Download URL flask_inbound_email-0.1.0.tar.gz
Size 31.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d65dd38893f7b2904d7d65d0539f7acc0169ae0d2f5168a3985f834092f904d4
BLAKE2b-256 checksum
How to use checksums
ec837652d60eff4068674526c9ecb05ab2ab0885169c990d819c285760ef1ee5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / flask_inbound_email-0.1.0-py3-none-any.whl

Download URL flask_inbound_email-0.1.0-py3-none-any.whl
Size 27.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cbe3581d7a262f74fcdf6bc41bbf9f165fdbab4587eccb3e9be66b77ae1230a7
BLAKE2b-256 checksum
How to use checksums
e26f8857632e4f0ee358369d5a2923519a6370e01d3f5f5ff7aa891e5c92a572
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page