Agent Email List — Free SMTP Relay for AI Agents

com.agentemaillist.ai由 ai.agentemaillist.com 發布官方發布者v0.2.0更新於 Oct 27, 2026

Free transactional email + SMTP relay. Agents self-signup, no API key needed to start.

已驗證Streamable HTTP可網頁執行開發者工具溝通與協作

概覽

AI 產生的概覽

免費交易型郵件服務的遠端 MCP 伺服器,助理可自行開帳號、驗證寄件網域並寄信。

可以做什麼
  • 建立帳號並取回 API 金鑰,不需要任何既有憑證
  • 列出帳號下的寄件網域、驗證狀態與當日剩餘額度
  • 註冊網域並取得需公布的 DNS 記錄,再重新讀取 DNS 以啟用
  • 寄送郵件,支援只跑完整流程而不實際寄出的測試模式
  • 查看近期已寄與已收郵件,可依方向與資料夾篩選
  • 查詢投遞事件以及目前寄送上限、額度與帳號狀態
適用情境
適合助理需要自行寄送交易型郵件、而不想先由人工註冊服務商的情況,也適合在正式寄送前端對端測試郵件流程。想確認網域是否已驗證、當日額度還剩多少時同樣有用。
執行需求
遠端 MCP 端點,透過 HTTP 連線,不需在本機安裝。第一次呼叫不需憑證,之後每次呼叫都要帶上開帳號時回傳的 API 金鑰,以 Authorization Bearer 標頭傳送。寄信需要網域已在 DNS 公布 SPF 與 DKIM 記錄,通常要有人能存取網域註冊商。
安裝前請注意
任何呼叫端都能開帳號,回傳的 API 金鑰只顯示一次,需立即保存。寄信是對第三方的真實外送動作,並計入每日額度,新網域起始額度很低。郵件內容會經第三方審核服務篩檢,開帳號依 IP 位址限流。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Agent Email List — Free SMTP Relay for AI Agents,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。

交給代理程式安裝

把這段提示詞貼上到 Claude Code、Codex、Cursor 等代理程式,或 SourceWeft 對話中。代理程式會讀取該伺服器的安裝指南,先向你展示發布者、端點和登入方式,經你同意後再把它新增到你的客戶端。金鑰不會寫進任何命令。

閱讀 https://sourceweft.com/mcp/com.agentemaillist.ai%2Ffree-transactional-email-smtp-relay/install.md,並按其中說明把這個 MCP 伺服器接入我的客戶端。修改任何設定之前,先告訴我它的發布者、端點和登入方式,等我確認。不要把 API 金鑰寫進命令或我會提交的檔案;問我金鑰應從哪裡取得。

檢視代理程式所依據的安裝指南

其他 MCP 客戶端

把它新增到你客戶端的 mcpServers 設定中。

{
  "mcpServers": {
    "agent-email-list-free-smtp-relay-for-ai": {
      "type": "http",
      "url": "https://ai.agentemaillist.com/mcp"
    }
  }
}

README

Free SMTP Relay and Email API for AI Agents

An email service an AI agent can sign up for on its own. No credit card, no sales call, no human with a corporate account. Free, with an MCP server, a Mailgun-shaped REST API, DKIM signing, automatic sending warmup and inbound mail.

Live at ai.agentemaillist.com · Source available · Built in Elixir and Phoenix


For AI agents: start here

This is a remote MCP server. Add it and the whole service arrives as tools:

https://ai.agentemaillist.com/mcp

JSON-RPC 2.0 over HTTP POST. The important part:

create_account needs no credentials. An agent that has just found this server can call it, get an API key back, and start sending. Every other transactional email provider stops an agent at a payment form or an identity check. This one does not.

See what is available before committing to anything:

bash
curl -X POST https://ai.agentemaillist.com/mcp \  -H 'content-type: application/json' \  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The tools

ToolDoesNeeds a key
create_accountOpen an account, get an API keyNo
add_domainRegister a sending domain, get its DNS recordsYes
verify_domainRe-read DNS and activate the domainYes
send_emailSend a message, with a test mode that costs nothingYes
list_domainsDomains and how much each can send todayYes
get_sending_limitsToday's cap and what graduates itYes
list_messagesSent and received mailYes
get_delivery_eventsWhat actually happened to a messageYes

