Skip to main content

A trivial message platform abstraction layer (Fastmail/IMAP/SMTP; more platforms WIP).

Project description

TrivialMessage

A small, composable library that normalizes “messages” from different platforms (email today; others later) behind a single interface.

The goal is to make it easy to:

  • fetch unread messages (polling),
  • listen for new messages (async generators),
  • send/reply/forward when supported,
  • apply consistent filtering (sender/recipient/subject/time windows),
  • keep datetimes normalized to UTC.

Note: Several platforms (Gmail/Outlook/Slack/WhatsApp) are intentionally WIP and may be stubbed or absent.


Installation

Base install (interfaces + core models/utilities):

pip install trivialmessage

Email support (Fastmail + IMAP/SMTP helpers):

pip install "trivialmessage[email]"

If you keep Protonmail Bridge support in this library (optional):

pip install "trivialmessage[protonmail]"

Concepts

Message

All platforms yield a Message object (or a dict with the same shape, depending on your integration layer). The message is intended to be JSON-friendly and stable across sources.

Typical fields:

  • message_id (stable external id if available)
  • from, to, cc, bcc
  • subject
  • text, html
  • date (send-time, if known) normalized to UTC
  • internaldate (receive-time, if known) normalized to UTC
  • raw_headers (dict of headers when available)
  • platform-specific metadata (e.g. uid for IMAP-like sources)

Filters

Platforms accept a MessageFilter (or a list of them) to limit what you fetch/listen to.

Common filter fields:

  • sender/recipient matching should be case-insensitive (addresses are normalized)
  • datetime comparisons should be against UTC-aware datetimes

Example filter patterns you’ll commonly want:

  • “from is in allowlist”
  • “to contains support@…”
  • “subject contains keyword”
  • “received since ”
  • “only unread”

Platform API

All platforms implement the same general interface (some methods may raise NotImplementedError / PlatformNotSupported if the platform can’t do that operation).

Core methods

  • get_unread(filters=...) -> list[Message] Fetch unread messages (polling style).

  • listen(filters=..., **opts) -> AsyncIterator[Message] Yield messages as they arrive. Must not block other platforms if you compose multiple listeners.

  • send(to, subject, text=None, html=None, cc=None, bcc=None, **opts) -> Message Send a new outbound message.

  • reply(message, text=None, html=None, **opts) -> Message Reply to an existing inbound message.

  • forward(message, to, text=None, html=None, **opts) -> Message Forward an existing message to a new recipient.

Capability notes

Not every platform can both send and receive.

  • Some platforms are receive-only (e.g., IMAP if you only configure IMAP and no SMTP).
  • Some platforms are send-only (e.g., SMTP-only configuration).
  • If a method is unsupported, it should fail fast with a clear exception.

Platforms

Fastmail

Fastmail is treated as a first-class platform (HTTP API).

Credentials

You’ll need an API token from Fastmail.

High-level steps:

  1. Log in to Fastmail.
  2. Create an API token in Fastmail settings (app/password/token section).
  3. Provide it to the platform via env var or explicit constructor argument.

Common env var:

  • FASTMAIL_API_TOKEN (your token)

Usage

import asyncio

from trivialmessage.platform.fastmail import FastmailPlatform
from trivialmessage.types import MessageFilter  # name may vary in your codebase

fm = FastmailPlatform.from_env()

# Poll unread
msgs = fm.get_unread(filters=[
    MessageFilter(from_email="alerts@example.com"),
])

# Listen for new mail
async def main():
    async for msg in fm.listen(filters=[MessageFilter(any_recipient="me@mydomain.com")]):
        print(msg.subject)

asyncio.run(main())

Platform-specific concerns

  • Fastmail provides stable ids; message_id should be populated.
  • Datetimes are normalized to UTC.
  • If you apply sender/recipient filters, they should be case-insensitive (normalize first).

IMAP

IMAP support is typically used for receiving messages.

Credentials

You’ll need standard IMAP credentials:

  • host (e.g., imap.fastmail.com)
  • port (usually 993 for SSL)
  • username
  • password (or app password)
  • folder (optional; often INBOX)

If you’re using env vars, you’ll typically have something like:

  • IMAP_HOST
  • IMAP_PORT
  • IMAP_USER
  • IMAP_PASSWORD
  • IMAP_SECURITY (SSL / STARTTLS / PLAINTEXT)
  • IMAP_FOLDER

Usage

import asyncio

from trivialmessage.platform.imap import IMAPPlatform

imap = IMAPPlatform.from_env()

# Poll unread
unread = imap.get_unread()

# Listen (async generator)
async def main():
    async for msg in imap.listen():
        print(msg.from_email, msg.subject)

asyncio.run(main())

Platform-specific concerns

  • IMAP is generally receive-only.

    • send, reply, forward should fail (unsupported) unless you separately configure SMTP and use a different platform for sending.
  • Depending on server behavior, “fetching” can set flags unless you use BODY.PEEK semantics; implementations should avoid accidentally marking mail seen.

  • IMAP UID exists but may not be stable across folders/servers; prefer message_id when possible.


