@framers/agentos-ext-channel-email 0.4.1 → 0.4.2
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 +31 -8
- package/dist/EmailChannelAdapter.d.ts +3 -1
- package/dist/EmailChannelAdapter.d.ts.map +1 -1
- package/dist/EmailChannelAdapter.js +3 -1
- package/dist/EmailChannelAdapter.js.map +1 -1
- package/dist/EmailService.d.ts +48 -16
- package/dist/EmailService.d.ts.map +1 -1
- package/dist/EmailService.js +130 -67
- package/dist/EmailService.js.map +1 -1
- package/dist/ResendTransport.d.ts +5 -2
- package/dist/ResendTransport.d.ts.map +1 -1
- package/dist/ResendTransport.js.map +1 -1
- package/dist/messageHeaders.d.ts +50 -10
- package/dist/messageHeaders.d.ts.map +1 -1
- package/dist/messageHeaders.js +175 -15
- package/dist/messageHeaders.js.map +1 -1
- package/dist/messageText.d.ts +9 -1
- package/dist/messageText.d.ts.map +1 -1
- package/dist/messageText.js +17 -1
- package/dist/messageText.js.map +1 -1
- package/dist/preferences/RecipientCapability.d.ts +4 -4
- package/dist/preferences/SuppressionStore.d.ts +2 -2
- package/dist/preferences/database.d.ts +2 -2
- package/dist/preferences/database.d.ts.map +1 -1
- package/dist/preferences/database.js +11 -4
- package/dist/preferences/database.js.map +1 -1
- package/dist/tools/extractCodes.js +1 -1
- package/dist/tools/extractCodes.js.map +1 -1
- package/dist/tools/reply.d.ts +2 -1
- package/dist/tools/reply.d.ts.map +1 -1
- package/dist/tools/reply.js +3 -2
- package/dist/tools/reply.js.map +1 -1
- package/manifest.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,10 +79,20 @@ await email.sendEmail({
|
|
|
79
79
|
});
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
Through Resend the fields go in the request's `headers` ([custom headers](https://resend.com/docs/dashboard/emails/custom-headers)); through SMTP nodemailer writes each into the message
|
|
82
|
+
Through Resend the fields go in the request's `headers` ([custom headers](https://resend.com/docs/dashboard/emails/custom-headers)); through SMTP nodemailer writes each into the message as given, folded at its white space. Both transports get the same text, so `headers` takes only what both carry alike:
|
|
83
|
+
|
|
84
|
+
- `headers` is a plain object of names and values, made in this realm or in another, such as a `vm` context. A list, a `Map` or text is refused.
|
|
85
|
+
- A name is printable US-ASCII with no space or colon, the field name of RFC 5322, given once in any case.
|
|
86
|
+
- A value is a string of printable US-ASCII words, with one space or tab between each two and none at either end. An empty or blank value is refused: nodemailer would leave the field out, and Resend would send it empty. So is a character outside US-ASCII, which nodemailer would write as an RFC 2047 encoded word that a structured field such as `List-Unsubscribe` cannot hold; percent-encode a link instead. A line break or any other control character is refused, so no field can end and start another, and so is white space at either end or two white-space characters in a row, which nodemailer can fold into a line of white space alone: obsolete syntax ([RFC 5322 section 4.2](https://www.rfc-editor.org/rfc/rfc5322#section-4.2)) that a message must not be written with.
|
|
87
|
+
- No word may be so long that the name, a colon, a space and the word pass 998 characters, the longest line [RFC 5322 section 2.1.1](https://www.rfc-editor.org/rfc/rfc5322#section-2.1.1) allows: a field folds only at its white space, so a word stays on one line. A `List-Unsubscribe` link holds at most 980 characters.
|
|
88
|
+
- `In-Reply-To` and `References` list message ids, each in angle brackets, such as `<id@example.com>`.
|
|
89
|
+
|
|
90
|
+
Each value is read once, and the fields are sent as read. A value with a double quote, such as a `List-Id` of `"Weekly summary" <weekly.example.com>`, arrives as written through both transports. Two differences stay, and neither changes what a field means: nodemailer writes a name in its own case, such as `List-ID` for `List-Id`, and RFC 5322 makes field names case-insensitive; and it writes `Sender` as the address it parses from the value, the same mailbox. Resend's [custom headers](https://resend.com/docs/dashboard/emails/custom-headers) page and its [send call](https://resend.com/docs/api-reference/emails/send-email) do not say how Resend writes a long field into the message.
|
|
83
91
|
|
|
84
92
|
`headers` adds fields and never replaces the service's own: `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `MIME-Version` and every `Content-` field are refused, in any case. The recipients, the reply-to and the subject have their own options, and the From comes from the configuration's `from`. A name given twice in different cases is refused too. A refusal throws an `Error` before either transport is used, naming the field, or its place in the object when its name is at fault, and none of its value. The `emailSend` tool and the channel adapter add no header fields.
|
|
85
93
|
|
|
94
|
+
The subject goes out on one line through both transports: each line break in it, and each other control character but the tab, is sent as a space, as nodemailer writes a line break in a subject.
|
|
95
|
+
|
|
86
96
|
## Suppressions and unsubscribe tokens
|
|
87
97
|
|
|
88
98
|
Two stores a sender reads, each on the host's SQL database: `SqlSuppressionStore` keeps the addresses not to mail, each named by 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.
|
|
@@ -97,7 +107,8 @@ import {
|
|
|
97
107
|
emailPreferencesDdl,
|
|
98
108
|
} from '@framers/agentos-ext-channel-email';
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
// strict: with DATABASE_URL unset, throw rather than open a local database in its place
|
|
111
|
+
const db = await createDatabase({ url: process.env.DATABASE_URL, strict: true });
|
|
101
112
|
await db.exec(emailPreferencesDdl('mail')); // mail_suppressions and mail_capabilities
|
|
102
113
|
|
|
103
114
|
// A bounce reported by the provider: the store gets the address's hash, never the address.
|
|
@@ -114,17 +125,17 @@ const link = `https://example.com/unsubscribe?t=${token}`;
|
|
|
114
125
|
const grant = await tokens.find(token); // { recipient: 'account-1', scope: 'weekly-summary' }, or null
|
|
115
126
|
```
|
|
116
127
|
|
|
117
|
-
`suppress(scope, addressHash, reason, day)` records an address in a scope with its reason and its day (`YYYY-MM-DD`), and no end. 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 SHA-256 of the address as 64 lower-case hexadecimal characters, 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.
|
|
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 (`DELETE FROM <prefix>_suppressions WHERE scope = ? AND address_hash = ?`). 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 SHA-256 of the address as 64 lower-case hexadecimal characters, 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.
|
|
118
129
|
|
|
119
|
-
`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 and
|
|
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 bytes or more, given as bytes; a different key gives different tokens for the same nonces. `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.
|
|
120
131
|
|
|
121
|
-
`suppressionStoreDdl(prefix)` makes the table `<prefix>_suppressions`, `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, and it keeps two hosts on one database apart. The statements are `CREATE ... IF NOT EXISTS` and run on SQLite and 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 and
|
|
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, and it keeps two hosts on one database apart. The statements are `CREATE ... IF NOT EXISTS` and run on SQLite and 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 characters 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.
|
|
122
133
|
|
|
123
134
|
## Errors and retries
|
|
124
135
|
|
|
125
136
|
A refusal from Resend throws `EmailApiError`, with `status` (the HTTP status), `type` (Resend's error name, such as `validation_error`, or `unknown` when the answer's body is not JSON, names no error, or names one that is not 1 to 64 letters, digits, `_`, `.`, `:` or `-`) and `retryAfterSeconds` (the answer's `retry-after`, or `null`). Its message names the status and the type and never carries Resend's message, which can quote an address.
|
|
126
137
|
|
|
127
|
-
`fetch`'s own errors pass through as it throws them: a `TypeError` when the connection fails, and a `TimeoutError` when `timeoutMs` passes before an answer comes. A deadline that passes while an answer's body is being read throws no `TimeoutError`: a refusal then throws `EmailApiError` with `type` `unknown`, and an accepted send resolves with an empty `messageId`. The key is trimmed at each send. A key, an `idempotencyKey` or a `userAgent` that holds a character an HTTP header cannot carry (a line break or another control character other than a tab, or a character above U+00FF) throws an `Error` before any request, and its message names which one and none of its value. `ResendTransport.send` checks a message's `headers` by the rules for names and values in [header fields](#header-fields), though not against the fields the service writes, and throws the same way
|
|
138
|
+
`fetch`'s own errors pass through as it throws them: a `TypeError` when the connection fails, and a `TimeoutError` when `timeoutMs` passes before an answer comes. A deadline that passes while an answer's body is being read throws no `TimeoutError`: a refusal then throws `EmailApiError` with `type` `unknown`, and an accepted send resolves with an empty `messageId`. The key is trimmed at each send. A key, an `idempotencyKey` or a `userAgent` that holds a character an HTTP header cannot carry (a line break or another control character other than a tab, or a character above U+00FF) throws an `Error` before any request, and its message names which one and none of its value. `ResendTransport.send` checks a message's `headers` by the rules for names and values in [header fields](#header-fields), though not against the fields the service writes, and throws the same way.
|
|
128
139
|
|
|
129
140
|
A send is tried again only on the terms of Resend's [errors](https://resend.com/docs/api-reference/errors) and [rate limit](https://resend.com/docs/api-reference/rate-limit) pages, as `retryWaitSeconds` decides:
|
|
130
141
|
|
|
@@ -136,7 +147,19 @@ The wait is the answer's `retry-after` when it has one, else as many seconds as
|
|
|
136
147
|
|
|
137
148
|
## Reading mail
|
|
138
149
|
|
|
139
|
-
`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`.
|
|
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
|
+
|
|
152
|
+
`searchEmails` finds the messages whose subject, body or From holds `query`, ignoring case, 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
|
+
|
|
154
|
+
`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.
|
|
155
|
+
|
|
156
|
+
A reply goes out through the transport that sends:
|
|
157
|
+
|
|
158
|
+
- to the original's Reply-To, or its From when it names none ([RFC 5322 section 3.6.2](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.2));
|
|
159
|
+
- under the original's subject on one line, with `Re: ` before it unless it starts with `Re:` in any case;
|
|
160
|
+
- threaded as [RFC 5322 section 3.6.4](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4) has it: `In-Reply-To` names the original, and `References` lists the ids of the original's own References, or the one id of its In-Reply-To when its References lists none, then the original's id. Only fields of those very names are read, each whole however it is folded. An id in them that holds a character outside printable US-ASCII, or that is too long for one line, is left out, so whatever the original holds, both transports send the same reply.
|
|
161
|
+
|
|
162
|
+
`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.
|
|
140
163
|
|
|
141
164
|
## In an agent
|
|
142
165
|
|
|
@@ -146,7 +169,7 @@ A model names the tools' arguments and the channel's blocks, so `emailSend` and
|
|
|
146
169
|
|
|
147
170
|
They send each attachment as they checked it: a copy of its own `filename`, `content`, `path` and `contentType`, each read once, with a Buffer's bytes copied. They refuse the same way an attachment that is not an object, one that gives any of those four through a getter or a setter, and one whose `filename` or `contentType` is not a string. Any other property of an attachment is left out.
|
|
148
171
|
|
|
149
|
-
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.
|
|
172
|
+
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.
|
|
150
173
|
|
|
151
174
|
## API
|
|
152
175
|
|
|
@@ -49,7 +49,9 @@ export declare class EmailChannelAdapter {
|
|
|
49
49
|
* filename before anything is sent, a reply included; what is sent is the copy `requireAgentAttachments` made of
|
|
50
50
|
* each attachment, so a Buffer is sent as the bytes it held then. A text block's `text`, an HTML block's `html` (or
|
|
51
51
|
* its `text`), the `subject` in `platformOptions` and `replyToMessageId` must be strings when given: otherwise the
|
|
52
|
-
* service throws a `TypeError` naming the field before anything is sent.
|
|
52
|
+
* service throws a `TypeError` naming the field before anything is sent. A reply goes to the original's Reply-To, or
|
|
53
|
+
* its From, as `EmailService.replyToEmail` sends it, and a `replyToMessageId` that is not one message id, or that no
|
|
54
|
+
* message in the inbox has, throws an `Error` before anything is sent.
|
|
53
55
|
*/
|
|
54
56
|
sendMessage(conversationId: string, content: MessageContent): Promise<ChannelSendResult>;
|
|
55
57
|
sendTypingIndicator(_conversationId: string, _isTyping: boolean): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailChannelAdapter.d.ts","sourceRoot":"","sources":["../src/EmailChannelAdapter.ts"],"names":[],"mappings":"AACA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AACrC,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACvC,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;AAC7G,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,uBAAuB,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAW;IAC7C,QAAQ,CAAC,WAAW,WAAW;IAC/B,QAAQ,CAAC,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAEjD;IAEF,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,YAAY,CAAuB;gBAE/B,OAAO,EAAE,YAAY;IAI3B,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWnD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B,iBAAiB,IAAI,qBAAqB;IAc1C
|
|
1
|
+
{"version":3,"file":"EmailChannelAdapter.d.ts","sourceRoot":"","sources":["../src/EmailChannelAdapter.ts"],"names":[],"mappings":"AACA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AACrC,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACvC,MAAM,MAAM,uBAAuB,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,OAAO,CAAC;AAC7G,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEtC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,uBAAuB,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAE,CAAC,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvE,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAW;IAC7C,QAAQ,CAAC,WAAW,WAAW;IAC/B,QAAQ,CAAC,YAAY,EAAE,SAAS,iBAAiB,EAAE,CAEjD;IAEF,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,QAAQ,CAAuC;IACvD,OAAO,CAAC,YAAY,CAAuB;gBAE/B,OAAO,EAAE,YAAY;IAI3B,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAWnD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAK/B,iBAAiB,IAAI,qBAAqB;IAc1C;;;;;;;;;;OAUG;IACG,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAyCxF,mBAAmB,CAAC,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF,EAAE,CAAC,OAAO,EAAE,mBAAmB,EAAE,WAAW,CAAC,EAAE,gBAAgB,EAAE,GAAG,MAAM,IAAI;CAI/E"}
|
|
@@ -53,7 +53,9 @@ class EmailChannelAdapter {
|
|
|
53
53
|
* filename before anything is sent, a reply included; what is sent is the copy `requireAgentAttachments` made of
|
|
54
54
|
* each attachment, so a Buffer is sent as the bytes it held then. A text block's `text`, an HTML block's `html` (or
|
|
55
55
|
* its `text`), the `subject` in `platformOptions` and `replyToMessageId` must be strings when given: otherwise the
|
|
56
|
-
* service throws a `TypeError` naming the field before anything is sent.
|
|
56
|
+
* service throws a `TypeError` naming the field before anything is sent. A reply goes to the original's Reply-To, or
|
|
57
|
+
* its From, as `EmailService.replyToEmail` sends it, and a `replyToMessageId` that is not one message id, or that no
|
|
58
|
+
* message in the inbox has, throws an `Error` before anything is sent.
|
|
57
59
|
*/
|
|
58
60
|
async sendMessage(conversationId, content) {
|
|
59
61
|
const textBlock = content.blocks.find((b) => b.type === 'text');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailChannelAdapter.js","sourceRoot":"","sources":["../src/EmailChannelAdapter.ts"],"names":[],"mappings":";AAAA,cAAc;AACd;;GAEG;;;AAGH,+DAAgE;AAiChE,MAAa,mBAAmB;IAY9B,YAAY,OAAqB;QAXxB,aAAQ,GAAoB,OAAO,CAAC;QACpC,gBAAW,GAAG,OAAO,CAAC;QACtB,iBAAY,GAAiC;YACpD,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS;SACtD,CAAC;QAGM,gBAAW,GAAkB,IAAI,CAAC;QAClC,aAAQ,GAA6B,IAAI,GAAG,EAAE,CAAC;QAC/C,iBAAY,GAAkB,IAAI,CAAC;QAGzC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAAwB;QACvC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YAC5C,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC;YAChC,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,iBAAiB;QACf,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC;QAC9D,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;YAC5B,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;QACpC,CAAC;QACD,OAAO;YACL,MAAM,EAAE,WAAW;YACnB,cAAc,EAAE,IAAI,CAAC,WAAW,IAAI,SAAS;YAC7C,YAAY,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE;SACpC,CAAC;IACJ,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"EmailChannelAdapter.js","sourceRoot":"","sources":["../src/EmailChannelAdapter.ts"],"names":[],"mappings":"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"}
|
package/dist/EmailService.d.ts
CHANGED
|
@@ -45,10 +45,12 @@ export interface SendEmailOptions {
|
|
|
45
45
|
idempotencyKey?: string;
|
|
46
46
|
/**
|
|
47
47
|
* Header fields added to the message, by name, such as `List-Unsubscribe` and `List-Unsubscribe-Post`: through Resend
|
|
48
|
-
* the request's `headers`, through SMTP fields nodemailer writes into the message. A name is printable
|
|
49
|
-
* space or colon, given once in any case
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* the request's `headers`, through SMTP fields nodemailer writes into the message as given. A name is printable
|
|
49
|
+
* US-ASCII with no space or colon, given once in any case. A value is a string of printable US-ASCII words, one space
|
|
50
|
+
* or tab between each two and none at either end, with no word so long that the name, a colon, a space and the word
|
|
51
|
+
* pass 998 characters, RFC 5322's longest line; `In-Reply-To` and `References` list message ids, each in angle
|
|
52
|
+
* brackets. A field the service writes itself is refused, in any case: `From`, `To`, `Cc`, `Bcc`, `Reply-To`,
|
|
53
|
+
* `Subject`, `Date`, `Message-ID`, `MIME-Version` and any `Content-` field.
|
|
52
54
|
*/
|
|
53
55
|
headers?: Record<string, string>;
|
|
54
56
|
}
|
|
@@ -98,31 +100,61 @@ export declare class EmailService {
|
|
|
98
100
|
* `subject` and `body` must be strings, and `html` a string when it is given (`null` sends no HTML part, as
|
|
99
101
|
* `undefined` does): any other value throws a `TypeError` naming the field, never its value, before either transport
|
|
100
102
|
* is used. An object body would have nodemailer read the file its `path` names or fetch the address its `href` names
|
|
101
|
-
* into the message, and an object subject marked `prepared` would be written into the headers as it is.
|
|
103
|
+
* into the message, and an object subject marked `prepared` would be written into the headers as it is. The subject
|
|
104
|
+
* goes out on one line through either transport: each line break in it, and each other control character but the
|
|
105
|
+
* tab, is sent as a space, as nodemailer writes a line break in a subject.
|
|
102
106
|
*
|
|
103
107
|
* `headers` adds fields to the message and never replaces one the service writes. It must be a plain object of names
|
|
104
|
-
* and values: a name printable US-ASCII with no space or colon, given once in any case,
|
|
105
|
-
* `Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `MIME-Version` or a `Content-`
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
108
|
+
* and values, made in this realm or another: a name printable US-ASCII with no space or colon, given once in any case,
|
|
109
|
+
* and none of `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `MIME-Version` or a `Content-`
|
|
110
|
+
* field, in any case; a value a string of printable US-ASCII words, one space or tab between each two and none at
|
|
111
|
+
* either end, with no word so long that the name, a colon, a space and the word pass 998 characters, and for
|
|
112
|
+
* `In-Reply-To` and `References` message ids, each in angle brackets. Anything else throws an `Error` that names the
|
|
113
|
+
* field, or its place when its name is at fault, and none of its value, before either transport is used. Each value
|
|
114
|
+
* is read once, and both transports send the fields as read: nodemailer writes each value as given, folded at its
|
|
115
|
+
* white space, except `Sender`, which it writes as the address it parses from the value.
|
|
109
116
|
*/
|
|
110
117
|
sendEmail(options: SendEmailOptions): Promise<{
|
|
111
118
|
messageId: string;
|
|
112
119
|
}>;
|
|
113
120
|
readInbox(folder?: string, limit?: number): Promise<EmailMessage[]>;
|
|
121
|
+
/**
|
|
122
|
+
* The messages of `folder` (INBOX unless given) whose subject, body or From holds `query`, ignoring case, received on
|
|
123
|
+
* or after `since` when it is given: the last `limit` (20 unless given) the search finds, newest first. The search
|
|
124
|
+
* answers UIDs, and the messages are fetched by those UIDs, which need not follow the messages' sequence numbers.
|
|
125
|
+
*/
|
|
114
126
|
searchEmails(options: SearchEmailOptions): Promise<EmailMessage[]>;
|
|
127
|
+
/**
|
|
128
|
+
* The codes in the INBOX message whose Message-ID is `messageId`, given with or without its angle brackets: the
|
|
129
|
+
* matches of `pattern` when it is given, else runs of four to eight digits, then the codes that phrases such as "code
|
|
130
|
+
* is", "OTP:", "is your" or "enter ... to" point to, each once. The message is searched for by the id in angle brackets
|
|
131
|
+
* and fetched by the UID the search answers; a search matches every Message-ID that holds the text, so only the
|
|
132
|
+
* message whose own id is the one given counts. An id that is not one message id, or that no message has, gives no
|
|
133
|
+
* codes.
|
|
134
|
+
*/
|
|
115
135
|
extractCodes(messageId: string, pattern?: string): Promise<{
|
|
116
136
|
codes: string[];
|
|
117
137
|
messageId: string;
|
|
118
138
|
}>;
|
|
119
139
|
/**
|
|
120
|
-
* Replies to the message whose Message-ID is `messageId`,
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
140
|
+
* Replies to the INBOX message whose Message-ID is `messageId`, given with or without its angle brackets, and sends
|
|
141
|
+
* `body` with `html`, when given, through the transport that sends. The original is searched for by the id in angle
|
|
142
|
+
* brackets and fetched by the UID the search answers; a search matches every Message-ID that holds the text, so the
|
|
143
|
+
* original is the message whose own id is the one given. The reply goes to the original's Reply-To, or its From when
|
|
144
|
+
* it names none (RFC 5322 section 3.6.2), under its subject with one `Re: ` before it unless it starts with one in any
|
|
145
|
+
* case, on one line. It is threaded as RFC 5322 section 3.6.4 has it: `In-Reply-To` names the original, and
|
|
146
|
+
* `References` lists the ids of the original's own References, or the one id of its In-Reply-To when its References
|
|
147
|
+
* lists none, then the original's id. Only fields of those very names are read, each whole however it is folded, and
|
|
148
|
+
* an id in them that holds a character outside printable US-ASCII, or that is too long for one line, is left out, so
|
|
149
|
+
* both transports send the same fields whatever the original holds.
|
|
150
|
+
*
|
|
151
|
+
* `messageId` and `body` must be strings, and `html` a string when it is given (`null` sends no HTML part): any other
|
|
152
|
+
* value throws a `TypeError` naming the field, never its value, before the original is read or anything is sent.
|
|
153
|
+
* Besides the bodies' files and addresses (see `sendEmail`), an object message id marked `prepared` would be written
|
|
154
|
+
* into the `In-Reply-To` header as it is. A message id that is not one id of printable US-ASCII with no space, or that
|
|
155
|
+
* is too long for one line of `In-Reply-To`, throws an `Error` before the original is read; no message with that id,
|
|
156
|
+
* or an original that names no address to reply to, throws an `Error` before anything is sent. None of these errors
|
|
157
|
+
* holds the id.
|
|
126
158
|
*/
|
|
127
159
|
replyToEmail(messageId: string, body: string, html?: string): Promise<{
|
|
128
160
|
messageId: string;
|
|
@@ -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;AAQ1D,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
|
|
1
|
+
{"version":3,"file":"EmailService.d.ts","sourceRoot":"","sources":["../src/EmailService.ts"],"names":[],"mappings":"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"}
|
package/dist/EmailService.js
CHANGED
|
@@ -47,6 +47,24 @@ const ResendTransport_js_1 = require("./ResendTransport.js");
|
|
|
47
47
|
const messageHeaders_js_1 = require("./messageHeaders.js");
|
|
48
48
|
const messageText_js_1 = require("./messageText.js");
|
|
49
49
|
// ---------------------------------------------------------------------------
|
|
50
|
+
// Helpers
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* The fields nodemailer writes in a form of its own whatever value it is given (its `FORMATTED_HEADERS`) and that
|
|
54
|
+
* `sendEmail` lets a caller add, in lowercase: `Sender`, an address nodemailer parses and writes again, and
|
|
55
|
+
* `References`, whose message ids nodemailer writes as they are once the check has passed them. Every other added
|
|
56
|
+
* field is given to nodemailer as a prepared value, which it writes as given, folded at its white space.
|
|
57
|
+
*/
|
|
58
|
+
const FORMATTED_BY_NODEMAILER = new Set(['sender', 'references']);
|
|
59
|
+
/** A checked field as nodemailer's `headers` list takes it, so nodemailer writes the value Resend would be given. */
|
|
60
|
+
function smtpField(key, value) {
|
|
61
|
+
return { key, value: FORMATTED_BY_NODEMAILER.has(key.toLowerCase()) ? value : { prepared: true, foldLines: true, value } };
|
|
62
|
+
}
|
|
63
|
+
/** The first address an envelope's list names, such as its Reply-To or its From, or `undefined`. */
|
|
64
|
+
function firstAddress(list) {
|
|
65
|
+
return list?.find((entry) => typeof entry?.address === 'string' && entry.address !== '')?.address;
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
50
68
|
// Service
|
|
51
69
|
// ---------------------------------------------------------------------------
|
|
52
70
|
class EmailService {
|
|
@@ -121,19 +139,24 @@ class EmailService {
|
|
|
121
139
|
* `subject` and `body` must be strings, and `html` a string when it is given (`null` sends no HTML part, as
|
|
122
140
|
* `undefined` does): any other value throws a `TypeError` naming the field, never its value, before either transport
|
|
123
141
|
* is used. An object body would have nodemailer read the file its `path` names or fetch the address its `href` names
|
|
124
|
-
* into the message, and an object subject marked `prepared` would be written into the headers as it is.
|
|
142
|
+
* into the message, and an object subject marked `prepared` would be written into the headers as it is. The subject
|
|
143
|
+
* goes out on one line through either transport: each line break in it, and each other control character but the
|
|
144
|
+
* tab, is sent as a space, as nodemailer writes a line break in a subject.
|
|
125
145
|
*
|
|
126
146
|
* `headers` adds fields to the message and never replaces one the service writes. It must be a plain object of names
|
|
127
|
-
* and values: a name printable US-ASCII with no space or colon, given once in any case,
|
|
128
|
-
* `Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `MIME-Version` or a `Content-`
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
147
|
+
* and values, made in this realm or another: a name printable US-ASCII with no space or colon, given once in any case,
|
|
148
|
+
* and none of `From`, `To`, `Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `MIME-Version` or a `Content-`
|
|
149
|
+
* field, in any case; a value a string of printable US-ASCII words, one space or tab between each two and none at
|
|
150
|
+
* either end, with no word so long that the name, a colon, a space and the word pass 998 characters, and for
|
|
151
|
+
* `In-Reply-To` and `References` message ids, each in angle brackets. Anything else throws an `Error` that names the
|
|
152
|
+
* field, or its place when its name is at fault, and none of its value, before either transport is used. Each value
|
|
153
|
+
* is read once, and both transports send the fields as read: nodemailer writes each value as given, folded at its
|
|
154
|
+
* white space, except `Sender`, which it writes as the address it parses from the value.
|
|
132
155
|
*/
|
|
133
156
|
async sendEmail(options) {
|
|
134
157
|
this.requireRunning();
|
|
135
158
|
// Each is read once, so the value checked is the value sent.
|
|
136
|
-
const subject = (0, messageText_js_1.requireText)('subject', options.subject);
|
|
159
|
+
const subject = (0, messageText_js_1.oneLine)((0, messageText_js_1.requireText)('subject', options.subject));
|
|
137
160
|
const body = (0, messageText_js_1.requireText)('body', options.body);
|
|
138
161
|
const html = (0, messageText_js_1.optionalText)('html', options.html);
|
|
139
162
|
const headers = (0, messageHeaders_js_1.requireAddedHeaders)(options.headers);
|
|
@@ -164,8 +187,8 @@ class EmailService {
|
|
|
164
187
|
cc: options.cc,
|
|
165
188
|
bcc: options.bcc,
|
|
166
189
|
// A list, so nodemailer adds each field under its own name: an object with `key` and `value` properties would be
|
|
167
|
-
// read as one field named by its `key`.
|
|
168
|
-
headers: headers === undefined ? undefined : Object.entries(headers).map(([key, value]) => (
|
|
190
|
+
// read as one field named by its `key`. Each value is prepared, so nodemailer writes it as Resend is given it.
|
|
191
|
+
headers: headers === undefined ? undefined : Object.entries(headers).map(([key, value]) => smtpField(key, value)),
|
|
169
192
|
attachments: options.attachments?.map((a) => ({
|
|
170
193
|
filename: a.filename,
|
|
171
194
|
content: a.content,
|
|
@@ -219,6 +242,11 @@ class EmailService {
|
|
|
219
242
|
}
|
|
220
243
|
}
|
|
221
244
|
// ── Search Emails ──
|
|
245
|
+
/**
|
|
246
|
+
* The messages of `folder` (INBOX unless given) whose subject, body or From holds `query`, ignoring case, received on
|
|
247
|
+
* or after `since` when it is given: the last `limit` (20 unless given) the search finds, newest first. The search
|
|
248
|
+
* answers UIDs, and the messages are fetched by those UIDs, which need not follow the messages' sequence numbers.
|
|
249
|
+
*/
|
|
222
250
|
async searchEmails(options) {
|
|
223
251
|
this.requireRunning();
|
|
224
252
|
const client = await this.getImapClient();
|
|
@@ -243,12 +271,13 @@ class EmailService {
|
|
|
243
271
|
const uidList = Array.isArray(uids) ? uids : [];
|
|
244
272
|
const limitedUids = uidList.slice(-(options.limit ?? 20));
|
|
245
273
|
if (limitedUids.length > 0) {
|
|
274
|
+
// The search answered UIDs, so the fetch reads its range as UIDs rather than sequence numbers.
|
|
246
275
|
for await (const msg of client.fetch(limitedUids, {
|
|
247
276
|
envelope: true,
|
|
248
277
|
source: true,
|
|
249
278
|
flags: true,
|
|
250
279
|
uid: true,
|
|
251
|
-
})) {
|
|
280
|
+
}, { uid: true })) {
|
|
252
281
|
const envelope = msg.envelope;
|
|
253
282
|
const source = msg.source?.toString() ?? '';
|
|
254
283
|
const bodyText = this.extractPlainText(source);
|
|
@@ -275,53 +304,66 @@ class EmailService {
|
|
|
275
304
|
}
|
|
276
305
|
}
|
|
277
306
|
// ── Extract Verification Codes ──
|
|
307
|
+
/**
|
|
308
|
+
* The codes in the INBOX message whose Message-ID is `messageId`, given with or without its angle brackets: the
|
|
309
|
+
* matches of `pattern` when it is given, else runs of four to eight digits, then the codes that phrases such as "code
|
|
310
|
+
* is", "OTP:", "is your" or "enter ... to" point to, each once. The message is searched for by the id in angle brackets
|
|
311
|
+
* and fetched by the UID the search answers; a search matches every Message-ID that holds the text, so only the
|
|
312
|
+
* message whose own id is the one given counts. An id that is not one message id, or that no message has, gives no
|
|
313
|
+
* codes.
|
|
314
|
+
*/
|
|
278
315
|
async extractCodes(messageId, pattern) {
|
|
279
316
|
this.requireRunning();
|
|
317
|
+
const wanted = (0, messageHeaders_js_1.oneMessageId)(messageId);
|
|
318
|
+
if (wanted === undefined)
|
|
319
|
+
return { codes: [], messageId };
|
|
280
320
|
const client = await this.getImapClient();
|
|
281
321
|
try {
|
|
282
322
|
const lock = await client.getMailboxLock('INBOX');
|
|
323
|
+
let source;
|
|
283
324
|
try {
|
|
284
|
-
|
|
285
|
-
const uids = await client.search({ header: { 'message-id': messageId } }, { uid: true });
|
|
325
|
+
const uids = await client.search({ header: { 'message-id': wanted } }, { uid: true });
|
|
286
326
|
const uidList = Array.isArray(uids) ? uids : [];
|
|
287
|
-
if (uidList.length
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const bodyText = this.extractPlainText(source);
|
|
293
|
-
const htmlBody = this.extractHtmlBody(source);
|
|
294
|
-
const fullText = bodyText + ' ' + htmlBody;
|
|
295
|
-
// Apply custom or default code extraction patterns
|
|
296
|
-
const regex = pattern
|
|
297
|
-
? new RegExp(pattern, 'g')
|
|
298
|
-
: /\b(\d{4,8})\b/g;
|
|
299
|
-
const codes = [];
|
|
300
|
-
let match;
|
|
301
|
-
while ((match = regex.exec(fullText)) !== null) {
|
|
302
|
-
codes.push(match[1] ?? match[0]);
|
|
303
|
-
}
|
|
304
|
-
// Also look for common verification code patterns
|
|
305
|
-
const verifyPatterns = [
|
|
306
|
-
/(?:code|verification|OTP|pin)\s*(?:is|:)\s*(\w{4,8})/gi,
|
|
307
|
-
/(\d{6})\s*(?:is your|to verify)/gi,
|
|
308
|
-
/(?:enter|use)\s+(\w{4,8})\s+(?:to|as|for)/gi,
|
|
309
|
-
];
|
|
310
|
-
for (const vp of verifyPatterns) {
|
|
311
|
-
while ((match = vp.exec(fullText)) !== null) {
|
|
312
|
-
const code = match[1];
|
|
313
|
-
if (code && !codes.includes(code)) {
|
|
314
|
-
codes.push(code);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
327
|
+
if (uidList.length > 0) {
|
|
328
|
+
// The loop reads every message fetched and keeps the first whose own id is the one given.
|
|
329
|
+
for await (const msg of client.fetch(uidList, { envelope: true, source: true }, { uid: true })) {
|
|
330
|
+
if (source === undefined && (0, messageHeaders_js_1.messageIdIn)(msg.envelope?.messageId) === wanted)
|
|
331
|
+
source = msg.source?.toString() ?? '';
|
|
317
332
|
}
|
|
318
|
-
return { codes: [...new Set(codes)], messageId };
|
|
319
333
|
}
|
|
320
334
|
}
|
|
321
335
|
finally {
|
|
322
336
|
lock.release();
|
|
323
337
|
}
|
|
324
|
-
|
|
338
|
+
if (source === undefined)
|
|
339
|
+
return { codes: [], messageId };
|
|
340
|
+
const bodyText = this.extractPlainText(source);
|
|
341
|
+
const htmlBody = this.extractHtmlBody(source);
|
|
342
|
+
const fullText = bodyText + ' ' + htmlBody;
|
|
343
|
+
// Apply custom or default code extraction patterns
|
|
344
|
+
const regex = pattern
|
|
345
|
+
? new RegExp(pattern, 'g')
|
|
346
|
+
: /\b(\d{4,8})\b/g;
|
|
347
|
+
const codes = [];
|
|
348
|
+
let match;
|
|
349
|
+
while ((match = regex.exec(fullText)) !== null) {
|
|
350
|
+
codes.push(match[1] ?? match[0]);
|
|
351
|
+
}
|
|
352
|
+
// Also look for common verification code patterns
|
|
353
|
+
const verifyPatterns = [
|
|
354
|
+
/(?:code|verification|OTP|pin)\s*(?:is|:)\s*(\w{4,8})/gi,
|
|
355
|
+
/(\d{6})\s*(?:is your|to verify)/gi,
|
|
356
|
+
/(?:enter|use)\s+(\w{4,8})\s+(?:to|as|for)/gi,
|
|
357
|
+
];
|
|
358
|
+
for (const vp of verifyPatterns) {
|
|
359
|
+
while ((match = vp.exec(fullText)) !== null) {
|
|
360
|
+
const code = match[1];
|
|
361
|
+
if (code && !codes.includes(code)) {
|
|
362
|
+
codes.push(code);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return { codes: [...new Set(codes)], messageId };
|
|
325
367
|
}
|
|
326
368
|
finally {
|
|
327
369
|
await client.logout();
|
|
@@ -329,12 +371,24 @@ class EmailService {
|
|
|
329
371
|
}
|
|
330
372
|
// ── Reply to Email ──
|
|
331
373
|
/**
|
|
332
|
-
* Replies to the message whose Message-ID is `messageId`,
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
374
|
+
* Replies to the INBOX message whose Message-ID is `messageId`, given with or without its angle brackets, and sends
|
|
375
|
+
* `body` with `html`, when given, through the transport that sends. The original is searched for by the id in angle
|
|
376
|
+
* brackets and fetched by the UID the search answers; a search matches every Message-ID that holds the text, so the
|
|
377
|
+
* original is the message whose own id is the one given. The reply goes to the original's Reply-To, or its From when
|
|
378
|
+
* it names none (RFC 5322 section 3.6.2), under its subject with one `Re: ` before it unless it starts with one in any
|
|
379
|
+
* case, on one line. It is threaded as RFC 5322 section 3.6.4 has it: `In-Reply-To` names the original, and
|
|
380
|
+
* `References` lists the ids of the original's own References, or the one id of its In-Reply-To when its References
|
|
381
|
+
* lists none, then the original's id. Only fields of those very names are read, each whole however it is folded, and
|
|
382
|
+
* an id in them that holds a character outside printable US-ASCII, or that is too long for one line, is left out, so
|
|
383
|
+
* both transports send the same fields whatever the original holds.
|
|
384
|
+
*
|
|
385
|
+
* `messageId` and `body` must be strings, and `html` a string when it is given (`null` sends no HTML part): any other
|
|
386
|
+
* value throws a `TypeError` naming the field, never its value, before the original is read or anything is sent.
|
|
387
|
+
* Besides the bodies' files and addresses (see `sendEmail`), an object message id marked `prepared` would be written
|
|
388
|
+
* into the `In-Reply-To` header as it is. A message id that is not one id of printable US-ASCII with no space, or that
|
|
389
|
+
* is too long for one line of `In-Reply-To`, throws an `Error` before the original is read; no message with that id,
|
|
390
|
+
* or an original that names no address to reply to, throws an `Error` before anything is sent. None of these errors
|
|
391
|
+
* holds the id.
|
|
338
392
|
*/
|
|
339
393
|
async replyToEmail(messageId, body, html) {
|
|
340
394
|
this.requireRunning();
|
|
@@ -343,23 +397,26 @@ class EmailService {
|
|
|
343
397
|
(0, messageText_js_1.requireText)('messageId', messageId);
|
|
344
398
|
(0, messageText_js_1.requireText)('body', body);
|
|
345
399
|
html = (0, messageText_js_1.optionalText)('html', html);
|
|
346
|
-
//
|
|
400
|
+
// The original's id as In-Reply-To and the end of References carry it, through either transport.
|
|
401
|
+
const parent = (0, messageHeaders_js_1.requireMessageId)(messageId);
|
|
347
402
|
const client = await this.getImapClient();
|
|
348
|
-
let
|
|
349
|
-
let originalSubject = '';
|
|
350
|
-
let previous = '';
|
|
403
|
+
let original;
|
|
351
404
|
try {
|
|
352
405
|
const lock = await client.getMailboxLock('INBOX');
|
|
353
406
|
try {
|
|
354
|
-
const uids = await client.search({ header: { 'message-id':
|
|
407
|
+
const uids = await client.search({ header: { 'message-id': parent } }, { uid: true });
|
|
355
408
|
const uidList = Array.isArray(uids) ? uids : [];
|
|
356
409
|
if (uidList.length > 0) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
410
|
+
// The loop reads every message fetched and keeps the first whose own id is the one given.
|
|
411
|
+
for await (const msg of client.fetch(uidList, { envelope: true, headers: true }, { uid: true })) {
|
|
412
|
+
if (original !== undefined || (0, messageHeaders_js_1.messageIdIn)(msg.envelope?.messageId) !== parent)
|
|
413
|
+
continue;
|
|
414
|
+
original = {
|
|
415
|
+
// A server gives the From as the Reply-To of a message that names none (RFC 3501 section 7.4.2).
|
|
416
|
+
to: firstAddress(msg.envelope?.replyTo) ?? firstAddress(msg.envelope?.from) ?? '',
|
|
417
|
+
subject: (0, messageText_js_1.oneLine)(msg.envelope?.subject ?? ''),
|
|
418
|
+
thread: (0, messageHeaders_js_1.threadOf)(msg.headers?.toString() ?? ''),
|
|
419
|
+
};
|
|
363
420
|
}
|
|
364
421
|
}
|
|
365
422
|
}
|
|
@@ -370,25 +427,31 @@ class EmailService {
|
|
|
370
427
|
finally {
|
|
371
428
|
await client.logout();
|
|
372
429
|
}
|
|
373
|
-
|
|
374
|
-
|
|
430
|
+
if (original === undefined)
|
|
431
|
+
throw new Error('The reply was refused: no message in the inbox has that message id');
|
|
432
|
+
if (original.to === '')
|
|
433
|
+
throw new Error('The reply was refused: the message names no address to reply to');
|
|
434
|
+
const replySubject = /^re:/i.test(original.subject) ? original.subject : `Re: ${original.subject}`;
|
|
435
|
+
const references = [...original.thread, parent].join(' ');
|
|
436
|
+
// Checked by the rules every added field meets, so both transports are given the same fields.
|
|
437
|
+
const thread = (0, messageHeaders_js_1.requireHeaders)({ 'In-Reply-To': parent, References: references });
|
|
375
438
|
if (this.resend) {
|
|
376
439
|
return this.resend.send({
|
|
377
440
|
from: this.config.from,
|
|
378
|
-
to:
|
|
441
|
+
to: original.to,
|
|
379
442
|
subject: replySubject,
|
|
380
443
|
text: body,
|
|
381
444
|
html,
|
|
382
|
-
headers:
|
|
445
|
+
headers: thread,
|
|
383
446
|
});
|
|
384
447
|
}
|
|
385
448
|
const info = await this.transporter.sendMail({
|
|
386
449
|
from: this.config.from ?? this.config.smtp.user,
|
|
387
|
-
to:
|
|
450
|
+
to: original.to,
|
|
388
451
|
subject: replySubject,
|
|
389
452
|
text: body,
|
|
390
453
|
html,
|
|
391
|
-
inReplyTo:
|
|
454
|
+
inReplyTo: parent,
|
|
392
455
|
references,
|
|
393
456
|
});
|
|
394
457
|
return { messageId: info.messageId };
|