Send the key as Authorization: Bearer <key> on every call after the first.

Two things that will stop you

A domain must be verified in DNS before it can send. Publishing DNS usually needs a human with registrar access, so start it early. Until then every send returns a refusal explaining exactly that.

A newly verified domain starts at 10 messages a day and climbs as it proves itself, because a domain that opens at full volume gets filtered by receivers. Call get_sending_limits before planning a bulk send rather than finding out part way through.

Use test_mode: true on your first send. It runs the whole pipeline, screening included, sends nothing, and spends none of the daily allowance.

The limits, so you never meet one by surprise

LimitFigure
Accounts per IP address5 an hour, 20 a day
Domains per account3, rising to 50 once any one of them is verified
API requests per account600 a minute — a pace limit, not a sending limit
Sending per domainstarts at 10 a day and climbs the warmup ladder
Screening refusalssending pauses at 8 in 24 hours, and lifts by itself

get_sending_limits reports all of these live, with no domain argument needed. Every 429 carries a Retry-After. Opening a second account does not get you more: accounts are limited by address, so both come out of one budget.


For developers: the REST API

Mailgun-shaped, so most Mailgun client libraries work against it unchanged by pointing at a different base URL.

bash
# An account and an API key, in one requestcurl -X POST https://ai.agentemaillist.com/v1/accounts \  -d '[email protected]' -d 'password=a sufficiently long password'
# Sendcurl -X POST https://ai.agentemaillist.com/v3/yourdomain.com/messages \  --user 'api:YOUR_KEY' \  -F from='[email protected]' \  -F [email protected] \  -F subject='Hello' \  -F text='Hello there.'

Full API reference, written for machines to read: ai.agentemaillist.com/llms.txt. Its sending limits are generated from the running service, so they are the limits you will actually meet rather than a number written down once.

There is also a web console at ai.agentemaillist.com/signup for setting up a domain by hand.


What it does

  • Free SMTP relay and email API. No trial clock, no card.
  • Send over SMTP or REST. Adding a domain issues SMTP credentials; the REST API is Mailgun-shaped.
  • Receive mail too. A real SMTP server on port 25, with inbound routing to your webhook. Not just sending.
  • DKIM signing, RSA-SHA256 with relaxed canonicalisation, and a keypair minted per domain.
  • Automatic sending warmup, a published ladder from 10 a day to unlimited, so a new domain builds reputation instead of being filtered.
  • Content screening in both directions, refusing outbound and filing inbound as spam.
  • Delivery events for every message, plus signed webhooks.
  • Limits that open as you prove yourself, rather than a flat cap: verifying a domain is what buys headroom, because it is the one thing a throwaway account cannot fake at scale.

Compared to the alternatives

ThisMailgunSendGridAmazon SES
An agent can sign up aloneYesNoNoNo
MCP serverYesNoNoNo
Free tierFree, no clockLimitedTrial, then paidPay per message
Inbound mailYesYesYesVia S3
Self-hostableYesNoNoNo

A longer comparison, including Brevo, Resend and SMTP2GO, is at ai.agentemaillist.com/free-smtp-relay.

Self-hosting

It is one Phoenix application and a Postgres database. One command deploys it:

bash
sudo bash deploy/deploy.sh --domain mail.yourcompany.com --email [email protected]

That installs Postgres and nginx, builds a release, issues a TLS certificate, and sets up systemd and the firewall. DEPLOY.md explains every step it takes and what to do when one fails.

Licence

Currently unlicensed, which means all rights reserved. If you want to use or contribute to this, say so and a licence will be added.

Maintained by Logan Besecker. Questions, bug reports and cold outreach all welcome at [email protected] or [email protected].


Running and operating it

Everything below is for someone running their own copy.

Running it

bash
mix setup            # deps, database, migrationsmix phx.server       # http://localhost:4005mix test

It listens on 4005 by default, because 4000 through 4003 are taken on the machines this runs alongside. PORT overrides it.

Database

Set credentials once, in .env at the project root:

bash
cp .env.example .env

Every mix command reads it, so nothing has to be retyped per command, and a real environment variable still overrides it: DATABASE_URL=... mix test does what it looks like. .env is gitignored; .env.example lists everything that can go in it.

