Skip to main content

firstmail-py

PyPI Python License: MIT

Async email client for firstmail.ltd — powered by aioimaplib & aiosmtplib.

Full async/await rewrite of nichind/firstmail with IMAP IDLE support, OTP extraction, typed exceptions, and a clean Pythonic API.

Installation

pip install firstmail-py

Quick Start

import asyncio
from firstmail import FirstMail

async def main():
    async with FirstMail("user@firstmail.ltd", "password") as client:
        # Latest email
        last = await client.get_last_mail()
        if last:
            print(last.subject, last.sender)

        # Inbox count
        print(await client.get_message_count())

        # Fetch newest 10
        emails = await client.get_all_mail(limit=10)

        # Extract OTP code from latest email
        otp = await client.get_otp_code()
        print(f"OTP: {otp}")

asyncio.run(main())

Usage

async with FirstMail("user@firstmail.ltd", "password") as client:
    count = await client.get_message_count()

Manual Resource Management

client = FirstMail("user@firstmail.ltd", "password")
try:
    count = await client.get_message_count()
finally:
    await client.close()

Credential String

client = FirstMail("user@firstmail.ltd:password")

Send Email

async with FirstMail("user@firstmail.ltd", "password") as client:
    await client.send_mail(
        to="recipient@example.com",
        subject="Hello",
        body="World!",
        cc="cc@example.com",
        bcc="bcc@example.com",
    )

Watch for New Emails

Uses IMAP IDLE for real-time push (falls back to polling automatically):

async with FirstMail("user@firstmail.ltd", "password") as client:
    async for email in client.watch_for_new_emails(check_interval=30):
        print(f"New: {email.subject} from {email.sender}")

Wait for Specific Emails

async with FirstMail("user@firstmail.ltd", "password") as client:
    # Wait for any new email (max 60s)
    msg = await client.wait_for_new_mail(timeout=60)

    # Wait for email from a specific sender (substring, case-insensitive)
    msg = await client.wait_for_sender("openai.com", timeout=60)

    # Wait for email with specific subject text
    msg = await client.wait_for_email("verification code", timeout=60)

OTP Code Extraction

async with FirstMail("user@firstmail.ltd", "password") as client:
    # Immediate — parse OTP from current inbox
    code = await client.get_otp_code()                        # "719680"
    code = await client.get_otp_code(sender="openai.com")     # filter by sender

    # Wait mode — wait for new OTP email, then extract
    code = await client.get_otp_code(sender="openai.com", timeout=60)

OTP extraction priority:

  1. HTML heading tags (<h1>123456</h1>)
  2. Subject line digits (4–8 digits)
  3. Plain-text body digits

EmailMessage

Every email is returned as an EmailMessage dataclass:

msg.subject      # str
msg.sender       # str
msg.recipient    # str
msg.body         # str  (plain text)
msg.html_body    # str | None  (HTML body if present)
msg.date         # str
msg.message_id   # str
msg.raw_message  # bytes | None

msg.to_dict()    # serialize to dict (excludes raw_message)

Exception Handling

All exceptions inherit from FirstMailError:

from firstmail import (
    FirstMailError,            # base — catch-all
    FirstMailConnectionError,  # server unreachable
    FirstMailAuthError,        # wrong credentials
    FirstMailSendError,        # send failed
    FirstMailFetchError,       # fetch failed
    FirstMailTimeoutError,     # operation timed out
    FirstMailParseError,       # email parsing failed
)

try:
    async with FirstMail("user@firstmail.ltd", "wrong") as client:
        await client.get_last_mail()
except FirstMailAuthError:
    print("Bad credentials")
except FirstMailTimeoutError:
    print("Timed out")
except FirstMailError as e:
    print(f"Error: {e}")

CLI

firstmail -e <email> -p <password> read-last
firstmail -e <email> -p <password> read-last --json
firstmail -e <email> -p <password> read-all --limit 10 --full
firstmail -e <email> -p <password> send --to user@example.com --subject "Hi" --body "Hello!"
firstmail -e <email> -p <password> watch --interval 60 --show-body
firstmail -e <email> -p <password> count

# Credential string shortcut
firstmail "user@firstmail.ltd:password" read-last

# Module invocation
python -m firstmail <command>

API Reference

Constructor

FirstMail(
    address: str,                          # email or "email:password"
    password: str | None = None,
    *,
    use_ssl: bool = True,                  # SSL/TLS (port 993/465)
    imap_host: str = "imap.firstmail.ltd",
    smtp_host: str = "imap.firstmail.ltd",
    imap_port: int | None = None,          # auto: 993 (SSL) / 143
    smtp_port: int | None = None,          # auto: 465 (SSL) / 587
    timeout: float = 30.0,                 # connection timeout (seconds)
)

Methods

Method Returns Description
await get_last_mail() EmailMessage | None Most recent email
await get_all_mail(limit=None) list[EmailMessage] All emails, newest first
await send_mail(to, subject, body, *, cc, bcc) bool Send plain-text email
await get_message_count() int Inbox message count
async for msg in watch_for_new_emails(check_interval, *, use_idle) EmailMessage Yield new emails as they arrive
await wait_for_new_mail(*, timeout, check_interval) EmailMessage Block until a new email arrives
await wait_for_sender(sender, *, timeout, check_interval) EmailMessage Block until email from sender arrives
await wait_for_email(title_contains, *, timeout, check_interval) EmailMessage Block until email with matching subject arrives
await get_otp_code(*, sender, timeout, check_interval) str | None Extract OTP code (4–8 digits) from latest or next email
await close() None Close all connections

Exceptions

Exception Parent Raised when
FirstMailError Exception Base for all errors
FirstMailConnectionError FirstMailError Cannot connect to IMAP/SMTP server
FirstMailAuthError FirstMailError Login credentials rejected
FirstMailSendError FirstMailError Email send operation failed
FirstMailFetchError FirstMailError Email fetch operation failed
FirstMailTimeoutError FirstMailError Operation exceeded timeout
FirstMailParseError FirstMailError Raw email bytes could not be parsed

Server Info

Protocol Host SSL Port Non-SSL Port
IMAP imap.firstmail.ltd 993 143
POP3 imap.firstmail.ltd 995 110
SMTP imap.firstmail.ltd 465 587

License

MIT

Metadata

Release files for firstmail-py 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 firstmail-py 0.1.0
File Size Uploaded Provenance
firstmail_py-0.1.0.tar.gz 18.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for firstmail-py 0.1.0
File Interpreter ABI Platform
firstmail_py-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 32.7 kB

Release files / firstmail_py-0.1.0.tar.gz

Download URL firstmail_py-0.1.0.tar.gz
Size 18.6 kB
Tags Source
SHA-256 checksum
How to use checksums
5150f4f7bee071407ac556b0c92c673d681cc13f81a1659bd37abd348128424d
BLAKE2b-256 checksum
How to use checksums
8be3bbf194fa19dc453ce2947a18a26d53b7a618fd05f121fa5bab134cbd0aac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

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

Download URL firstmail_py-0.1.0-py3-none-any.whl
Size 14.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fd6848bfd8ece3d54c53c76b406c7dedf6b8fe322b5857b69299d1d4c0b02151
BLAKE2b-256 checksum
How to use checksums
ee90c04222b2871dc54a55f145ea249b2970baeaa4957312d9a7eddc12f74b88
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Security for firstmail-py 0.1.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

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