email

star 68

Send and read email from the iii engine — SMTP send, IMAP read, and real-time `IDLE` push as a subscribable trigger type. Credentials live in `harness/auth-credentials`; this worker fetches them on every connect.

iii-hq By iii-hq schedule Updated 6/2/2026

name: email description: >- Send and read email from the iii engine — SMTP send, IMAP read, and real-time IDLE push as a subscribable trigger type. Credentials live in harness/auth-credentials; this worker fetches them on every connect.

email

The email worker speaks SMTP and IMAP through persistent per-account connections. Every callable surface lives under the email::* namespace. The transport is chosen per account in config.yamlprovider: smtp for send-only accounts, provider: imap for two-way accounts that also need to read and react to inbound mail.

The worker refuses to fall back to polling. If an IMAP server does not advertise the IDLE capability, the supervisor fails at startup with E610. Inbound messages flow through the email::new-mail trigger type, fanned out by an IDLE-driven dispatcher the moment a server-side EXISTS notification lands. Credentials never live in this worker — every IMAP login and every email::send calls auth::get_token against harness/auth-credentials under provider key email::<account>.

When to Use

  • An agent needs to send a transactional email right now (email::send).
  • You need to enumerate configured accounts and their capabilities so an LLM picks the right account field (email::accounts::list).
  • You need to page recent messages in an IMAP folder by UID with a stable cursor (email::list).
  • You need the full parsed body of one message plus attachment part refs (email::get).
  • You need to stream IMAP SEARCH results as NDJSON as the server returns matches (email::search).
  • You need to add or remove a system flag — \Seen, \Flagged, \Answered, \Deleted, \Draft (email::flag).
  • You need to move a message to another folder atomically with RFC 6851 UID MOVE (email::move).
  • You need to stream an attachment's raw bytes onto a response channel with no in-memory buffering (email::attachment::get).
  • You need to react to inbound mail the instant it lands in a configured (account, folder) (email::new-mail trigger type).

Boundaries

  • Not a mail server — this is a client. Bring your own SMTP and IMAP endpoints.
  • Not an OAuth flow runner in 0.1.0 — use app-password style credentials via harness/auth-credentials until the OAuth follow-up PR.
  • Not a polling fallback — IMAP servers without IDLE fail fast with E610 so the failure stays visible instead of silently degrading.
  • Not a credential store — every connect re-fetches the secret from harness/auth-credentials. Pair the two workers; deploying email alone is unsupported.
  • email::flag with flag: "deleted" does NOT expunge — use email::move to a Trash folder for visible deletion.
  • email::search and email::attachment::get use a streaming response channel; they're invoked by sibling workers passing a StreamChannelRef, not directly from iii trigger.

Functions

  • email::send — deliver a message via the account's SMTP transport (STARTTLS or plain on a trusted network).
  • email::accounts::list — enumerate configured accounts with provider, from, can_send, can_read, and folders.
  • email::list — page recent UIDs in a folder, newest first, with next_since_uid cursor.
  • email::get — fetch one message by UID; returns parsed html, text, headers, and an attachments[] list of part refs.
  • email::search — stream IMAP SEARCH results as NDJSON frames onto a StreamChannelRef; one header-summary object per match.
  • email::flag — add or remove a system flag on a UID via UID STORE +FLAGS.SILENT / -FLAGS.SILENT.
  • email::move — move a UID to another folder with RFC 6851 UID MOVE; falls back to COPY + STORE \Deleted when the server lacks MOVE.
  • email::attachment::get — stream attachment bytes by part_id onto a StreamChannelRef chunk-by-chunk; no in-memory buffering.

SMTP send timeout is limits.send_timeout_ms (default 30 s). IMAP connect timeout is limits.imap_connect_timeout_ms (default 15 s). Total recipients across to + cc + bcc cap at limits.max_recipients (default 100). Each attachment caps at limits.max_attachment_bytes (default 25 MiB).