Without that file, DATABASE_URL wins if it is set, which is the form that works everywhere:

bash
DATABASE_URL=ecto://user:pass@localhost/email_provider_dev mix setup

Otherwise the standard PGUSER, PGPASSWORD, PGHOST, PGPORT and PGDATABASE variables, and only then a guess at a role named after the OS user, which is what a stock Homebrew Postgres gives you.

The guess skips the OS user when that user is root. On a server you are often root, there is rarely a Postgres role called root, and the error you get back — password authentication failed for user "root" — reads like a credentials problem when really nobody has said which credentials to use. If you hit that on a fresh box, either set DATABASE_URL or create the role:

bash
sudo -u postgres psql -c "CREATE ROLE youruser LOGIN PASSWORD 'apassword' CREATEDB;"

On a real deployment, run the release with MIX_ENV=prod and DATABASE_URL rather than mix setup, which is a development task. One command does the whole thing, and DEPLOY.md explains every step it takes:

bash
sudo bash deploy/deploy.sh --domain ai.agentemaillist.com --email [email protected]

On a machine that is running other things

mix setup is a developer command and it is not a good neighbour. Use this instead:

bash
DATABASE_URL=ecto://user:pass@localhost/email_provider_dev bin/setup-server

Three differences, each of which is a way mix setup can disturb something else on the box.

It caps the build. Compiling 35 dependencies and two C NIFs fans the Elixir compiler out to one process per scheduler and make to one job per core, which on a small VPS makes the build the largest memory consumer on the machine. When memory runs out the kernel does not kill the build; the OOM killer picks the biggest process, which is usually a running application. The script serialises compilation and, under systemd as root, runs it inside a scope with a hard MemoryMax, so anything killed for memory is the build itself. Override with MEMORY_MAX=1G.

It opens two connections, not ten. Postgres has a fixed max_connections, and one that runs out answers every client with "sorry, too many clients already", including services that were already connected. The dev pool now defaults to 5 and reads POOL_SIZE; the script sets it to 2.

It never starts the application. mix setup boots the whole supervision tree to run priv/repo/seeds.exs, which opens a pool and starts the delivery queue. mix setup.server creates and migrates without booting anything.

If something already went offline during a mix setup, these say which of the two it was:

