firstmail-py
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
Context Manager (recommended)
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:
- HTML heading tags (
<h1>123456</h1>) - Subject line digits (4–8 digits)
- 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| firstmail_py-0.1.0.tar.gz | 18.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.