@framers/agentos-ext-channel-email 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @framers/agentos-ext-channel-email
2
2
 
3
- Email for AgentOS. `EmailService` sends mail through SMTP or Resend's HTTPS API, and reads, searches, extracts codes from and replies to mail through IMAP. The pack's factory wraps the service in a messaging-channel adapter and five tools for an agent. Two stores on a host's SQL database keep what a sender reads before a send: a suppression record and the tokens of unsubscribe links.
3
+ Email for AgentOS. `EmailService` sends mail through SMTP or Resend's HTTPS API, and reads, searches, extracts codes from, replies to, moves, labels, flags and deletes mail through IMAP. The pack's factory wraps the service in a messaging-channel adapter and five tools for an agent. Two stores on a host's SQL database keep what a sender reads before a send: a suppression record and the tokens of unsubscribe links.
4
4
 
5
5
  ## Installation
6
6
 
@@ -95,7 +95,7 @@ The subject goes out on one line through both transports: each line break in it,
95
95
 
96
96
  ## Suppressions and unsubscribe tokens
97
97
 
98
- Two stores a sender reads, each on the host's SQL database: `SqlSuppressionStore` keeps the addresses not to mail, each named by a hash of it, such as its SHA-256, and `SqlRecipientCapability` keeps opaque tokens, such as an unsubscribe link's, each naming a recipient and a scope. Both take [`@framers/sql-storage-adapter`](https://www.npmjs.com/package/@framers/sql-storage-adapter)'s adapter on SQLite or PostgreSQL, or any handle with its `run` and `get` (`?` placeholders, positional parameters), so the pack gains no dependency. The handle answers each `get` from the database: one that answers from a cache of its earlier reads, such as the adapter's deprecated `SupabaseAdapter` after `enableCache()`, can answer a row as it stood before a later write, so `find` could still answer a token after its `revoke`. A handle's `run` answers `changes`, the rows the statement wrote. A handle whose answer holds no such count, because it leaves `changes` out or gives something other than a whole number of 0 or more (the adapter's Capacitor handle gave an object before 0.6.14), still records, issues and revokes, but `suppress` then answers `false` and `revoke` 0, whatever they wrote.
98
+ Two stores a sender reads, each on the host's SQL database: `SqlSuppressionStore` keeps the addresses not to mail, each named by a hash of it, such as its SHA-256, and `SqlRecipientCapability` keeps opaque tokens, such as an unsubscribe link's, each naming a recipient and a scope. Both take [`@framers/sql-storage-adapter`](https://www.npmjs.com/package/@framers/sql-storage-adapter)'s adapter on SQLite or PostgreSQL, or any handle with its `run` and `get` (`?` placeholders, positional parameters), so the pack gains no dependency. The handle answers each `get` from the database: one that answers from a cache of its earlier reads can answer a row as it stood before a later write, so `find` could still answer a token after its `revoke`. Two such handles are the adapter's deprecated `SupabaseAdapter` after `enableCache()`, and an adapter a host builds on the adapter's `BaseStorageAdapter` at its default performance tier, `balanced`, which keeps each read for five seconds: give such an adapter the tier `accurate`, which keeps none. A handle's `run` answers `changes`, the rows the statement wrote. A handle whose answer holds no such count, because it leaves `changes` out or gives something other than a whole number of 0 or more (the adapter's Capacitor handle gave an object before 0.6.14), still records, issues and revokes, but `suppress` then answers `false` and `revoke` 0, whatever they wrote.
99
99
 
100
100
  ```ts
101
101
  import { createHash } from 'node:crypto';
@@ -127,7 +127,7 @@ const grant = await tokens.find(token); // { recipient: 'account-1', scope: 'wee
127
127
 
128
128
  `suppress(scope, addressHash, reason, day)` records an address in a scope with its reason and its day (`YYYY-MM-DD`), and no end: the store has no call that lifts an address, so an operator lifts one by deleting its row through the same handle, as in `await db.run('DELETE FROM mail_suppressions WHERE scope = ? AND address_hash = ?', [DEFAULT_SUPPRESSION_SCOPE, addressHash])` for the example's prefix `mail`. The `?` placeholders are the adapter's, which binds the values given and on PostgreSQL writes `$1` and `$2` in their place. In a console, write the scope and the hash as quoted literals instead: PostgreSQL reads a `?` as an operator and refuses the statement, and the sqlite3 shell binds a `?` to NULL, so the statement deletes nothing. The reason is `bounce`, `complaint`, `suppressed` (the provider refused a message because its own list held the address) or `manual`. A second record of the address in the scope writes nothing and keeps the first, so a provider's repeated event is harmless; `suppress` answers `true` only for the call that recorded it, and two hosts recording one address at once leave one row. `isSuppressed(scope, addressHash)` answers for one scope. A scope is one list, and a host that keeps one passes `DEFAULT_SUPPRESSION_SCOPE` (`mail`) everywhere. `addressHash` is the address's hash as 64 lower-case hexadecimal characters, such as its SHA-256, which the host computes the same way wherever it reads or writes the record. Any other value, an address among them, throws a `TypeError` that holds none of it, so a mistake stores nothing and is never answered as not suppressed. The store takes any 64 lower-case hexadecimal characters, so the host chooses the hash. A plain SHA-256, as in the example, lets anyone who holds the table check an address they guess against it; a host that wants the table to tell nothing of an address it does not already hold computes HMAC-SHA256 under a secret key of its own instead, the same way at every reader and writer. `SUPPRESSION_REASONS` lists the four reasons, and it is frozen, since `suppress` checks a reason against it.
129
129
 
130
- `issue(recipient, scope, nonce, key)` answers base64url of HMAC-SHA256 over the nonce under the key, 43 characters, and stores the token's SHA-256 beside the recipient and the scope: the token carries neither, and the table holds nothing it can be recovered from. The same nonce under the same key gives the same token, so each message takes a nonce of its own, such as its id. Issuing it again for the same recipient, scope and expiry writes nothing; for another recipient, scope or expiry it throws, and the first token keeps all three. The key is 32 random bytes or more, given as a `Uint8Array` or a `Buffer` and kept secret, such as `crypto.randomBytes(32)` stored with the host's other secrets; the store cannot judge how random a key is. A key made in another realm, such as a `vm` context or a test runner's sandbox, is taken as the same bytes made here. A different key gives different tokens for the same nonces, and the same key the same tokens, whatever the prefix: the prefix keeps two hosts' tables apart, not their tokens. Two hosts on one database that share a key and use one nonce draw one token, and each one's `find` answers its own row for it, so a link one host sent could act on the other host's recipient. Each host takes a key of its own, or nonces no other host uses, such as its own name before each message id. `find(token)` answers the recipient and the scope, or `null` for a token that is malformed, unknown, revoked or past its expiry: it looks the token up by its SHA-256 and compares in constant time. `revoke(recipient)` ends every token of the recipient, in every scope, and answers how many it ended. Each revoked row stays, marked with the time of the revoke, so `issue` refuses a revoked token's nonce for every recipient, scope and expiry: a revoked link never works again and never comes to name someone else. A message that needs a working token after a revoke takes a new nonce, and the token it gets works. A token has no expiry unless `issue`'s options set `expiresAt`, in epoch milliseconds.
130
+ `issue(recipient, scope, nonce, key)` answers base64url of HMAC-SHA256 over the nonce under the key, 43 characters, and stores the token's SHA-256 beside the recipient and the scope: the token carries neither, and the table holds nothing it can be recovered from. The same nonce under the same key gives the same token, so each message takes a nonce of its own, such as its id. Issuing it again for the same recipient, scope and expiry writes nothing; for another recipient, scope or expiry it throws, and the first token keeps all three. The key is 32 random bytes or more, given as a `Uint8Array` or a `Buffer` and kept secret, such as `crypto.randomBytes(32)` stored with the host's other secrets; the store cannot judge how random a key is. A key made in another realm, such as a `vm` context or a test runner's sandbox, is taken as the same bytes made here. A different key gives different tokens for the same nonces, and the same key the same tokens, whatever the prefix: the prefix keeps two hosts' tables apart, not their tokens. Two hosts on one database that share a key and use one nonce draw one token, and each one's `find` answers its own row for it, so a link one host sent could act on the other host's recipient. Each host takes a key of its own, or nonces no other host uses, such as its own name before each message id. `find(token)` answers the recipient and the scope, or `null` for a token that is malformed, unknown, revoked or past its expiry: it looks the token up by its SHA-256 and compares in constant time. `revoke(recipient)` ends every token of the recipient, in every scope, and answers how many it marked: each of the recipient's tokens not revoked before, one past its expiry among them. Each revoked row stays, marked with the time of the revoke, so `issue` refuses a revoked token's nonce for every recipient, scope and expiry: a revoked link never works again and never comes to name someone else. A message that needs a working token after a revoke takes a new nonce, and the token it gets works. A token has no expiry unless `issue`'s options set `expiresAt`, in epoch milliseconds.
131
131
 
132
132
  `suppressionStoreDdl(prefix)` makes the table `<prefix>_suppressions` (`scope`, `address_hash`, `reason`, `suppressed_on`), `recipientCapabilityDdl(prefix)` makes `<prefix>_capabilities` (`token_hash`, `recipient`, `scope`, `expires_at`, `revoked_at`) with its index on the recipient, and `emailPreferencesDdl(prefix)` makes both. The prefix is a lower-case letter and at most 39 lower-case letters, digits or underscores; any other value throws a `RangeError` from the DDL functions and both constructors. The prefix keeps two hosts' tables apart on one database. On SQLite, take a prefix other than `sqlite` and those beginning `sqlite_`: their tables would begin `sqlite_`, names SQLite keeps for its own tables, so its `CREATE TABLE` refuses them ([CREATE TABLE](https://www.sqlite.org/lang_createtable.html)) and every call of a store on such a prefix then fails on the missing table. PostgreSQL makes them. The statements are `CREATE ... IF NOT EXISTS`, so a migration can run them again, and they run on SQLite and PostgreSQL, from one process at a time: PostgreSQL does not promise that two `CREATE TABLE IF NOT EXISTS` of one new table at once both succeed, and one of them can fail on a duplicate key in `pg_type` (pgsql-hackers, 9 October 2018: [the report](https://www.postgresql.org/message-id/93336f06-86c2-71b6-abe3-0d42cc614b2b%40postgrespro.ru) and [the reply](https://www.postgresql.org/message-id/22700.1539093909%40sss.pgh.pa.us)). A host whose processes start together runs them in a migration, such as the adapter's `runSchemaMigrations` (0.7.2 or later), which takes an advisory lock on PostgreSQL. A token's row refuses its nonce for as long as it stays, revoked or not. A host that deletes a recipient's rows, by its own foreign key with `ON DELETE CASCADE` or by hand, never issues their nonces again: each message's nonce is one no other message ever had, such as a random id or a row id that is never reused. A host that cannot promise that calls `revoke` with each deletion and keeps the rows. Each store checks every argument before its first statement. A scope, a recipient and a nonce are strings of 1 to 512 UTF-16 code units (JavaScript's `length`, in which a character past U+FFFF counts two) with no U+0000, which PostgreSQL's text refuses, and no lone surrogate, which UTF-8 cannot encode, so a driver would store something else in its place: any other value throws a `TypeError` that holds none of it, and a name is stored as given on every backend. A database error passes through as it was thrown. A store made on the handle that the adapter's `transaction` gives its callback writes inside that transaction. On a server, open the database as the example does, with the adapter's `strict` option (0.6.15 or later): without it, a `url` that is unset opens a local SQLite database in its place, and the record a sender reads before each send would live there.
133
133
 
@@ -149,13 +149,13 @@ The wait is the answer's `retry-after` when it has one, else as many seconds as
149
149
 
150
150
  `readInbox`, `searchEmails`, `extractCodes` and `replyToEmail` read through IMAP: the `imap` account (`host`, `user` and `password`, with `port` and `secure`, by default TLS on port 993), or the SMTP account when `imap` is not given. With Resend, reading needs `imap`. The pack's factory reads the account from the secrets `email.imap.host`, `email.imap.user` and `email.imap.password`, or from `IMAP_HOST`, `IMAP_USER` and `IMAP_PASSWORD`.
151
151
 
152
- `readInbox(folder?, limit?)` gives the last `limit` messages of `folder`, newest first: INBOX and 20 when they are not given. `searchEmails({ query, folder?, since?, limit? })` finds the messages whose subject, body or From holds `query`, ignoring case, received on or after the day `since` names when it is given, and gives the last `limit` it finds (20 by default), newest first. Each search answers UIDs, and the messages are fetched by those UIDs, which need not follow the messages' sequence numbers once a message has been removed ([RFC 3501 section 2.3.1.1](https://www.rfc-editor.org/rfc/rfc3501#section-2.3.1.1)).
152
+ `readInbox(folder?, limit?)` gives the last `limit` messages of `folder`, newest first: INBOX and 20 when they are not given; given one object instead, it can read after a UID ([the read surface](#the-read-surface)). `searchEmails({ query, folder?, since?, limit? })` finds the messages whose subject, body or From holds `query`, ignoring case, received on or after the day `since` names when it is given, and gives the last `limit` it finds (20 by default), newest first. Each search answers UIDs, and the messages are fetched by those UIDs, which need not follow the messages' sequence numbers once a message has been removed ([RFC 3501 section 2.3.1.1](https://www.rfc-editor.org/rfc/rfc3501#section-2.3.1.1)).
153
153
 
154
154
  `limit` is a whole number of at least 1. `since` is an ISO 8601 date, such as `2026-10-15`, alone or followed by a time and an offset, such as `2026-10-15T09:30:00-05:00`. The search finds the messages the server received on the day the date names, as written, or later, the time of day disregarded ([RFC 3501 section 6.4.4](https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4)), on every server: the time and the offset are checked, then disregarded, and imapflow is given the day as text, which it sends as `SINCE` and that day. Given a `Date`, imapflow would search a server that announces `WITHIN` from that moment, to the second ([RFC 5032 section 2](https://www.rfc-editor.org/rfc/rfc5032#section-2)), and any other server from its day in UTC.
155
155
 
156
156
  `limit` and `since` count as not given when they are `undefined` or `null`, and `since` also when it is empty. Any other `limit` or `since` throws an `Error` before any mail is read: the fetch would otherwise name a message past the last one, which a server answers `BAD`, and imapflow leaves a date it cannot read out of the search, which then answers mail of every date. The refused `since` values include a number, text in another form such as `October 15, 2026`, which `Date` reads by guesswork and in the local time zone, and a day its month lacks such as `2026-02-30`, which `Date` reads as 2 March.
157
157
 
158
- Each message read carries its `messageId`, the first address of its `from` and of its `to`, its `subject`, `date`, `body`, `flags` and `uid`. `date` is the message's Date field in ISO 8601, or the empty string when it has none or the field does not read as a date, so one such message never fails the read.
158
+ Each message read carries its `messageId`, the first address of its `from` and of its `to`, its `subject`, `date`, `body`, `flags` and `uid`, and the fields [the read surface](#the-read-surface) lists. `date` is the message's Date field in ISO 8601, or the empty string when it has none or the field does not read as a date, so one such message never fails the read.
159
159
 
160
160
  `extractCodes(messageId, pattern?)` and `replyToEmail(messageId, body, html?)` name an INBOX message by its Message-ID, with or without its angle brackets: `<id@example.com>` and `id@example.com` name the same message. An IMAP search matches every Message-ID that holds the text ([RFC 3501 section 6.4.4](https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4)), so each searches for the id in angle brackets and takes only the message whose own id is that one. `extractCodes` answers no codes for an id that is not one message id or that no message has.
161
161
 
@@ -169,6 +169,69 @@ A reply goes out through the transport that sends:
169
169
 
170
170
  `replyToEmail` throws an `Error` that holds none of the id when the id is not one message id of printable US-ASCII with no space, or is too long for one line of `In-Reply-To`, before the original is read; and when no message in INBOX has the id, or the original names no address to reply to, before anything is sent.
171
171
 
172
+ ## The read surface
173
+
174
+ A host that files the mail a mailbox receives, such as a support desk, reads by UID and acts on each message by its UID:
175
+
176
+ ```ts
177
+ import { EmailMailboxError } from '@framers/agentos-ext-channel-email';
178
+
179
+ // The last UID filed and the folder's UIDVALIDITY when it was read, which the host keeps.
180
+ let cursor = { uid: 0, uidValidity: '' };
181
+ const mails = await email.readInbox({ afterUid: cursor.uid, uidValidity: cursor.uidValidity, limit: 50, authServId: 'mx.example.com' });
182
+ for (const mail of mails) {
183
+ const picture = mail.attachments.find((attachment) => attachment.type === 'image/png');
184
+ const bytes = picture === undefined ? undefined : await picture.read(5_242_880);
185
+ // File mail.text and bytes, then remove the mailbox's copy; a copy already gone counts as removed.
186
+ await email.deleteMessage(mail.uid).catch((error: unknown) => {
187
+ if (!(error instanceof EmailMailboxError && error.code === 'gone')) throw error;
188
+ });
189
+ cursor = { uid: mail.uid, uidValidity: mail.uidValidity };
190
+ }
191
+ ```
192
+
193
+ `readInbox({ folder?, limit?, afterUid?, uidValidity?, maxBytes?, authServId? })` reads as `readInbox(folder, limit)` does: the last `limit` messages, newest first. With `afterUid` it gives the first `limit` messages whose UID is greater, oldest first, whatever their flags, so a cursor that keeps the last UID filed moves forward without skipping a message. A FETCH response the server sends unasked during a read, such as the flags another session changed ([RFC 3501 section 7.4.2](https://www.rfc-editor.org/rfc/rfc3501#section-7.4.2)), is no message of the read. It searches `UID <afterUid + 1>:*` and keeps the greater UIDs alone, since a range ending in `*` names the last message even when its UID is not greater ([RFC 3501 section 9](https://www.rfc-editor.org/rfc/rfc3501#section-9)). `afterUid` is a whole number from 0. A UID names the same message only while the folder's UIDVALIDITY holds ([RFC 3501 section 2.3.1.1](https://www.rfc-editor.org/rfc/rfc3501#section-2.3.1.1)), so each message gives the folder's `uidValidity`, and given it beside `afterUid`, a read of a folder whose UIDVALIDITY is another starts from the folder's first message, since its UIDs were given anew. `uidValidity` is a whole number from 1 to 4,294,967,295, or its digits with no leading zero; `null` and the empty string count as not given. `searchEmails` takes `maxBytes` and `authServId` too, and gives the same fields.
194
+
195
+ Each message `readInbox` and `searchEmails` give carries, beside `messageId`, `from`, `to`, `subject`, `date`, `body`, `flags` and `uid`:
196
+
197
+ | Field | What it holds |
198
+ |---|---|
199
+ | `uidValidity` | The folder's UIDVALIDITY when the message was read, in decimal; the empty string when the server gave none. A host that keeps the last UID it read keeps this beside it |
200
+ | `key` | The key the mailbox keeps the mail by, the id the server gives where it gives one: the EMAILID ([RFC 8474](https://www.rfc-editor.org/rfc/rfc8474)) where the server offers OBJECTID, else Gmail's X-GM-MSGID where it offers X-GM-EXT-1. On any other server, the folder's name, a space and the Message-ID; else, for a message with no Message-ID, the folder's name, a space, its UIDVALIDITY, a colon and its UID. A sender writes the Message-ID, so on such a server a message that copies another's Message-ID has its key: a host that keeps one mail a key compares the UID there too. A string to compare and store, not to parse |
201
+ | `recipients` | Every address the To, Cc and Delivered-To fields name, in lowercase, each once, in that order. A local part is in quotes, as [RFC 5322](https://www.rfc-editor.org/rfc/rfc5322#section-3.4.1) writes it, only when it holds white space, a double quote or another special but the dot, such as `"support team"@example.com`, whether the server gave it with its quoting removed or the field kept it |
202
+ | `inReplyTo`, `references` | The first message id In-Reply-To lists, or the empty string, and the ids References lists, each in angle brackets |
203
+ | `autoSubmitted` | The Auto-Submitted keyword ([RFC 3834](https://www.rfc-editor.org/rfc/rfc3834#section-5)) in lowercase, such as `auto-replied`; `no` when the field is absent |
204
+ | `precedence` | The Precedence keyword in lowercase, such as `bulk` or `list`; the empty string when the field is absent |
205
+ | `autoreply` | Whether the message carries an X-Autoreply or an X-Auto-Response-Suppress field |
206
+ | `isReport` | Whether its own type is `multipart/report` ([RFC 6522](https://www.rfc-editor.org/rfc/rfc6522)), as a delivery or read report's is |
207
+ | `authentication` | `dkim`, `spf` and `dmarc`, each result in lowercase, `dmarcFrom`, the domain the DMARC result names, and `alignedDomain`, read from an Authentication-Results field ([RFC 8601](https://www.rfc-editor.org/rfc/rfc8601)) as below; a result the field does not record reads `none` |
208
+ | `text`, `html` | The text parts, and the HTML parts, each decoded from its transfer encoding and charset and joined by a line break in their order, as a picture between two of them leaves them; each at most `maxBytes` bytes of UTF-8, cut on a character's edge. Of a `multipart/alternative`'s forms only the first that holds text, or HTML, counts. The empty string when the message has none |
209
+ | `truncated` | Whether the text or the HTML held more than `maxBytes` gives |
210
+ | `attachments` | Every other part, in the message's order: its `name`, `type`, `bytes` and `read(maxBytes?)` |
211
+
212
+ `authentication` comes from the first Authentication-Results field, which is the receiving host's only when that host writes one at the top of every message it takes in: on a host that writes none, the first field is one the sender wrote. Give `authServId`, the receiving host's authserv-id such as `mx.google.com`, for any decision that trusts the results: only a field that names that host counts, so a field a sender wrote under another name never does. An `authServId` that is not text, or is white space alone, throws an `Error` before any mail is read. Comments are left out, so a semicolon inside one splits nothing. `dkim` reads `pass` when any signature passed. `dmarc` is the receiving host's verdict for the domain `dmarcFrom` names, the domain of the From field that host read ([RFC 7489 section 11.1](https://www.rfc-editor.org/rfc/rfc7489#section-11.1)), so a host that trusts `dmarc` compares `dmarcFrom` with the domain of the message's `from`. `alignedDomain` is the From address's domain when a DKIM signature that passed was made for that domain or for a domain it ends in after a dot, its `header.d`, or the domain of its `header.i` where it names no `header.d`; it reads `none` otherwise, and when the From names no domain, as a group's or an absent From's does. A signature made for a domain below the From domain does not count, though the relaxed alignment of [RFC 7489 section 3.1.1](https://www.rfc-editor.org/rfc/rfc7489#section-3.1.1) counts it: finding a domain's organizational domain needs the public suffix list. A host that reads a message's header another way, such as through Gmail's API, reads the same fields with `readHeaderFields(headerBlock, { from, authServId })`, and the results alone with `parseAuthenticationResults`; given no `from` at all, these align with the domain the DMARC result names.
213
+
214
+ `maxBytes` is a whole number from 1 to 268,435,456, 262,144 when not given. A read fetches each message's envelope, flags, body structure and the header fields above through `BODY.PEEK`, which marks nothing seen, then its text and HTML parts by UID, each up to the bytes that can decode to what `maxBytes` leaves once the parts before it are read, so a large part is never fetched whole and a part past a filled bound is not fetched. A quoted-printable part written with runs of soft line breaks, which no encoder that keeps to the rules writes, can hold more than four bytes for each byte it decodes to; it reads as truncated, since a read fetches no more of a part than about four times the bound. `body` is `text`, or, for a message with no text part, its HTML with each tag written as a space.
215
+
216
+ An attachment is every part that is not the message's text or HTML: a part marked an attachment or named as a file, a part of any other type, and a message attached whole (`message/rfc822`), whose own parts are not listed. A text or HTML form a `multipart/alternative` does not take is neither. `bytes` is its size as the server holds it, in its transfer encoding, never less than its decoded bytes: about four thirds of them for base64. No read fetches an attachment's bytes. `read(maxBytes?)` fetches them by the message's UID over a connection of its own and resolves with the decoded bytes. It rejects with `EmailMailboxError` `too_large`, having fetched at most the bytes that could decode to the bound, when they pass `maxBytes`, a whole number from 1 to 268,435,456, 26,214,400 when not given, or when the part holds more bytes in its transfer encoding than decode to the bound from an encoder that keeps to its rules: twice the bound for base64, four times for quoted-printable. Since `bytes` counts the transfer encoding, a host that bounds an attachment's decoded size passes that bound to `read` and catches `too_large`. It rejects with `gone` when the folder no longer holds the message or its UIDVALIDITY changed since the read, and with `refused` when the server will not open the folder, as when it was deleted or renamed since the read.
217
+
218
+ ### Changes by UID
219
+
220
+ Each change acts on one message by its UID in `folder`, INBOX when not given, over a connection of its own:
221
+
222
+ - `moveTo(uid, destination, { folder? })` moves it with `UID MOVE` ([RFC 6851](https://www.rfc-editor.org/rfc/rfc6851)) where the server offers MOVE, else with `UID COPY`, the `\Deleted` flag and `UID EXPUNGE`; the original is touched only once the copy is made.
223
+ - `addLabel(uid, label, { folder? })` labels it through Gmail's `X-GM-LABELS` where the server offers X-GM-EXT-1, else copies it into the folder named `label`; the message stays where it is.
224
+ - `setFlags(uid, flags, { folder? })` replaces its flags with `flags`, such as `['\\Seen', '$Handled']`, with `UID STORE ... FLAGS`; an empty list clears them. A keyword goes in the spelling the folder's PERMANENTFLAGS give it, matched ignoring case, as IMAP reads a flag's name.
225
+ - `deleteMessage(uid, { folder? })` sets the `\Deleted` flag and expunges that UID alone with `UID EXPUNGE` ([RFC 4315](https://www.rfc-editor.org/rfc/rfc4315)), which leaves every other message marked deleted where it is.
226
+
227
+ A `UID STORE`, `UID COPY` or `UID MOVE` of a UID no message has succeeds having done nothing ([RFC 3501 section 6.4.8](https://www.rfc-editor.org/rfc/rfc3501#section-6.4.8), which [RFC 6851 section 3.2](https://www.rfc-editor.org/rfc/rfc6851#section-3.2) extends to `UID MOVE`), so each change first searches for the UID, and after a store, a label or a copy whose answer cannot tell that it acted on the message, searches again. A change rejects with `EmailMailboxError`, whose `code` says why and whose message holds no UID, folder or address:
228
+
229
+ - `gone`: the folder holds no message with that UID, which comes before any other answer; or another session expunged it between the search and the change, as the COPYUID a server with UIDPLUS sends ([RFC 4315](https://www.rfc-editor.org/rfc/rfc4315#section-3)), or the search after the change, tells. A caller that wants the message gone, as after a delete that ran before a crash, counts it as done. A `UID MOVE` that another session's expunge overtakes, on a server that sends no COPYUID, resolves as done, the server having answered OK.
230
+ - `unsupported`, before anything changes: `deleteMessage` on a server without UIDPLUS, and `moveTo` on one with neither MOVE nor UIDPLUS, where `EXPUNGE` would remove every message marked deleted.
231
+ - `refused`: the server refused a command, as it does a destination or a label folder that does not exist, or would not open `folder`; imapflow's own error for that carries the server's text and the command, which name the folder, and the change's error carries neither. `setFlags` also refuses, before anything is stored, a flag the folder's PERMANENTFLAGS do not keep in any spelling, since imapflow would leave it out and a set left with no flag clears every flag.
232
+
233
+ A `uid` that is not a whole number from 1 to 4,294,967,295, a folder, destination or label that is empty or holds a control character, flags that are not a list of flag names, and `\Recent`, which the server alone sets, throw an `Error` before connecting.
234
+
172
235
  ## In an agent
173
236
 
174
237
  `createExtensionPack(context)` returns five tools, `emailSend`, `emailRead`, `emailSearch`, `emailExtractCodes` and `emailReply`, and the messaging channel `emailChannel`. Its options (`smtpHost`, `smtpUser`, `smtpPassword`, `smtpPort`, `smtpSecure`, `imapHost`, `imapUser`, `imapPassword`, `imapPort`, `imapSecure`, `resendApiKey`, `resendBaseUrl`, `from`) take precedence over the secrets and the environment variables named above. `manifest.json` lists the SMTP secrets as required, and the IMAP secrets and `email.resendApiKey` as optional.
@@ -179,14 +242,16 @@ They send each attachment as they checked it: a copy of its own `filename`, `con
179
242
 
180
243
  The subject, the bodies and the message id a reply names must be text. Through SMTP, nodemailer reads the file or fetches the address an object body names into the message, and writes an object subject or message id marked `prepared` into the headers as it is. So `sendEmail` and `replyToEmail`, whoever calls them, throw a `TypeError` that names the field, never its value, before anything is sent on either transport: `subject`, `body` and the reply's `messageId` must be strings, and `html` a string when given (`null` leaves the HTML part out). `emailSend` and `emailReply` answer it as `{ success: false, error }`, and the adapter throws it. A reply whose message id is not one message id, or names no message in INBOX, is answered and thrown the same way, with the `Error` [reading mail](#reading-mail) describes.
181
244
 
182
- `emailRead`, `emailSearch` and `emailExtractCodes` answer a `limit`, a `since` or a `pattern` that [reading mail](#reading-mail) refuses as `{ success: false, error }`, before any mail is read, and a pattern that runs past its 250 ms the same way. `emailSearch` gives the last 20 messages it finds.
245
+ `emailRead`, `emailSearch` and `emailExtractCodes` answer a `limit`, a `since` or a `pattern` that [reading mail](#reading-mail) refuses as `{ success: false, error }`, before any mail is read, and a pattern that runs past its 250 ms the same way; `emailRead` answers a `folder` that is not a name the same way. `emailSearch` gives the last 20 messages it finds. `emailRead` and `emailSearch` answer plain data a model reads: each message without `html` and `text`, its text once as `body`, and each attachment's `name`, `type` and `bytes` without its `read()`.
183
246
 
184
247
  ## API
185
248
 
186
249
  | Export | What it is |
187
250
  |---|---|
188
251
  | `createExtensionPack(context)` | The pack's factory: the five tools and the channel adapter |
189
- | `EmailService` | Sends through SMTP or Resend's HTTPS API; reads, searches, extracts codes and replies through IMAP |
252
+ | `EmailService` | Sends through SMTP or Resend's HTTPS API; reads, searches, extracts codes, replies, moves, labels, flags and deletes through IMAP |
253
+ | `EmailMailboxError` | A change by UID, or an attachment's read, that failed, with its `code`: `gone`, `refused`, `unsupported` or `too_large` |
254
+ | `readHeaderFields(headerBlock, options?)`, `parseAuthenticationResults(headerBlock, options?)` | The fields a header block gives a read, and its Authentication-Results alone |
190
255
  | `ResendTransport` | Resend's send call, which `EmailService` uses when its configuration names `resend` |
191
256
  | `EmailApiError`, `retryWaitSeconds` | A refusal from Resend, and the wait before its retry (`null` for none) |
192
257
  | `EmailChannelAdapter` | The messaging-channel adapter |
@@ -195,7 +260,7 @@ The subject, the bodies and the message id a reply names must be text. Through S
195
260
  | `SqlRecipientCapability`, `recipientCapabilityDdl(prefix)` | Recipient capabilities on a SQL database, and their table's DDL |
196
261
  | `emailPreferencesDdl(prefix)` | Both tables' DDL |
197
262
  | `DEFAULT_SUPPRESSION_SCOPE`, `SUPPRESSION_REASONS` | The scope of a host that keeps one list (`mail`), and the reasons an address is suppressed, frozen |
198
- | `EmailConfig`, `SendEmailOptions`, `EmailMessage`, `SearchEmailOptions`, `ResendOptions`, `OutgoingEmail`, `OutgoingAttachment`, `EmailChannelOptions`, `ExtensionContext`, `ExtensionPack` | The types of the configuration, the messages, the options and the pack |
263
+ | `EmailConfig`, `SendEmailOptions`, `EmailMessage`, `EmailAttachment`, `SearchEmailOptions`, `ReadOptions`, `ReadInboxOptions`, `MailboxOptions`, `EmailMailboxErrorCode`, `EmailAuthentication`, `HeaderFieldOptions`, `HeaderFields`, `ResendOptions`, `OutgoingEmail`, `OutgoingAttachment`, `EmailChannelOptions`, `ExtensionContext`, `ExtensionPack` | The types of the configuration, the messages, the options and the pack |
199
264
  | `SuppressionStore`, `SuppressionReason`, `RecipientCapability`, `RecipientGrant`, `RecipientCapabilityIssueOptions`, `SqlRecipientCapabilityOptions`, `EmailPreferencesDb` | The stores' interfaces, their answers and options, and what they need of a database |
200
265
 
201
266
  ## License
@@ -3,8 +3,13 @@
3
3
  *
4
4
  * Sends through nodemailer (SMTP) or Resend's HTTPS API, and reads,
5
5
  * searches, and fetches the messages it replies to through imapflow (IMAP).
6
+ * A read gives each message's recipients, threading and authentication
7
+ * headers, its text and HTML parts under a bound, and its attachments, whose
8
+ * bytes are fetched only when asked; one message is moved, labelled, flagged
9
+ * or deleted by its UID.
6
10
  */
7
11
  import type { ResendOptions } from './ResendTransport.js';
12
+ import type { EmailAuthentication } from './headers.js';
8
13
  /** The accounts the service uses: an SMTP account or Resend's HTTPS API to send, and IMAP to read. */
9
14
  export interface EmailConfig {
10
15
  /** The SMTP account that sends, and reads when `imap` is not given. Required unless `resend` is given. */
@@ -54,6 +59,29 @@ export interface SendEmailOptions {
54
59
  */
55
60
  headers?: Record<string, string>;
56
61
  }
62
+ /** An attachment of a message a read gave: what its body structure says of it, and a read of its bytes. */
63
+ export interface EmailAttachment {
64
+ /** The file name its Content-Disposition, or else its Content-Type, gives; the empty string when neither does. */
65
+ name: string;
66
+ /** Its media type in lowercase, such as `image/png`; `message/rfc822` for a message attached whole. */
67
+ type: string;
68
+ /**
69
+ * Its size as the server holds it, in its transfer encoding (RFC 3501 section 7.4.2), never less than its decoded
70
+ * bytes: about four thirds of them for base64.
71
+ */
72
+ bytes: number;
73
+ /**
74
+ * Fetches the attachment's bytes, decoded, by the message's UID in the folder it was read from, over a connection of
75
+ * its own: a read fetches none of them. Rejects with `EmailMailboxError` `too_large`, having fetched at most the bytes
76
+ * that could decode to the bound, when the decoded bytes pass `maxBytes`, a whole number from 1 to 268,435,456
77
+ * (26,214,400 when not given or `null`), or when its bytes in their transfer encoding pass the most that decode to the
78
+ * bound from an encoder that keeps to its rules (twice it for base64, four times for quoted-printable), as a
79
+ * quoted-printable part written with runs of soft line breaks can; with `gone` when the folder holds no message with
80
+ * that UID, or its UIDVALIDITY changed since the read; with `refused` when the server will not open the folder, as
81
+ * when it was deleted or renamed since the read; and with an `Error` before connecting for any other `maxBytes`.
82
+ */
83
+ read(maxBytes?: number): Promise<Buffer>;
84
+ }
57
85
  /** A message as `readInbox` and `searchEmails` give it. */
58
86
  export interface EmailMessage {
59
87
  /** Its Message-ID field as the server's envelope gives it, or the empty string when it has none. */
@@ -65,13 +93,135 @@ export interface EmailMessage {
65
93
  subject: string;
66
94
  /** Its Date field in ISO 8601, or the empty string when it has none or the field does not read as a date. */
67
95
  date: string;
96
+ /** `text`, or, for a message with no text part, its HTML with each tag written as a space and white space run together. */
68
97
  body: string;
69
- html?: string;
98
+ /**
99
+ * Its HTML parts as written, decoded, joined by a line break in their order, at most `maxBytes` bytes of UTF-8; the
100
+ * empty string when it has none. Of a `multipart/alternative`'s forms only the first that holds HTML counts.
101
+ */
102
+ html: string;
70
103
  flags: string[];
71
104
  uid: number;
105
+ /**
106
+ * The folder's UIDVALIDITY when the message was read, in decimal; the empty string when the server gave none. A UID
107
+ * names the same message only while it holds (RFC 3501 section 2.3.1.1), so a host that keeps the last UID it read
108
+ * keeps this beside it and gives both to its next read.
109
+ */
110
+ uidValidity: string;
111
+ /**
112
+ * The key the mailbox keeps the mail by, the id the server gives where it gives one: the EMAILID (RFC 8474) where the
113
+ * server offers OBJECTID, which imapflow fetches in place of Gmail's id, else Gmail's X-GM-MSGID where it offers
114
+ * X-GM-EXT-1. On any other server, the folder's name, a space and the Message-ID in angle brackets, or, for a message
115
+ * with no Message-ID, the folder's name, a space, its UIDVALIDITY, a colon and its UID. A sender writes the Message-ID,
116
+ * so on such a server a message that copies another's Message-ID has its key: a host that keeps one mail a key compares
117
+ * the UID beside it there. A string to compare and store, not to parse.
118
+ */
119
+ key: string;
120
+ /**
121
+ * Every address its To, Cc and Delivered-To fields name, in lowercase, each once, in that order. A local part that
122
+ * holds white space, a double quote or another special but the dot is given in quotes, as RFC 5322 writes it, such as
123
+ * `"support team"@example.com`, whether the server gave it with its quoting removed or the field kept it.
124
+ */
125
+ recipients: string[];
126
+ /** The first message id its In-Reply-To lists, in angle brackets, or the empty string. */
127
+ inReplyTo: string;
128
+ /** The message ids its References lists, each in angle brackets, in its order. */
129
+ references: string[];
130
+ /** The keyword of its Auto-Submitted field (RFC 3834), in lowercase, such as `auto-replied`; `no` when it has none. */
131
+ autoSubmitted: string;
132
+ /** The keyword of its Precedence field, in lowercase, such as `bulk`; the empty string when it has none. */
133
+ precedence: string;
134
+ /** Whether it carries an X-Autoreply or an X-Auto-Response-Suppress field. */
135
+ autoreply: boolean;
136
+ /** Whether its own type is `multipart/report` (RFC 6522), as a delivery or read report's is. */
137
+ isReport: boolean;
138
+ /**
139
+ * The first Authentication-Results field's results, parsed, or, given `authServId`, those of the first field that
140
+ * names that host; a result the field does not record reads `none`. The first field is the receiving host's only when
141
+ * that host writes one at the top of every message it takes in: on a host that writes none, it is one the sender wrote,
142
+ * so a decision that trusts these results reads them with `authServId`.
143
+ */
144
+ authentication: EmailAuthentication;
145
+ /**
146
+ * Its text parts, decoded, joined by a line break in their order, as a picture between two of them leaves them, at
147
+ * most `maxBytes` bytes of UTF-8 cut on a character's edge; the empty string when it has none. Of a
148
+ * `multipart/alternative`'s forms only the first that holds text counts.
149
+ */
150
+ text: string;
151
+ /**
152
+ * Whether its text or its HTML held more than `maxBytes` gives, or a part more bytes in its transfer encoding than the
153
+ * most that decode to the bound from an encoder that keeps to its rules, as a quoted-printable part written with runs
154
+ * of soft line breaks can: a read never fetches more of a part than that.
155
+ */
156
+ truncated: boolean;
157
+ /**
158
+ * Every part that is neither its text nor its HTML, in the message's order: a part marked an attachment or named as a
159
+ * file, a part of any other type, and a message attached whole.
160
+ */
161
+ attachments: EmailAttachment[];
162
+ }
163
+ /** How a read bounds and reads each message, which `readInbox` and `searchEmails` take. */
164
+ export interface ReadOptions {
165
+ /**
166
+ * The most bytes of each message's text and of its HTML given, in UTF-8: a whole number from 1 to 268,435,456, 262,144
167
+ * when not given or `null`. A part is fetched up to the bytes that can decode to what the bound leaves, so a large one
168
+ * is never read whole. Any other value throws an `Error` before any mail is read.
169
+ */
170
+ maxBytes?: number;
171
+ /**
172
+ * The authserv-id of the host that received the mail, such as `mx.google.com`, compared ignoring case: only an
173
+ * Authentication-Results field that names it counts, so a field a sender wrote under another name never does. Without
174
+ * it the first field counts, which is the receiving host's only when that host writes one at the top of every message
175
+ * it takes in; on a host that writes none, the first field is one the sender wrote. Give it for any decision that
176
+ * trusts the results. A value that is not text, or that is white space alone, throws an `Error` before any mail is
177
+ * read; `null` and the empty string count as not given.
178
+ */
179
+ authServId?: string;
180
+ }
181
+ /** What `readInbox` reads, given as one object. */
182
+ export interface ReadInboxOptions extends ReadOptions {
183
+ /** The folder read; INBOX when not given or `null`. */
184
+ folder?: string;
185
+ /** The most messages given, a whole number of at least 1; 20 when not given or `null`. */
186
+ limit?: number;
187
+ /**
188
+ * A UID to read after: the first `limit` messages whose UID is greater, whatever their flags, oldest first, selected by
189
+ * `UID SEARCH UID <afterUid + 1>:*`. A whole number from 0, 0 for every message. Without it the read gives the last
190
+ * `limit` messages, newest first. Any other value throws an `Error` before any mail is read.
191
+ */
192
+ afterUid?: number;
193
+ /**
194
+ * The folder's UIDVALIDITY when `afterUid` was read, as each message gives it: a whole number from 1 to 4,294,967,295,
195
+ * or its digits with no leading zero. When the folder's UIDVALIDITY is another, its UIDs were given anew (RFC 3501
196
+ * section 2.3.1.1), so `afterUid` names nothing of it and the read starts from the folder's first message, as
197
+ * `afterUid` 0 does. It counts only with `afterUid`; `null` and the empty string count as not given; any other value
198
+ * throws an `Error` before any mail is read.
199
+ */
200
+ uidValidity?: string | number;
201
+ }
202
+ /** The folder a change to one message acts in. */
203
+ export interface MailboxOptions {
204
+ /** The folder that holds the message; INBOX when not given or `null`. */
205
+ folder?: string;
206
+ }
207
+ /** What made a change to a message, or a read of an attachment, fail. */
208
+ export type EmailMailboxErrorCode = 'gone' | 'refused' | 'unsupported' | 'too_large';
209
+ /**
210
+ * A change to one message, or a read of an attachment, that failed: `gone` when the folder holds no message with the UID
211
+ * named, which a caller that wants the message gone counts as done; `refused` when the server refused the command or
212
+ * would not open the folder; `unsupported` when the server lacks the extension the change needs; `too_large` when an
213
+ * attachment passes the bound a read named. Its message never holds a UID, a folder's name or an address, and it
214
+ * carries nothing the server said.
215
+ */
216
+ export declare class EmailMailboxError extends Error {
217
+ /** The type's own name. */
218
+ name: string;
219
+ /** Why it failed. */
220
+ readonly code: EmailMailboxErrorCode;
221
+ constructor(code: EmailMailboxErrorCode, message: string);
72
222
  }
73
223
  /** What `searchEmails` searches for. */
74
- export interface SearchEmailOptions {
224
+ export interface SearchEmailOptions extends ReadOptions {
75
225
  /** Text the subject, the body or the From holds, ignoring case; every message when empty. */
76
226
  query: string;
77
227
  /** The folder searched; INBOX when not given. */
@@ -136,25 +286,99 @@ export declare class EmailService {
136
286
  messageId: string;
137
287
  }>;
138
288
  /**
139
- * The last `limit` messages of `folder`, newest first: INBOX and 20 when they are not given, and 20 when `limit` is
140
- * `null`. `limit` must be a whole number of at least 1: any other value throws an `Error` before any mail is read,
141
- * where the fetch would name a message past the last one, which a server answers BAD. A message whose Date field does
142
- * not read as a date is given with an empty `date`.
289
+ * Reads `folder`'s messages. Given a folder and a limit, or neither, it gives the last `limit` messages of `folder`,
290
+ * newest first: INBOX and 20 when they are not given, and 20 when `limit` is `null`. Given one object
291
+ * (`ReadInboxOptions`), it reads the same way, or, with `afterUid`, gives the first `limit` messages whose UID is
292
+ * greater, oldest first, whatever their flags: it searches `UID <afterUid + 1>:*` and keeps only the greater UIDs,
293
+ * since a range ending in `*` names the last message even when its UID is not greater (RFC 3501 section 9). Given
294
+ * `uidValidity` with `afterUid`, a folder whose UIDVALIDITY is another is read from its first message, since its UIDs
295
+ * were given anew and `afterUid` names nothing of it.
296
+ *
297
+ * `limit` must be a whole number of at least 1, `afterUid` a whole number from 0, `uidValidity` a UIDVALIDITY,
298
+ * `maxBytes` a whole number from 1 to 268,435,456, `folder` a name with no control character and `authServId` text that
299
+ * is not white space alone: any other value throws an `Error` before any mail is read, where the fetch would name a
300
+ * message past the last one, which a server answers BAD. A server that refuses the search after a UID rejects the read
301
+ * with `EmailMailboxError` `refused`. A message whose Date field does not read as a date is given with an empty `date`.
302
+ *
303
+ * Each message is fetched with its envelope, flags, body structure and the header fields its recipients, threading
304
+ * and authentication come from, then its text and HTML parts by UID, each up to the bytes that can decode to what
305
+ * `maxBytes` leaves, in one fetch for the messages whose parts share their sections and windows. No fetch marks a
306
+ * message seen, and no attachment's bytes are fetched until its `read()` is called. A FETCH response the server sends
307
+ * unasked during a fetch, such as the flags another session changed, is no message of the read.
143
308
  */
144
- readInbox(folder?: string, limit?: number): Promise<EmailMessage[]>;
309
+ readInbox(folder?: string | ReadInboxOptions, limit?: number): Promise<EmailMessage[]>;
145
310
  /**
146
311
  * The messages of `folder` (INBOX unless given) whose subject, body or From holds `query`, ignoring case, received on
147
312
  * or after `since` when it is given: the last `limit` (20 unless given) the search finds, newest first. The search
148
- * answers UIDs, and the messages are fetched by those UIDs, which need not follow the messages' sequence numbers.
313
+ * answers UIDs, and the messages are fetched by those UIDs, which need not follow the messages' sequence numbers. Each
314
+ * message carries what `readInbox` gives, its parts bounded by `maxBytes` and its Authentication-Results read as
315
+ * `authServId` says.
149
316
  *
150
317
  * `limit` must be a whole number of at least 1, and `since` an ISO 8601 date, such as `2026-10-15`, alone or followed
151
318
  * by a time and an offset, which are checked and then disregarded: the search starts from the day the date names, as
152
319
  * written, on every server. Either counts as not given when `null`, and `since` when empty. Any other value of either,
153
320
  * a day its month lacks such as `2026-02-30` among them, throws an `Error` before the search: imapflow leaves a date it
154
321
  * cannot read out of the search, which would answer mail of every date, and `Date` rolls a day past its month's end
155
- * over into the next month. A message whose Date field does not read as a date is given with an empty `date`.
322
+ * over into the next month. `maxBytes` and `authServId` are checked as `readInbox` checks them. A message whose Date
323
+ * field does not read as a date is given with an empty `date`.
156
324
  */
157
325
  searchEmails(options: SearchEmailOptions): Promise<EmailMessage[]>;
326
+ /**
327
+ * Moves the message whose UID is `uid` in `options.folder` (INBOX unless given) to `destination`: by `UID MOVE`
328
+ * (RFC 6851) where the server offers MOVE, else by `UID COPY`, the `\Deleted` flag and `UID EXPUNGE` (RFC 4315), which
329
+ * expunges no other message marked deleted. The copy must succeed before the original is touched.
330
+ *
331
+ * Rejects with `EmailMailboxError`: `gone` when the folder holds no message with that UID, which comes before any other
332
+ * answer, and when another session expunges the message before the move, as the COPYUID a server with UIDPLUS sends
333
+ * tells, or as a search after a copy that named none tells; `unsupported`, before anything changes, when the server
334
+ * offers neither MOVE nor UIDPLUS, where `EXPUNGE` would remove every message marked deleted; `refused` when the server
335
+ * refuses a command, as it does a destination that does not exist, or will not open the folder. A move another
336
+ * session's expunge overtakes on a server that sends no COPYUID resolves as done, the server having answered OK. A
337
+ * `uid` that is not a whole number from 1 to 4,294,967,295, or a folder or destination that is not a name, throws an
338
+ * `Error` before connecting.
339
+ */
340
+ moveTo(uid: number, destination: string, options?: MailboxOptions | null): Promise<void>;
341
+ /**
342
+ * Labels the message whose UID is `uid` in `options.folder` (INBOX unless given) with `label`: through Gmail's
343
+ * `X-GM-LABELS` where the server offers X-GM-EXT-1, else by `UID COPY` into the folder named `label`. The message
344
+ * stays where it is.
345
+ *
346
+ * Rejects with `EmailMailboxError` `gone` when the folder holds no message with that UID, and when another session
347
+ * expunges the message between the search for it and the change, which then did nothing (RFC 3501 section 6.4.8), as
348
+ * the COPYUID a server with UIDPLUS sends, or a search after the change, tells; and `refused` when the server refuses
349
+ * the command, as it does a copy into a folder that does not exist, or will not open the folder. A `uid` that is not
350
+ * a whole number from 1 to 4,294,967,295, or a folder or label that is not a name, throws an `Error` before
351
+ * connecting.
352
+ */
353
+ addLabel(uid: number, label: string, options?: MailboxOptions | null): Promise<void>;
354
+ /**
355
+ * Replaces the flags of the message whose UID is `uid` in `options.folder` (INBOX unless given) with `flags`, by
356
+ * `UID STORE ... FLAGS`; an empty list clears them. Each flag is a flag name such as `\Seen` or `$Handled`; `\Recent`,
357
+ * which the server alone sets, is refused. A keyword goes in the spelling the folder's PERMANENTFLAGS give it, matched
358
+ * ignoring case, as IMAP reads a flag's name.
359
+ *
360
+ * Rejects with `EmailMailboxError` `gone` when the folder holds no message with that UID, which comes before any other
361
+ * answer, and when another session expunges the message between the search for it and the STORE, as a search after the
362
+ * STORE tells; `refused`, before anything is stored, when the folder's PERMANENTFLAGS keep neither `\*` nor every flag
363
+ * named, in any spelling, since imapflow would leave such a flag out and a set left with none clears every flag; and
364
+ * `refused` when the server refuses the STORE or will not open the folder. A `uid` that is not a whole number from 1
365
+ * to 4,294,967,295, flags that are not a list of flag names, or a folder that is not a name, throw an `Error` before
366
+ * connecting.
367
+ */
368
+ setFlags(uid: number, flags: string[], options?: MailboxOptions | null): Promise<void>;
369
+ /**
370
+ * Deletes the message whose UID is `uid` in `options.folder` (INBOX unless given): the `\Deleted` flag, then
371
+ * `UID EXPUNGE` of that UID alone (RFC 4315), so no other message marked deleted is expunged. What an expunge does with
372
+ * the message beyond the folder is the server's to decide.
373
+ *
374
+ * Rejects with `EmailMailboxError`: `gone` when the folder holds no message with that UID, which a caller that wants
375
+ * the message gone counts as done, so a second call rejects with it, and which comes before any other answer;
376
+ * `unsupported`, before anything changes, when the server does not offer UIDPLUS, where `EXPUNGE` would remove every
377
+ * message marked deleted; `refused` when the server refuses the STORE or the expunge, or will not open the folder. A
378
+ * message another session expunges during the delete is gone either way, and the delete resolves. A `uid` that is not
379
+ * a whole number from 1 to 4,294,967,295, or a folder that is not a name, throws an `Error` before connecting.
380
+ */
381
+ deleteMessage(uid: number, options?: MailboxOptions | null): Promise<void>;
158
382
  /**
159
383
  * The codes in the INBOX message whose Message-ID is `messageId`, given with or without its angle brackets: the
160
384
  * matches of `pattern` when it is given, else runs of four to eight digits, then the codes that phrases such as "code
@@ -197,6 +421,34 @@ export declare class EmailService {
197
421
  messageId: string;
198
422
  }>;
199
423
  private getImapClient;
424
+ /**
425
+ * The messages a set names in the selected folder, as a read gives them: first their envelopes, flags, body
426
+ * structures and read header fields, then their text and HTML parts by UID under the bound.
427
+ */
428
+ private fetchMessages;
429
+ /**
430
+ * The text and HTML bytes of each message by UID, each part up to one byte past its window: one fetch for the messages
431
+ * whose parts share their sections and windows, and no fetch of a part whose window is 0, since the parts before it
432
+ * fill the bound.
433
+ */
434
+ private fetchTextParts;
435
+ /**
436
+ * An attachment's bytes, decoded, fetched by the message's UID in `folder` over a connection of its own, up to one byte
437
+ * past the bytes that can decode to the bound: an attachment's `read()`.
438
+ */
439
+ private readAttachment;
440
+ /**
441
+ * Runs `work` on the message whose UID is `uid` in `folder`, over a connection of its own, once a search finds the UID:
442
+ * a UID STORE, COPY or MOVE of a UID no message has succeeds having done nothing (RFC 3501 section 6.4.8, which RFC 6851
443
+ * section 3.2 extends to UID MOVE), so no message with it rejects with `gone`, before `work` checks what the server
444
+ * offers, since a caller that wants the message gone counts gone as done.
445
+ */
446
+ private onMessage;
447
+ /**
448
+ * Marks one message deleted and expunges it by UID, on a server the caller found to offer UIDPLUS. imapflow's delete
449
+ * goes on to the expunge whatever its STORE answered, so the STORE is sent and checked first.
450
+ */
451
+ private expungeOne;
200
452
  private extractPlainText;
201
453
  private extractHtmlBody;
202
454
  private decodeQuotedPrintable;
@@ -1 +1 @@
1
- {"version":3,"file":"EmailService.d.ts","sourceRoot":"","sources":["../src/EmailService.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAS1D,sGAAsG;AACtG,MAAM,WAAW,WAAW;IAC1B,0GAA0G;IAC1G,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,wFAAwF;IACxF,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,iIAAiI;IACjI,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,oGAAoG;IACpG,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,6GAA6G;IAC7G,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wCAAwC;AACxC,MAAM,WAAW,kBAAkB;IACjC,6FAA6F;IAC7F,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8CD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,WAAW;IAI/B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B3B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAS/B,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;OAGG;IACH,IAAI,SAAS,IAAI,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGxC;IAID;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAiD1E;;;;;OAKG;IACG,SAAS,CAAC,MAAM,GAAE,MAAgB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAoClF;;;;;;;;;;;OAWG;IACG,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IA2DxE;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA0DxG;;;;;;;;;;;;;;;;;;;OAmBG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;YAoEnB,aAAa;IAuB3B,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,eAAe;IA4BvB,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,cAAc;CAGvB"}
1
+ {"version":3,"file":"EmailService.d.ts","sourceRoot":"","sources":["../src/EmailService.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAK1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAQxD,sGAAsG;AACtG,MAAM,WAAW,WAAW;IAC1B,0GAA0G;IAC1G,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF,wFAAwF;IACxF,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,iIAAiI;IACjI,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,2GAA2G;AAC3G,MAAM,WAAW,eAAe;IAC9B,kHAAkH;IAClH,IAAI,EAAE,MAAM,CAAC;IACb,uGAAuG;IACvG,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1C;AAED,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,oGAAoG;IACpG,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,6GAA6G;IAC7G,IAAI,EAAE,MAAM,CAAC;IACb,2HAA2H;IAC3H,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uHAAuH;IACvH,aAAa,EAAE,MAAM,CAAC;IACtB,4GAA4G;IAC5G,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,SAAS,EAAE,OAAO,CAAC;IACnB,gGAAgG;IAChG,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,cAAc,EAAE,mBAAmB,CAAC;IACpC;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,SAAS,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,mDAAmD;AACnD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,CAAC;AAErF;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,2BAA2B;IAClB,IAAI,SAAuB;IAEpC,qBAAqB;IACrB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;gBAEzB,IAAI,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM;CAIzD;AAED,wCAAwC;AACxC,MAAM,WAAW,kBAAmB,SAAQ,WAAW;IACrD,6FAA6F;IAC7F,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA4SD,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,WAAW;IAI/B;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IA8B3B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAS/B,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;;OAGG;IACH,IAAI,SAAS,IAAI,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGxC;IAID;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAiD1E;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,gBAAgB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAmD5F;;;;;;;;;;;;;;OAcG;IACG,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAiDxE;;;;;;;;;;;;;OAaG;IACG,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA4B9F;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB1F;;;;;;;;;;;;;OAaG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5F;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhF;;;;;;;;;;;;;OAaG;IACG,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA0DxG;;;;;;;;;;;;;;;;;;;OAmBG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;YAoEnB,aAAa;IAuB3B;;;OAGG;YACW,aAAa;IA6B3B;;;;OAIG;YACW,cAAc;IA0B5B;;;OAGG;YACW,cAAc;IAqC5B;;;;;OAKG;YACW,SAAS;IAevB;;;OAGG;YACW,UAAU;IASxB,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,eAAe;IA4BvB,OAAO,CAAC,qBAAqB;IAM7B,OAAO,CAAC,cAAc;CAGvB"}