@hasna/mailery
v0.6.116
Published
Mailery email management CLI + MCP server + dashboard for Resend, AWS SES, and Cloudflare inbound workflows
Readme
@hasna/mailery
Mailery is an email management CLI + MCP server - send, receive, sync, and manage email via Resend, AWS SES, and Cloudflare-routed inbound mail.
Install
Mailery is built for the Bun runtime. Install Bun 1.3 or newer before installing the CLI with npm.
npm install -g @hasna/maileryOpen Core And Cloud
Users install the open-source package: @hasna/mailery.
Mailery stays local-first by default: local SQLite, local provider credentials,
and local MCP. Self-hosted mode uses user-owned PostgreSQL/S3/SES as source of
truth with local SQLite as a runtime cache. Mailery Cloud is an opt-in
hosted source of truth at https://mailery.co; the same public CLI can sign up,
create an agent API key, create a billing link, create hosted mailboxes, read
hosted messages, generate hosted digests, and pull cloud mail into local SQLite.
The per-domain readiness contract for local, self-hosted, and cloud operation
lives in docs/DOMAIN_READINESS.md.
The SaaS control plane is private Hasna Tools infrastructure. End users and open-source contributors should not install or depend on private Hasna Tools platform packages.
@hasna/emails is compatibility-only during the rename period. New installs and
docs should use @hasna/mailery; the compatibility name is expected to be
retired after migration.
Quick Start
# Add a provider (SES or Resend)
mailery provider add --name production-ses --type ses --region us-east-1 --access-key ... --secret-key ...
mailery provider add --name production-resend --type resend --api-key ...
# Set up a domain (buy + DNS + SES in one command)
mailery domain setup example.com --provider <id> --email [email protected] ...
# Or connect a domain you already own without buying or calling Mailery Cloud
mailery domains connect example.com --provider <id> --source-of-truth local --dry-run
mailery domains connect example.com --provider <id> --source-of-truth postgres --dns-provider route53 --no-register-provider
# Or configure DNS for an existing domain via Cloudflare
mailery domain setup-cloudflare example.com --provider <id>
# Check public DNS before changing inbound routing
mailery domain check example.com
# SES send-only setup preserves existing MX, such as Google Workspace
mailery provision domain example.com --provider <ses-id> --dry-run
# Send an email
mailery send --from [email protected] --to [email protected] --subject "Hi" --body "Hello"
# Pull inbound mail from SES/S3 or Cloudflare-routed storage
mailery inbox source add-s3 --bucket <bucket> --prefix inbound/example.com/ --provider <provider-id>
mailery inbox sync-s3 --bucket <bucket> --prefix inbound/example.com/
# Inspect mailbox folders and ingestion sources
mailery inbox mailboxes
mailery inbox sources
mailery inbox list --folder unread --source provider:<id>
# Check sent email log
mailery email list
# Use self-hosted PostgreSQL/S3/SES as source of truth
mailery self-hosted status
mailery self-hosted migrate-localMailery Cloud
Cloud commands are non-interactive enough for agents and CI. Use --no-open
when creating billing links from a headless environment.
# Show the hosted service status
mailery cloud --api-url https://mailery.co status
# Create or log into a hosted account, generate an agent API key, and create a
# hosted billing link without opening a browser
mailery cloud setup \
--api-url https://mailery.co \
--email [email protected] \
--password "$MAILERY_PASSWORD" \
--api-key-name "Agent CLI" \
--scope mail_read mail_write billing_read \
--billing \
--no-open
# Hosted mailbox and message workflow
mailery cloud mailbox add [email protected] --provider manual
mailery cloud messages list --limit 20
mailery cloud messages pull --limit 20
mailery inbox list --limit 20
# Billing and domains
mailery cloud billing overview
mailery cloud billing subscribe --plan starter --no-open
mailery cloud domain available example-agent-mail.com
mailery cloud domain setup example-agent-mail.com --address agent --catch-all --mx-migration-consentThe starter SaaS plan is currently $10/month and grants hosted credits. Domain
setup can return DNS records in safe planning mode before any domain purchase or
MX migration is performed.
Domain Modes
Mailery is a multi-domain aggregator. Every domain is tracked independently, so DNS, inbound, outbound, and safety state belong to the domain, not to the app as a whole.
Use these setup paths:
| Mode | Who owns the mail source of truth | Domain setup path |
| --- | --- | --- |
| local | The local SQLite/files install | mailery domains add or mailery domains connect --source-of-truth local; DNS checks are advisory unless using a real send/receive provider. |
| self_hosted | Your PostgreSQL/S3/SES or equivalent infrastructure | mailery domains connect --source-of-truth postgres, then publish the returned DNS tasks and enable inbound/outbound when evidence is ready. |
| cloud | Mailery Cloud at https://mailery.co | mailery cloud domain setup; SaaS billing and tenant checks are handled by the hosted control plane. |
Authentication records are required only for the capability you enable:
- Inbound aggregation needs an inbound route, usually MX plus SES/S3 or another configured source.
- Outbound sending needs ownership verification plus DKIM and SPF/custom MAIL FROM alignment for the selected provider.
- DMARC is per sending domain. It does not block local viewing or inbound
aggregation, but it should be present before production sending and monitored
before moving from
p=noneto stricter policies.
For self-hosted migration, run mailery self-hosted migrate-local once, switch
to MAILERY_MODE=self_hosted with HASNA_EMAILS_STORAGE_MODE=remote, and treat
PostgreSQL/S3 as the durable source of truth. Local SQLite is then only a runtime
cache prepared from and flushed back to the self-hosted source.
Mailery UI (mailery ui)
A full-screen OpenTUI mail client with a responsive dashboard shell. Wide
terminals use a two-column admin layout with persistent navigation, mailbox
metrics, operations health, folders, actions, and a focused workspace. Inbox on
wide terminals uses a split message list + preview reader. Narrow terminals collapse to
a compact single-column view with the same Inbox, Compose, Domains, and
Settings dialog. Inbox starts at all addresses and can be filtered to one email
address when needed. Mailbox source status is exposed through CLI/API/MCP
surfaces without treating provider credentials as inboxes. Live read-state,
local refresh, background auto-pull, and an auto/light/dark color theme
keep the mailbox current and readable across terminals.
mailery ui
mailery ui --mailbox unreadThe app uses visible buttons and the Shortcuts command palette for actions. Mailbox filtering is handled by the mailbox dialog, which lists all mailboxes and configured/observed recipient addresses. Sidebar labels filter mailbox content, and Gmail-style Categories show Primary, Social, Promotions, Updates, and Forums separately from custom labels. Reader shows attachments with size/type. Composer writes markdown rendered to HTML on send. Settings opens as a simple menu dialog for sync, defaults, and display controls. Folders: Inbox · Unread · Starred · Sent · Archived · Spam · Trash.
Command Structure
mailery ui # Mailbox UI - inbox, compose, domains, settings
mailery provider # provider credentials/capabilities (ses, resend, sandbox)
mailery domain # add/verify/buy/setup/dns/check domains
mailery address # manage sender addresses (add, suspend, activate, quota)
mailery status # redacted system status + next useful actions
mailery agent context # agent-oriented context snapshot and workflows
mailery daemon # background queue/realtime status and restart guidance
mailery logs tail # local daemon/sync/inbound/scheduler log tails
mailery owner # tenancy: register human/agent owners
mailery alias # per-domain aliases + catch-all routing
mailery forwarding # app-level forwarding for locally received/synced mail
mailery sendkey # scoped send keys (restrict an agent to its own addresses)
mailery send # send an email
mailery reply / forward # reply (in-thread) or forward a sent/inbound email
mailery email # sent email: list, search, show, replies, conversation
mailery inbox # mailbox folders, sources, sync, read/star/archive/label, watch
mailery template # email templates
mailery contact # contacts (suppression list)
mailery group # recipient groups
mailery sequence # drip sequences
mailery schedule # scheduled emails: list, cancel, run
mailery triage # AI triage: classify, prioritize, draft replies
mailery storage # self-hosted PostgreSQL storage: status, migrate, migrate-local, push, pull
mailery self-hosted # source-of-truth runtime setup/status/migrate commands
mailery cloud # optional Mailery Cloud signup/login/billing/mailbox/message/digest/domain workflow
mailery aws # AWS setup: SES receipt rules, S3 inbound bucket
mailery config # configuration (key=value)
mailery stats # delivery statistics (--inbox for received mail)
mailery analytics # email analytics
mailery doctor # system diagnostics
mailery doctor delivery # diagnose missing inbound mail for one address
mailery serve # HTTP server + dashboard + authenticated /api/v1
mailery mcp # install MCP serverCompact Output and Gradual Disclosure
Mailery CLI commands are compact by default so agent terminals do not fill with large records. List and status commands show essential fields, bounded row counts, and hints for the next detail command. Use these flags when you need more:
mailery address list # compact table
mailery address list --verbose # expanded owner/admin/quota rows
mailery domain status --verbose # includes per-domain issue and fix lines
mailery provider list --limit 50 # explicit larger page
mailery contact list --suppressed # compact filtered contact list
mailery template show <name> # detail path for template bodies
mailery sequence show <name> # detail path for steps/enrollments
mailery forwarding list --source [email protected]
mailery agent context # compact agent context summary
mailery agent context --verbose # full redacted context snapshot
mailery agent context --json # full machine-readable context
mailery config list --verbose # full redacted config values
mailery config keys --verbose # include examples for every key
mailery email show <id> # detail path for one sent email
mailery inbox read <id> # detail path for one inbound email--json remains the machine-readable path. Broad MCP list tools default to
their existing bounded summary page size for compatibility; use each tool's
limit/offset inputs or the matching detail tool/resource for larger or full
records. emails://agent/context is sampled for orientation; use
emails://agent/context/full for the full redacted MCP resource.
Tenancy, aliases & scoped send keys
Every address can have an owner that is a human or an agent. A human-owned address must be administered by an agent (the agent operates it on the human's behalf); agent-owned addresses are self-administered.
# Register owners and assign an address (human-owned, agent-administered)
mailery owner register Morgan --type human --email [email protected]
mailery owner register Atlas --type agent
mailery provision address [email protected] --provider <ses-id> --owner Morgan --administrator Atlas
mailery address owner [email protected]
mailery address set-owner [email protected] --owner Morgan --administrator Atlas
mailery address transfer-owner [email protected] --owner Atlas --reason "handoff" --yes
mailery address unassign-owner [email protected] --reason "retired" --yes
mailery address owner-history [email protected]
# Scoped send keys — an agent can only send from addresses it owns/administers
mailery sendkey create Atlas --label ci # prints the esk_... token ONCE
mailery sendkey check Atlas [email protected] # authorized
mailery sendkey list / revoke <id>
# Per-domain aliases + catch-all
mailery alias add [email protected] [email protected]
mailery alias catch-all example.com [email protected] # *@example.com -> inbox@
mailery alias global [email protected] # protected global catch-all (ALL domains)
mailery alias resolve [email protected] # show where it routes
# App-level forwarding: forwards only mail already received or synced locally.
# Use provider-native forwarding when the mailbox provider owns root MX.
mailery forwarding explain [email protected]
mailery forwarding add [email protected] [email protected] --provider <provider-id>
mailery forwarding run --provider <provider-id> # future mail only
mailery forwarding run --provider <provider-id> --backfill # intentionally include older synced mail
# Address lifecycle
mailery address provision [email protected] --provider <ses-id> --owner Atlas
mailery address suggest --domain example.com
mailery address suspend <id> # block sending from this address
mailery address activate <id>
mailery address quota <id> 200 # max 200 sends/day (use 'none' to clear)DNS and inbound safety
mailery domain check <domain> detects common root MX owners, including Google
Workspace, Microsoft 365, Cloudflare Email Routing, Zoho, Proton, and AWS SES.
SES send-only provisioning does not require changing root MX and is the safest
path when an existing mailbox provider already receives mail.
Publishing SES inbound MX is only for domains that should receive through
SES/S3. Commands that can add SES inbound MX refuse to proceed when public MX
already belongs to another provider. --force-mx-switch is available for
intentional migrations after confirming mailbox ownership can move.
MCP Server
100+ tools for AI agents — send/read mail, provisioning, tenancy, aliases, scoped send keys, inbound read-state, real-time sync, agent context, source-aware mailbox status, ownership lookup/assignment/transfer audit, and verification-code waiting.
Terminology used by the CLI, REST API, MCP tools, and TUI:
- Provider: credentials and capability, such as SES send rights, Resend API access, or a sandbox.
- Source: an ingestion stream that brings mail into local storage, such as
provider:<id>,s3:<bucket>, Cloudflare-routed inbound storage,legacy, ororphaned:<id>. - Mailbox: the user-visible scope being browsed, such as all mail, one address, or one domain.
- Folder: a mailbox view such as
inbox,unread,sent,starred,archived,spam, ortrash.
Useful source-aware surfaces:
mailery inbox sources --json
mailery inbox mailboxes --source provider:<id> --json
mailery inbox search invoice --folder sent --source provider:<id> --json
curl 'localhost:3900/api/sources'
curl 'localhost:3900/api/mailboxes?source_id=legacy'mailery-mcpREST API
mailery serve exposes a dashboard plus two API surfaces:
- Dashboard / management API under
/api/*(providers, domains, addresses, emails, stats). - Authenticated programmatic API under
/api/v1/*for agents/apps, keyed on a scoped send key (Authorization: Bearer esk_…). Every call is scoped to the key owner's addresses, so one caller can't act as another tenant:
mailery serve # or: mailery-serve (HOST=0.0.0.0 to allow other machines)
curl -H "Authorization: Bearer $ESK" localhost:3900/api/v1/addresses
curl -H "Authorization: Bearer $ESK" -X POST localhost:3900/api/v1/provision/address -d '{"email":"[email protected]"}'
curl -H "Authorization: Bearer $ESK" -X POST localhost:3900/api/v1/send -d '{"from":"[email protected]","to":"[email protected]","subject":"hi","text":"yo"}'
curl -H "Authorization: Bearer $ESK" 'localhost:3900/api/v1/inbox?limit=50&offset=0&search=invoice' # scoped, paginated inboxLibrary API
Import the stable local API from @hasna/mailery. The public entrypoint covers
provider/domain/address CRUD, sending, inbound storage and listing, templates,
contacts and suppression, sequences, exports, ownership helpers, and scoped send
keys.
import {
sendWithFailover,
createProvider,
createAddress,
storeInboundEmail,
createTemplate,
suppressContact,
createSequence,
exportEmailsJson,
createOwner,
setAddressOwnerByRef,
createSendKey,
} from "@hasna/mailery";Inbound Email (AWS SES -> S3)
# Set up S3 bucket + SES receipt rules
mailery aws setup-inbound --domain example.com --bucket my-emails
# Pull received emails on demand
mailery inbox sync-s3 --bucket my-emails --prefix inbound/example.com/
# Read-state / organize (works for SES-S3, SMTP, Cloudflare-routed, and legacy imported mail)
mailery inbox list --unread # filters: --unread/--read/--starred/--archived/--label <l>
mailery inbox latest [email protected] --json
mailery inbox wait [email protected] --timeout 120
mailery inbox wait-code [email protected] --from openai --timeout 120
mailery inbox sync-status --json # S3 and realtime status
mailery inbox explain <id> # route/owner/readiness trace
mailery inbox read <id> # opening marks it read
mailery inbox star|archive|label <id> # --undo / --remove to reverseReal-time inbound (no manual sync)
Push delivery so mail lands automatically. setup-realtime wires SES → SNS → SQS
(and attaches the topic to the receipt rule); watch long-polls and auto-syncs:
mailery inbox setup-realtime example.com # creates SNS topic + SQS queue, saves the queue URL
mailery inbox watch # auto-delivers new mail in real-time (--once to poll once)Alternatively, point an SNS HTTP subscription at POST /webhook/ses-inbound on
mailery serve auto-confirms the subscription and syncs on each notification.
Self-Hosted Runtime (PostgreSQL/S3/SES)
The canonical local/self-hosted/cloud runtime contract lives in
docs/SELF_HOSTED_RUNTIME.md. The per-domain
aggregator and sending-readiness contract lives in
docs/DOMAIN_READINESS.md. Together these documents
are the source of truth for the active migration from local-authoritative mail to
a self-hosted PostgreSQL/S3 runtime.
Mailery is local-first. The public OSS default is local SQLite and files under
~/.hasna/emails/, with no remote dependency. Self-hosted runtime is opt-in,
and uses the emails slug for database URL compatibility: use
HASNA_EMAILS_DATABASE_URL, not HASNA_MAILERY_DATABASE_URL.
For managed or self-hosted PostgreSQL, set HASNA_EMAILS_DATABASE_URL to the
database connection string without printing or committing it. Self-hosted
installs can use the fallback EMAILS_DATABASE_URL.
Mailery modes:
local- all reads/writes stay in local SQLite/files.self_hosted- user/org-owned infrastructure. PostgreSQL is the source of truth for provider, mailbox, message, label, send, and state rows. S3 stores raw SES MIME and optional attachment objects. Local SQLite is a runtime cache that is prepared from PostgreSQL and flushed back after CLI commands; long running MCP/server processes flush periodically. For Hasna's own self-hosted deployment this means AWS RDS plus SES/S3, but the concrete cluster, bucket, and secret-path values live in private deployment secrets and are not package defaults.cloud- Hasna-operated Mailery Cloud SaaS athttps://mailery.co.
Deprecated remote and hybrid values are accepted as aliases only for the
deployment mode (MAILERY_MODE, HASNA_EMAILS_MODE, or legacy config keys) and
map to self_hosted. The lower-level storage sync mode remains separate:
HASNA_EMAILS_STORAGE_MODE=remote means PostgreSQL source of truth with local
runtime cache. HASNA_EMAILS_STORAGE_MODE=hybrid keeps local SQLite as source
and only syncs when mailery storage pull, mailery storage push, or
mailery storage sync --force is run explicitly.
# Configure RDS/PostgreSQL
export HASNA_EMAILS_DATABASE_URL="postgres://..."
# Optional self-hosted fallback:
# export EMAILS_DATABASE_URL="postgres://..."
# Optional explicit mode; default is local without a DB URL, self_hosted with one.
export MAILERY_MODE=self_hosted
export HASNA_EMAILS_STORAGE_MODE=remote
# Optional AWS/S3 settings for self-hosted inbound and attachments.
# Use your own bucket names and account-specific secrets.
export EMAILS_INBOUND_S3_BUCKET="your-mailery-inbound-bucket"
mailery config set attachment_storage s3
mailery config set attachment_s3_bucket "your-mailery-attachments-bucket"
# Check source-of-truth runtime status
mailery self-hosted status
# Apply PostgreSQL migrations
mailery self-hosted migrate
# One-time local SQLite → self-hosted PostgreSQL migration
mailery self-hosted migrate-localStorage internals are intentionally kept off the default library entrypoint. Import them from the explicit subpath when building storage tooling:
import { getStorageStatus, prepareSelfHostedRuntimeCache } from "@hasna/mailery/storage";See docs/SELF_HOSTED_RUNTIME.md for the source-of-truth contract.
Data
Stored in ~/.hasna/emails/ (SQLite + attachments).
Transport
The shared Streamable HTTP transport is the default (one process, many agents); pass
--stdio for a per-client stdio server:
mailery-mcp # http://127.0.0.1:8861/mcp (default)
mailery-mcp --port 8861 # explicit port
mailery-mcp --stdio # stdio transport (one server per client)
MCP_STDIO=1 mailery-mcp # same- Health:
GET http://127.0.0.1:8861/health->{"status":"ok","name":"mailery"} - Override port with
MCP_HTTP_PORTor--port
License
Apache-2.0 — see LICENSE