bash
sudo dmesg -T | grep -i -A2 'killed process'
bash
sudo grep -i "too many clients" /var/log/postgresql/*.log | tail

A note on PGDATABASE

Host, user and password are read from the environment. The database name is not. It is not a credential, it is which application's data this is, and PGDATABASE is a standard libpq variable that may already be exported on a shared box for some other service. Honouring it would point mix ecto.migrate at that service's database and create this application's tables inside it. To use a different database, name it in DATABASE_URL.

Environment

VariableMeaningDefault
OPENAI_API_KEYKey for the moderation endpointnone — screening is skipped
SSA_UIDSeriouslySimpleAnalytics account id for adoption reportingunset — nothing is reported
MODERATION_ENABLEDTurn screening off entirelytrue
MODERATION_ON_ERRORblock to fail closed when screening is unreachableallow
SMTP_RELAYSmarthost for outbound mailunset — mail is written to priv/local_mail
SMTP_PORT / SMTP_USERNAME / SMTP_PASSWORDSmarthost credentials587, none
SPF_HOSTWhat customers put in their SPF includemail.example.com
MX_HOSTWhat customers point their MX atmx.example.com

Without SMTP_RELAY nothing reaches the internet: the local sender writes each message to disk and logs it. That is the default on purpose.

Accounts and keys

bash
curl -s localhost:4005/v1/accounts \  -d [email protected] -d password='a sufficiently long password'

The response carries an API key. It is shown once — only a SHA-256 hash is stored, so a lost key is rotated rather than looked up. Authenticate either way:

bash
curl -s --user 'api:ep_live_...' localhost:4005/v3/domains     # Mailgun stylecurl -s -H 'Authorization: Bearer ep_live_...' localhost:4005/v3/domains

Keys carry scopes (messages:send, events:read, domains:write, …). Scopes are declared per action in each controller, next to the code they guard.

Custom domains

Adding a domain generates an RSA-2048 DKIM keypair. The private half stays in the database; the public half is what the customer publishes.

bash
curl -s --user 'api:KEY' localhost:4005/v3/domains -d name=mail.yourcompany.com

The response lists the records to publish, then:

bash
curl -s -X PUT --user 'api:KEY' localhost:4005/v3/domains/mail.yourcompany.com/verify

A domain is unverified until SPF and DKIM are both observed in DNS, and it cannot send until it is active. If the records later disappear it drops back to unverified and stops sending. MX is reported but not required — it is only needed to receive.

Outbound mail is signed RSA-SHA256 with relaxed/relaxed canonicalization, so a relay that re-folds a header or trims trailing whitespace does not break the signature.

Sending

bash
curl -s --user 'api:KEY' localhost:4005/v3/mail.yourcompany.com/messages \  -F from='Ada <[email protected]>' \  -F [email protected] \  -F subject='Hello' \  -F text='Hello there.' \  -F o:tag=welcome \  -F h:X-Campaign=spring \  -F v:customer_id=42

The from address must belong to the domain the key is sending for.

Endpoints

Messages

POST /v3/:domain/messagesSend. Fields below.
POST /v3/:domain/messages.mimeSend a pre-built MIME document. Still screened, still counted.
GET /v3/:domain/messagesList, filterable by folder and direction.
GET /v3/domains/:domain/messages/:keyRetrieve one stored message.

Send fields: from, to, cc, bcc, subject, text, html, o:tag, o:deliverytime, o:testmode, o:tracking-opens, o:tracking-clicks, h:* (headers to emit), v:* (variables that ride along and come back on events), recipient-variables, template, t:version, t:variables.

o:deliverytime takes RFC 2822 or ISO 8601 and is capped at three days out. o:testmode accepts and stores a message without sending it, and without spending warmup allowance. recipient-variables turns one request into one message per recipient, each with its own body and Message-ID, substituting %recipient.name%.

Domains

GET|POST /v3/domains · GET|PUT|DELETE /v3/domains/:domain · PUT /v3/domains/:domain/verify

Reporting

GET /v3/:domain/events (filter by event, recipient, tag, begin, limit) · GET /v3/:domain/stats/total · GET /v3/:domain/tags · GET /v3/:domain/limits (warmup position)

Suppressions

GET|POST /v3/:domain/bounces · GET|DELETE /v3/:domain/bounces/:address, and the same shape for unsubscribes and complaints. Enforced on every send; a hard bounce adds itself.

Templates

GET|POST /v3/:domain/templates · GET|DELETE /v3/:domain/templates/:name · POST /v3/:domain/templates/:name/versions

Substitution is {{name}} and nothing else — no expressions, no includes, no loops. Stored templates are attacker-controlled input in a multi-tenant service, and the smallest engine has the smallest blast radius. An unknown placeholder is left visible rather than blanked, so a typo shows up as {{frist_name}} instead of silently vanishing.

Routes (inbound)

GET|POST /v3/routes · GET|PUT|DELETE /v3/routes/:id

Expressions: match_recipient("regex"), match_header("name", "regex"), catch_all(). Actions: forward("https://…"), store(), stop(). Highest priority first, stopping at stop().

Webhooks

GET|POST /v3/domains/:domain/webhooks · GET|DELETE /v3/domains/:domain/webhooks/:id

Payloads are signed HMAC-SHA256(timestamp <> token, signing_key) with a per-domain secret, so one leaked secret cannot forge another customer's callbacks. The key is returned once, at creation.

Address validation

GET /v4/address/validate?address=… — syntax plus a live MX lookup. It does not probe the recipient's server with a partial SMTP conversation: that is what makes validation accurate, and it is also indistinguishable from the reconnaissance step of a directory harvest.

Operator dashboard

GET /admin — service-wide figures: accounts, domains, messages in and out, hard bounces, unsubscribes, complaints, screening outcomes, and how verified domains are spread across the warmup ladder.

Guarded by ADMIN_TOKEN, not by an API key, because these figures span every account and no customer credential should open them. With no token set the dashboard refuses to open at all, which is the right failure mode for a missing environment variable. The page itself is served without a token and carries no figures; it fetches GET /admin/stats with one, so landing on the URL uninvited shows a prompt rather than a count of anything.

One number it does not have: outbound messages refused by content screening. They are rejected before anything is stored, so they leave no row and no event. The dashboard says so rather than omitting it silently.

For people

/signup, /login, then /domains, /send, /messages, /account. Ordinary server-rendered pages, one URL each, so they can be linked and bookmarked.

They are a client of the same contexts the JSON API uses, not a second implementation: adding a domain from the form and from POST /v3/domains run the same code. The session holds a user id and nothing else, looked up per request, so suspending an account takes effect immediately rather than when its session expires.

HEEx rather than the string templates the landing page uses, because these render addresses, domain names and subject lines. All of that is customer input and ~H escapes it on the way out.

For agents

GET /llms.txt — an agent-facing description of this API, needing no key. It is rendered from the running service, so the sending ladder in it is the ladder actually enforced rather than a number written down once and left to drift. A test asserts both that the published rungs match EmailProvider.Warmup.stages/0 and that every endpoint the file advertises is really routed.

/robots.txt points at it.

Inbound

POST /v1/inbound/:domain — where the MTA hands us received mail, as raw message or as parsed fields. GET /v1/inbound/:domain/spam lists what screening filed away.

Automatic warmup

A new domain that opens at full volume gets filtered, so every domain climbs a ladder. Stages are configured in config/config.exs, not hard-coded:

StageDaily capGraduates when
110it has sent on 5 separate days
2201,000 messages sent on this rung
3100a further 1,000 on this rung
41,000a further 10,000 on this rung
5unlimited—

Two things worth knowing:

"Days of sending" means days it actually sent on, not days since the domain was created. A domain idle for a week has not warmed up for a week.

Each rung's number is its own allowance, not a lifetime total. A domain leaves the 20/day rung after 1,000 messages at 20/day, then leaves the 100/day rung after a further 1,000. Where that lands in absolute terms depends on how hard the domain sent during its first five days: one that maxed out rung 1 has 50 messages behind it, so its rungs run 50 to 1,050 to 2,050 to 12,050, while one that trickled reaches each rung a little sooner. GET /v3/:domain/limits reports progress through the current rung, which is the number that answers "how much longer at this cap?", alongside the absolute total it graduates at.

Use {:lifetime_sent, n} instead of {:stage_volume, n} in config/config.exs for a rung that should graduate at an absolute total.

Capacity is reserved before dispatch and released if the send never happens, so a crash between the two costs a few sends rather than letting a domain overrun. Reservation is a single atomic upsert: two requests arriving together cannot both read "9 sent today" and both be allowed. GET /v3/:domain/limits reports the current rung, today's headroom and what graduates it; a refused send comes back 429 with the same detail and a retry_after_seconds.

Warmup can be disabled per domain with warmup_enabled.

Content screening

Every message is screened through OpenAI's moderation endpoint, which is free to call. The consequence differs by direction:

  • Outbound — flagged content is refused with a 403 before it reaches the wire, and before it spends any warmup allowance. The verdict is stored on the message so a refusal can be explained afterwards rather than being a 403 in a log.
  • Inbound — flagged content is delivered but filed in spam rather than inbox. Dropping incoming mail outright loses real messages to false positives; filing it does not.

If the moderation service is unreachable the default is to allow and record the message as unscreened, so a third-party outage does not take the service down with it. MODERATION_ON_ERROR=block fails closed instead. "Unscreened" is a distinct state from "screened and clean" — it carries no check timestamp, so nothing downstream can mistake one for the other.

SMTP

The service speaks SMTP in both directions. Neither listener is on by default: receiving needs port 25, and sending directly needs outbound port 25, and both are decisions rather than defaults.

Sending

Three ways out, in order of precedence.

SettingRoute
SMTP_RELAYThrough a smarthost you chose. Demands STARTTLS and verifies the certificate.
DIRECT_DELIVERY=trueStraight to each recipient's MX, no smarthost.
neitherWritten to priv/local_mail. Nothing leaves the machine.

Direct delivery groups recipients by domain, looks up each domain's MX records, and tries them in preference order, falling back to the domain's own A record when it publishes no MX as RFC 5321 requires. A 5xx from a destination ends the attempt; anything else moves to the next host, because it usually means that host is unreachable rather than the mail being unwanted.

TLS is deliberately weaker here than to a smarthost: opportunistic and unverified. A smarthost is one server you chose and can hold to a standard. The open internet is full of receiving servers with self-signed or mismatched certificates and no prior agreement to check them against, so demanding verification would not make delivery safer, it would stop it working. This is what every other MTA does and what RFC 7435 calls opportunistic security.

Most hosts block outbound port 25, Vultr included. Until that is lifted for the machine, every direct delivery times out. Ask support to unblock it, or use a smarthost.

Receiving

SMTP_RECEIVE_ENABLED=true opens port 25 and accepts mail for hosted domains. SMTP_SUBMISSION_ENABLED=true opens 587, where a customer's own software authenticates with the credentials issued when their domain was added and then sends through us. Submission runs the same pipeline as the REST API, so screening, the suppression list and the warmup ladder all still apply.

The property that matters is not being an open relay, and it lives in one function, handle_RCPT/2. On port 25 a recipient is accepted only if its domain is one we host and is active; everything else gets 550 5.7.1. A hosted domain that is not verified yet gets 450 instead, so a legitimate sender retries once the customer finishes their DNS rather than being told permanently to go away. Relaying becomes permitted only once a session has authenticated, which is the entire purpose of the submission port and the reason it must never be port 25. AUTH is advertised only on the submission port, because offering it on 25 turns every customer's SMTP password into something guessable from anywhere.

Port 25 needs root or CAP_NET_BIND_SERVICE. A listener that cannot bind is logged and skipped rather than taken as a reason for the application not to start: a provider that cannot receive today should still serve its API and keep sending.

DNS this deployment needs

ai.agentemaillist.com already resolves to the box. These do not exist yet and are what make domain verification and delivery work:

TypeNameValueFor
TXTai.agentemaillist.comv=spf1 ip4:155.138.220.76 ~allso include:ai.agentemaillist.com in a customer's SPF authorises this machine
PTR155.138.220.76ai.agentemaillist.comset in the Vultr panel, not in DNS; receiving servers compare it against HELO

The root domain's existing MX points at Namecheap forwarding for ordinary mail to @agentemaillist.com. Everything here lives under ai. so that is left alone.

How delivery works

Delivery runs concurrently, bounded by DELIVERY_CONCURRENCY. It is almost entirely waiting on DNS, a TCP connect and a conversation with a server on the other side of the internet, so done one at a time the queue moves at the speed of its slowest recipient and one server taking thirty seconds stalls everything behind it. The ceiling stays under POOL_SIZE because each in-flight delivery holds a database connection.

The queue is the messages table rather than a separate broker. That costs a poll every few seconds and buys one-owner semantics: a claim is an UPDATE guarded by the current status, so two nodes racing for the same message cannot both win, and a node that dies mid-send leaves a row the stale-claim sweep returns to the queue. A 5xx fails the message permanently, suppresses the address and refunds its warmup allowance; a 4xx is retried up to five times.

Bcc is put on the envelope but never written into the headers, so blind recipients stay blind.

Layout

lib/email_provider/  accounts.ex          users, API keys, scopes  domains.ex           registration, DKIM keys, DNS records, verification  mail.ex              the send and receive pipeline  warmup.ex            the ladder and its counters  moderation.ex        OpenAI screening client  suppressions.ex      bounces, unsubscribes, complaints  templates.ex         stored bodies and substitution  routes.ex            inbound matching and actions  webhooks.ex          signed callbacks  delivery/    mime.ex            render RFC 5322    dkim.ex            relaxed/relaxed RSA-SHA256 signing    inbound.ex         parse MIME, decode RFC 2047 headers    sender.ex          SMTP and local adapters    queue.ex           claim and dispatch

Tests

bash
mix test

Nothing in the suite can reach the network: screening is off, the dispatch loop is not running, and the sender writes to a temp directory. config/runtime.exs skips its environment-driven configuration under test so a stray variable on a developer's machine cannot point the suite at a live relay or a paid API.

Account profiles

Every account holder gets a rolling description of about 200 words, rewritten each time a message moves in either direction. Two inputs feed it:

  • A CSuiteFinder enrichment lookup on the account's own address, via /csuitefinder/email/enrich. The response is cached on the profile row for 30 days, because the provider bills per result and a job title does not change between two emails.
  • A digest of mail activity: volume and direction, first and last activity, cadence per active day, top correspondents, recent subject lines, sending domains, tags, and a hard-capped excerpt of recent bodies.
bash
curl -s --user 'api:KEY' localhost:4005/v1/profilecurl -s --user 'api:KEY' -X POST localhost:4005/v1/profile/refreshcurl -s --user 'api:KEY' localhost:4005/v1/profile/signals

The stored row keeps the description and the enrichment record and activity digest it was written from, and the API returns all three. A description nobody can account for is not one you can act on, correct, or defend.

Two generators

EmailProvider.Profiles.Generator picks by config. With an OpenAI key it uses a chat model, prompted to stay on the evidence: no invented employers or titles, uncertain material attributed, and no inference about protected characteristics. Without a key it falls back to a deterministic builder that composes the paragraph from the facts directly, no model and no network. That is what the test suite runs, so the tests exercise the real path rather than a mock, and the feature works out of the box.

The deterministic generator says what the data supports and stops, so a thin record produces a short description rather than a padded one. Only the model adapter targets the full 200 words. Both are trimmed to 200, at a sentence boundary where there is one.

Cost

min_interval_seconds defaults to 0, which is one rewrite per message and, with the model adapter, one model call per message. That is what was asked for and it is the expensive setting. Raise it in config/config.exs to coalesce refreshes on busy accounts; every message is still counted, so the next rewrite knows how much it missed.

The work runs in a task off the send path, so neither the enrichment call nor the model can add latency to an API request or hold up a delivery. Failures are recorded on the row and never propagated: a profile that could not be rewritten keeps the description it had.

Scope

GET /v1/profile returns the calling account's own description and nothing else. There is no endpoint that returns somebody else's, and the enrichment lookup is on the account holder's own address, not on the people they correspond with.

The earlier note here said I had not built this. You confirmed you wanted it, so it is built as specified. Two things are worth deciding deliberately rather than by default, because they are policy rather than code: whether your terms tell account holders that this exists, and whether they can read or switch off what is held about them. EmailProvider.Profiles.enabled?/0 and the /v1/profile endpoint are the hooks for the second if you want it.

來源:README.md,提交 5d0da8a

工具

8

create_account

Create an account on this email service and get an API key back. Needs no existing credentials, so an agent with nothing can call this first. The key is returned once and never again. Free, with no card and no trial clock.

輸入參數nameemail必填password必填

list_domains

List the sending domains on this account, with their verification state and how much of today's sending allowance each has left.

無需輸入參數。

add_domain

Register a domain to send from, and get the DNS records that must be published before it will work. Publishing DNS usually needs a human with registrar access, so start this early. The domain cannot send until verify_domain succeeds.

輸入參數name必填

verify_domain

Re-read DNS now and mark the domain active if its SPF and DKIM records are visible. DNS takes minutes to hours to propagate; poll this every few minutes, not every few seconds.

輸入參數name必填

send_email

Send a message. The domain must be verified and the from address must belong to it. Counts one against the daily allowance per recipient. Set test_mode true to run the whole pipeline, including content screening, without sending anything and without spending allowance — do that first when building.

輸入參數to必填from必填htmltextdomain必填subjecttest_mode

get_sending_limits

How much this domain may send today and what graduates it to the next rung, plus this account's standing: how many domains it may hold and whether sending is paused. New domains start at a low daily cap and climb as they prove themselves, so check this before planning a bulk send rather than discovering it part-way. Call it with no domain for the account-level answer alone.

輸入參數domain

list_messages

Recent messages for a domain, sent and received. Use direction to filter, and folder to separate inbox from spam on received mail.

輸入參數limitdomain必填folderdirection

get_delivery_events

What happened to messages on this domain: accepted, delivered, failed, and so on. A send returning success means queued, not delivered; this is where the delivery outcome actually appears.

輸入參數eventlimitdomain必填recipient

版本歷史

1
  1. v0.2.0最新Sep 16, 2026

評分

已滿足適用檢查項的 90%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通必需25 分
  • 已宣告認證方式必需10 分
  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分

    請在儲存庫或註冊中心條目中宣告授權條款。