Stream-source attachments (source.kind = "stream") return E699 in 0.1.0; use source.kind = "base64" until the symmetric attachment-send path lands.

Reactive triggers

Register an email::new-mail trigger when a function should fire automatically the moment a new message arrives in a configured (account, folder) — without polling with email::list.

Reach for it when:

  • A support workflow should kick off the instant a ticket email lands.
  • A harness session should ingest new mail as an additional turn.
  • An archival worker should mirror inbound mail into a database.

Do not bind when:

  • The account is provider: smtp (send-only); only provider: imap accounts open an IDLE listener.
  • You only need to act on outbound mail — email::send already gives you the SMTP response synchronously.

How to bind

  1. Register a handler: registerFunction('my-worker::on-mail', handler).
  2. Register the trigger:
iii.registerTrigger({
  type: 'email::new-mail',
  function_id: 'my-worker::on-mail',
  config: {
    account: 'support',
    folder: 'INBOX',
    handler_timeout_ms: 30000,
  },
})

Config: account (required, must be a configured account with an imap: block), folder (default "INBOX", must appear in config.accounts.<name>.imap.folders), handler_timeout_ms (default 30 000).

Event payload per inbound message:

{
  "account":    "support",
  "folder":     "INBOX",
  "uid":        12345,
  "message_id": "<abc@mx.example.com>",
  "from":       "alice@example.com",
  "subject":    "Ticket #42",
  "snippet":    "first ~200 chars of body",
  "ts":         "2026-05-28T10:14:00+00:00"
}

The dispatch is event-driven off the IMAP server's EXISTS push — within milliseconds of a new message landing in the watched folder.

Delivery semantics

Best-effort, at-most-once. The dispatcher fires iii.trigger per subscriber and waits up to handler_timeout_ms. There is no upstream durable queue: IMAP IDLE is a wakeup mechanism, not a delivery guarantee.

  • A subscriber handler that panics, times out, or returns an error is logged and skipped — the event is NOT redelivered.
  • If the worker is down when a message lands, no email::new-mail event fires for it. On reconnect the supervisor sets the high-water mark to the current UIDNEXT - 1; UIDs that arrived during downtime are reachable via email::list but do NOT replay as events.
  • For at-least-once / replay, write each email::new-mail event to a durable store from your handler (database::execute, storage::putObject, …) before doing meaningful work, and use email::list with a persisted since_uid cursor to catch up on downtime gaps.

Errors

Stable across the trigger boundary. Match on the code field of the returned envelope.

Code When
E600 Unknown account name
E601 email::send with empty to
E602 Total recipients over limits.max_recipients
E603 Account missing the required transport block
E604 email::send with neither html nor text
E605 Attachment over limits.max_attachment_bytes
E606 auth::get_token upstream call failed
E607 No credential stored for the account
E608 Credential payload missing username / password
E609 Address parse / MIME build failure
E610 IMAP server lacks IDLE — refusing to fall back to polling
E612 IMAP UID SEARCH failed
E613 Folder not in account's imap.folders config
E614 IMAP connect / TLS handshake failed
E615 Plain (non-TLS) IMAP refused
E616 IMAP login failed
E617 IMAP SELECT failed
E619 IMAP body fetch / MIME parse failed
E620 SMTP send failed
E621 Response channel close failed
E622 Unknown flag name
E623 IMAP STORE failed
E624 IMAP COPY / STORE \Deleted fallback failed
E625 IMAP attachment-part fetch failed
E626 Attachment payload malformed (e.g. invalid base64)
E627 email::move partial: copy succeeded but STORE \Deleted failed — message in BOTH folders, reconcile
E699 Not yet implemented in 0.1.0
Install via CLI
npx skills add https://github.com/iii-hq/workers --skill email
Repository Details
star Stars 68
call_split Forks 11
navigation Branch main
article Path SKILL.md
More from Creator