Async email client for firstmail.ltd — powered by aioimaplib & aiosmtplib.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file firstmail_py-0.1.0.tar.gz.
File metadata
- Download URL: firstmail_py-0.1.0.tar.gz
- Upload date:
- Size: 18.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5150f4f7bee071407ac556b0c92c673d681cc13f81a1659bd37abd348128424d
|
|
| MD5 |
5a16f9999c16421efa68482fcd5c0d27
|
|
| BLAKE2b-256 |
8be3bbf194fa19dc453ce2947a18a26d53b7a618fd05f121fa5bab134cbd0aac
|
File details
Details for the file firstmail_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: firstmail_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 14.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd6848bfd8ece3d54c53c76b406c7dedf6b8fe322b5857b69299d1d4c0b02151
|
|
| MD5 |
59a97c6765a8d477ad67df285b5ced6f
|
|
| BLAKE2b-256 |
ee90c04222b2871dc54a55f145ea249b2970baeaa4957312d9a7eddc12f74b88
|