SMTP

SMTP is typically used for sending messages.

Credentials

You’ll need standard SMTP credentials:

  • host (e.g., smtp.fastmail.com)
  • port (commonly 465 for SSL or 587 for STARTTLS)
  • username
  • password (or app password)
  • from address (optional override)

Common env vars:

  • SMTP_HOST
  • SMTP_PORT
  • SMTP_USER
  • SMTP_PASSWORD
  • SMTP_SECURITY (SSL / STARTTLS / PLAINTEXT)
  • SMTP_EMAIL_FROM (optional)

Usage

from trivialmessage.platform.smtp import SMTPPlatform

smtp = SMTPPlatform.from_env()

sent = smtp.send(
    to="someone@example.com",
    subject="Hello",
    text="This is a test.",
)
print(sent.message_id)

Platform-specific concerns

  • SMTP is generally send-only.

    • get_unread / listen should fail (unsupported).
  • If you need bidirectional email, configure both an IMAP platform (receive) and SMTP platform (send), or use a single provider platform that supports both (e.g., Fastmail API).


Protonmail (Bridge) (optional / legacy-compatible)

This platform exists only if you keep it around and install the extra.

Credentials

Protonmail typically requires running Protonmail Bridge locally/in-container.

This is complex and usually deployed as a dedicated variant. If you’re using it:

  • ensure the Bridge is running and exposes IMAP + SMTP locally
  • set the bridge IMAP/SMTP credentials (often generated by Bridge)

Typical env var contract (example):

  • BRIDGE_IMAP_USER
  • BRIDGE_IMAP_PASSWORD
  • BRIDGE_IMAP_HOST (default 127.0.0.1)
  • BRIDGE_IMAP_PORT (default 1143)
  • BRIDGE_IMAP_SECURITY (STARTTLS/SSL/PLAINTEXT)
  • and similar SMTP vars if sending

Usage

import asyncio
from trivialmessage.platform.protonmail import ProtonmailPlatform

pm = ProtonmailPlatform.from_env()

# Receive
msgs = pm.get_unread()

# Send (via Bridge SMTP)
pm.send(to="someone@example.com", subject="Hi", text="...")

async def main():
    async for msg in pm.listen():
        print(msg.subject)

asyncio.run(main())

Platform-specific concerns

  • Running Bridge is operationally heavy; treat it as a separate deployment.
  • Bridge IMAP/SMTP are local-only; don’t expose those ports publicly.
  • Message ids and header behavior depend on Bridge; your implementation should normalize to the same Message shape.

Composing listeners

A common pattern is to listen to multiple platforms concurrently and yield messages as soon as they arrive from any source.

Desired usage:

compose(
  fastmail.listen(filters=fs),
  protonmail.listen(filters=fs),
  whatsapp.listen(filters=fs),
)

The intended behavior:

  • start all listeners concurrently,
  • yield messages from whichever source produces next,
  • do not block other sources,
  • (error handling strategy is up to the caller / composition helper).

This repo expects a helper for that pattern (either provided here or in your application layer).


Development

  • Python: 3.10+ recommended
  • Style: keep message payloads JSON-friendly
  • Datetimes: always normalize to UTC-aware values
  • Filters: normalize addresses for case-insensitive comparisons

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

trivialmessage-0.0.7.tar.gz (37.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

trivialmessage-0.0.7-py3-none-any.whl (40.7 kB view details)

Uploaded Python 3

File details

Details for the file trivialmessage-0.0.7.tar.gz.

File metadata

  • Download URL: trivialmessage-0.0.7.tar.gz
  • Upload date:
  • Size: 37.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for trivialmessage-0.0.7.tar.gz
Algorithm Hash digest
SHA256 81611a4ba8e136b4889d2616743eb7dfd24d2a1ce46a6bf0d0591ddddcdc0dbc
MD5 b9deb988d19229bc16ef1292b44ee5ec
BLAKE2b-256 8ecaf060cd0365dd4ba40eaf85ebcb1b59acce42224a0469b1ce8f5aa17d18ab

See more details on using hashes here.

File details

Details for the file trivialmessage-0.0.7-py3-none-any.whl.

File metadata

  • Download URL: trivialmessage-0.0.7-py3-none-any.whl
  • Upload date:
  • Size: 40.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for trivialmessage-0.0.7-py3-none-any.whl
Algorithm Hash digest
SHA256 667a90ba57778bd7767467e676f4b90d53c4aced38e3d1f992b7871b2cf58e19
MD5 4e5051176689c77608fe8d6168d67bed
BLAKE2b-256 56aba0ec44c425ad86d1cb61d2679bf28a83a1a3bf3434f71c5e231cd5729955

See more details on using hashes here.

Supported by

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