@bobfrankston/mailx-types 0.1.36 → 0.1.40
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/index.d.ts +42 -0
- package/index.js +120 -0
- package/mime-inline-images.d.ts +38 -0
- package/mime-inline-images.js +45 -0
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } fr
|
|
|
10
10
|
export type { GroupMap, RecipientToken, ExpansionResult } from "./groups.js";
|
|
11
11
|
export { REMINDER_STATE_FILE, normalizeReminderState, mergeReminderStates, reminderStatesEqual, } from "./reminder-state.js";
|
|
12
12
|
export type { ReminderState } from "./reminder-state.js";
|
|
13
|
+
export { extractInlineImages } from "./mime-inline-images.js";
|
|
14
|
+
export type { InlineImagePart } from "./mime-inline-images.js";
|
|
13
15
|
/** Supported authentication methods */
|
|
14
16
|
export type AuthMethod = "password" | "oauth2";
|
|
15
17
|
/** Mail account configuration */
|
|
@@ -361,6 +363,46 @@ export declare function sanitizeHtml(html: string): {
|
|
|
361
363
|
html: string;
|
|
362
364
|
hasRemoteContent: boolean;
|
|
363
365
|
};
|
|
366
|
+
/**
|
|
367
|
+
* Encode a header VALUE as RFC 2047 encoded-words when it contains non-ASCII.
|
|
368
|
+
*
|
|
369
|
+
* Why this has to exist: a header carrying raw 8-bit UTF-8 makes the whole
|
|
370
|
+
* message an EAI message (RFC 6530), which can only be relayed over SMTPUTF8
|
|
371
|
+
* (RFC 6531). Plenty of receivers still don't advertise that extension, and a
|
|
372
|
+
* sending MTA that can't downgrade has no choice but to bounce. mailx built
|
|
373
|
+
* every outgoing header with bare interpolation — `Subject: ${msg.subject}` —
|
|
374
|
+
* so ONE curly apostrophe was enough to make a message undeliverable:
|
|
375
|
+
*
|
|
376
|
+
* Subject: Re: … One of Canada’s Fastest Fiber Networks (U+2019, raw UTF-8)
|
|
377
|
+
* → <alland@soundbytesradio.com>: EAI message but server 17.57.152.5
|
|
378
|
+
* does not support SMTPUTF8 … This is a permanent error
|
|
379
|
+
*
|
|
380
|
+
* (Bob 2026-08-08, qmail bounce from gal.iecc.com — iCloud, Zoho and
|
|
381
|
+
* land-com all refused it; herot.com, gmail and bob.ma took it, so it looked
|
|
382
|
+
* like a partial failure.) Reply subjects inherit whatever punctuation the
|
|
383
|
+
* original used, and Word/paste smart quotes do the rest, so this fires
|
|
384
|
+
* constantly and silently.
|
|
385
|
+
*
|
|
386
|
+
* Pure-ASCII values are returned untouched — encoding them would be legal but
|
|
387
|
+
* needlessly unreadable in every mail client's raw-source view.
|
|
388
|
+
*
|
|
389
|
+
* Base64 rather than Q-encoding: it is uniform (no per-character escaping
|
|
390
|
+
* rules that differ between phrase and unstructured context) and never
|
|
391
|
+
* produces a token needing further escaping. Chunks are split so each
|
|
392
|
+
* encoded-word stays inside RFC 2047's 75-character limit, split on CHARACTER
|
|
393
|
+
* boundaries so a multi-byte sequence is never cut in half.
|
|
394
|
+
*/
|
|
395
|
+
export declare function encodeHeaderWord(text: string): string;
|
|
396
|
+
/**
|
|
397
|
+
* Encode an address header value (From/To/Cc/Bcc/Reply-To), encoding only the
|
|
398
|
+
* DISPLAY NAME of each address. The addr-spec must stay literal: encoding it
|
|
399
|
+
* would corrupt the address, and a non-ASCII addr-spec is genuinely EAI and
|
|
400
|
+
* cannot be represented any other way.
|
|
401
|
+
*
|
|
402
|
+
* Splits on commas outside angle brackets and quotes so a display name
|
|
403
|
+
* containing a comma ("Frankston, Bob" <x@y>) survives.
|
|
404
|
+
*/
|
|
405
|
+
export declare function encodeAddressHeader(value: string): string;
|
|
364
406
|
/** Encode text as RFC 2045 quoted-printable. */
|
|
365
407
|
export declare function encodeQuotedPrintable(text: string): string;
|
|
366
408
|
/** Render an HTML document as a plain-text approximation suitable for the
|
package/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export { expandRecipients, splitRecipients, isAddressToken, extractAddress, } fr
|
|
|
17
17
|
// Reminder dismissed/snoozed cross-device state — one merge implementation
|
|
18
18
|
// for the desktop service (mailx-settings) and Android/web (mailx-store-web).
|
|
19
19
|
export { REMINDER_STATE_FILE, normalizeReminderState, mergeReminderStates, reminderStatesEqual, } from "./reminder-state.js";
|
|
20
|
+
// Outgoing-mail inline images: data: URI → cid: extraction shared by both
|
|
21
|
+
// MIME assemblers (desktop send + Android/web send).
|
|
22
|
+
export { extractInlineImages } from "./mime-inline-images.js";
|
|
20
23
|
// ── Message flag state ──
|
|
21
24
|
//
|
|
22
25
|
// External API surface for the IMAP system flags. The literal strings
|
|
@@ -103,6 +106,123 @@ export function sanitizeHtml(html) {
|
|
|
103
106
|
clean = clean.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, "");
|
|
104
107
|
return { html: clean, hasRemoteContent };
|
|
105
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Encode a header VALUE as RFC 2047 encoded-words when it contains non-ASCII.
|
|
111
|
+
*
|
|
112
|
+
* Why this has to exist: a header carrying raw 8-bit UTF-8 makes the whole
|
|
113
|
+
* message an EAI message (RFC 6530), which can only be relayed over SMTPUTF8
|
|
114
|
+
* (RFC 6531). Plenty of receivers still don't advertise that extension, and a
|
|
115
|
+
* sending MTA that can't downgrade has no choice but to bounce. mailx built
|
|
116
|
+
* every outgoing header with bare interpolation — `Subject: ${msg.subject}` —
|
|
117
|
+
* so ONE curly apostrophe was enough to make a message undeliverable:
|
|
118
|
+
*
|
|
119
|
+
* Subject: Re: … One of Canada’s Fastest Fiber Networks (U+2019, raw UTF-8)
|
|
120
|
+
* → <alland@soundbytesradio.com>: EAI message but server 17.57.152.5
|
|
121
|
+
* does not support SMTPUTF8 … This is a permanent error
|
|
122
|
+
*
|
|
123
|
+
* (Bob 2026-08-08, qmail bounce from gal.iecc.com — iCloud, Zoho and
|
|
124
|
+
* land-com all refused it; herot.com, gmail and bob.ma took it, so it looked
|
|
125
|
+
* like a partial failure.) Reply subjects inherit whatever punctuation the
|
|
126
|
+
* original used, and Word/paste smart quotes do the rest, so this fires
|
|
127
|
+
* constantly and silently.
|
|
128
|
+
*
|
|
129
|
+
* Pure-ASCII values are returned untouched — encoding them would be legal but
|
|
130
|
+
* needlessly unreadable in every mail client's raw-source view.
|
|
131
|
+
*
|
|
132
|
+
* Base64 rather than Q-encoding: it is uniform (no per-character escaping
|
|
133
|
+
* rules that differ between phrase and unstructured context) and never
|
|
134
|
+
* produces a token needing further escaping. Chunks are split so each
|
|
135
|
+
* encoded-word stays inside RFC 2047's 75-character limit, split on CHARACTER
|
|
136
|
+
* boundaries so a multi-byte sequence is never cut in half.
|
|
137
|
+
*/
|
|
138
|
+
export function encodeHeaderWord(text) {
|
|
139
|
+
if (!text)
|
|
140
|
+
return "";
|
|
141
|
+
// eslint-disable-next-line no-control-regex
|
|
142
|
+
if (!/[^\x00-\x7F]/.test(text))
|
|
143
|
+
return text;
|
|
144
|
+
const encoder = new TextEncoder();
|
|
145
|
+
// "=?UTF-8?B?" + payload + "?=" must be ≤ 75 chars, so the base64 payload
|
|
146
|
+
// gets 75 - 12 = 63 chars, and base64 only splits cleanly every 4 chars →
|
|
147
|
+
// 60 chars of payload = 45 bytes of input per encoded-word.
|
|
148
|
+
const MAX_BYTES_PER_WORD = 45;
|
|
149
|
+
const words = [];
|
|
150
|
+
let chunk = [];
|
|
151
|
+
for (const ch of text) { // iterate by code point
|
|
152
|
+
const bytes = Array.from(encoder.encode(ch));
|
|
153
|
+
if (chunk.length + bytes.length > MAX_BYTES_PER_WORD) {
|
|
154
|
+
words.push(chunk);
|
|
155
|
+
chunk = [];
|
|
156
|
+
}
|
|
157
|
+
chunk.push(...bytes);
|
|
158
|
+
}
|
|
159
|
+
if (chunk.length)
|
|
160
|
+
words.push(chunk);
|
|
161
|
+
// Continuation lines are joined with CRLF + space: consecutive
|
|
162
|
+
// encoded-words separated by folding whitespace are concatenated by the
|
|
163
|
+
// receiver with the whitespace REMOVED (RFC 2047 §6.2), which is what we
|
|
164
|
+
// want — the original text had no space there.
|
|
165
|
+
return words
|
|
166
|
+
.map(b => `=?UTF-8?B?${bytesToBase64(Uint8Array.from(b))}?=`)
|
|
167
|
+
.join("\r\n ");
|
|
168
|
+
}
|
|
169
|
+
/** Base64 without assuming Buffer (this package is browser-capable). */
|
|
170
|
+
function bytesToBase64(bytes) {
|
|
171
|
+
let bin = "";
|
|
172
|
+
for (const b of bytes)
|
|
173
|
+
bin += String.fromCharCode(b);
|
|
174
|
+
// btoa exists in browsers and in Node ≥16.
|
|
175
|
+
return btoa(bin);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Encode an address header value (From/To/Cc/Bcc/Reply-To), encoding only the
|
|
179
|
+
* DISPLAY NAME of each address. The addr-spec must stay literal: encoding it
|
|
180
|
+
* would corrupt the address, and a non-ASCII addr-spec is genuinely EAI and
|
|
181
|
+
* cannot be represented any other way.
|
|
182
|
+
*
|
|
183
|
+
* Splits on commas outside angle brackets and quotes so a display name
|
|
184
|
+
* containing a comma ("Frankston, Bob" <x@y>) survives.
|
|
185
|
+
*/
|
|
186
|
+
export function encodeAddressHeader(value) {
|
|
187
|
+
if (!value)
|
|
188
|
+
return "";
|
|
189
|
+
// eslint-disable-next-line no-control-regex
|
|
190
|
+
if (!/[^\x00-\x7F]/.test(value))
|
|
191
|
+
return value;
|
|
192
|
+
const parts = [];
|
|
193
|
+
let cur = "";
|
|
194
|
+
let inAngle = false;
|
|
195
|
+
let inQuote = false;
|
|
196
|
+
for (const ch of value) {
|
|
197
|
+
if (ch === '"')
|
|
198
|
+
inQuote = !inQuote;
|
|
199
|
+
else if (ch === "<" && !inQuote)
|
|
200
|
+
inAngle = true;
|
|
201
|
+
else if (ch === ">" && !inQuote)
|
|
202
|
+
inAngle = false;
|
|
203
|
+
if (ch === "," && !inAngle && !inQuote) {
|
|
204
|
+
parts.push(cur);
|
|
205
|
+
cur = "";
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
cur += ch;
|
|
209
|
+
}
|
|
210
|
+
if (cur.trim())
|
|
211
|
+
parts.push(cur);
|
|
212
|
+
return parts.map(part => {
|
|
213
|
+
const p = part.trim();
|
|
214
|
+
const m = /^(.*?)\s*(<[^>]*>)\s*$/.exec(p);
|
|
215
|
+
if (!m)
|
|
216
|
+
return p; // bare addr-spec, nothing to encode
|
|
217
|
+
const display = m[1].replace(/^"(.*)"$/s, "$1");
|
|
218
|
+
if (!display)
|
|
219
|
+
return m[2];
|
|
220
|
+
// eslint-disable-next-line no-control-regex
|
|
221
|
+
if (!/[^\x00-\x7F]/.test(display))
|
|
222
|
+
return p; // ASCII name — leave the quoting alone
|
|
223
|
+
return `${encodeHeaderWord(display)} ${m[2]}`;
|
|
224
|
+
}).join(", ");
|
|
225
|
+
}
|
|
106
226
|
/** Encode text as RFC 2045 quoted-printable. */
|
|
107
227
|
export function encodeQuotedPrintable(text) {
|
|
108
228
|
const encoder = new TextEncoder();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-image extraction for outgoing mail.
|
|
3
|
+
*
|
|
4
|
+
* The compose editors embed pasted/inlined images as `data:` URIs — that's
|
|
5
|
+
* what renders inside a WebView editor with no server round-trip. But a
|
|
6
|
+
* `data:` URI inside the text/html MIME part is NOT interoperable mail:
|
|
7
|
+
* Outlook desktop (Word renderer) doesn't render data: images at all, and
|
|
8
|
+
* Gmail's web sanitizer strips them, so recipients on the two biggest
|
|
9
|
+
* platforms see broken/missing images while Thunderbird/Apple Mail users see
|
|
10
|
+
* the message fine. The interoperable form is RFC 2392 `cid:` references to
|
|
11
|
+
* base64 image parts inside a multipart/related wrapper — what every mail
|
|
12
|
+
* client has emitted since the 90s.
|
|
13
|
+
*
|
|
14
|
+
* This helper rewrites `<img src="data:...;base64,...">` to `cid:` refs and
|
|
15
|
+
* hands back the extracted parts; the platform send() paths (desktop
|
|
16
|
+
* mailx-service and Android/web web-service) wrap them into
|
|
17
|
+
* multipart/related. Shared here (mailx-types, zero deps) so both MIME
|
|
18
|
+
* assemblers can't drift apart.
|
|
19
|
+
*
|
|
20
|
+
* Non-base64 data: URIs (utf8 SVG etc.) are left in place — rare, small,
|
|
21
|
+
* and rewriting them would mean re-encoding; the size/interop problem this
|
|
22
|
+
* solves comes from pasted photos, which are always base64.
|
|
23
|
+
*/
|
|
24
|
+
export interface InlineImagePart {
|
|
25
|
+
/** Content-ID value WITHOUT angle brackets (use `<${contentId}>` in the header). */
|
|
26
|
+
contentId: string;
|
|
27
|
+
mime: string;
|
|
28
|
+
base64: string;
|
|
29
|
+
}
|
|
30
|
+
/** Pull base64 `data:` images out of the HTML, replacing each src with a
|
|
31
|
+
* `cid:` reference. Identical data: URIs (the same image pasted twice)
|
|
32
|
+
* collapse to one part. `idDomain` scopes the Content-IDs (RFC 2392 wants
|
|
33
|
+
* msg-id-like uniqueness). */
|
|
34
|
+
export declare function extractInlineImages(html: string, idDomain: string): {
|
|
35
|
+
html: string;
|
|
36
|
+
images: InlineImagePart[];
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=mime-inline-images.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-image extraction for outgoing mail.
|
|
3
|
+
*
|
|
4
|
+
* The compose editors embed pasted/inlined images as `data:` URIs — that's
|
|
5
|
+
* what renders inside a WebView editor with no server round-trip. But a
|
|
6
|
+
* `data:` URI inside the text/html MIME part is NOT interoperable mail:
|
|
7
|
+
* Outlook desktop (Word renderer) doesn't render data: images at all, and
|
|
8
|
+
* Gmail's web sanitizer strips them, so recipients on the two biggest
|
|
9
|
+
* platforms see broken/missing images while Thunderbird/Apple Mail users see
|
|
10
|
+
* the message fine. The interoperable form is RFC 2392 `cid:` references to
|
|
11
|
+
* base64 image parts inside a multipart/related wrapper — what every mail
|
|
12
|
+
* client has emitted since the 90s.
|
|
13
|
+
*
|
|
14
|
+
* This helper rewrites `<img src="data:...;base64,...">` to `cid:` refs and
|
|
15
|
+
* hands back the extracted parts; the platform send() paths (desktop
|
|
16
|
+
* mailx-service and Android/web web-service) wrap them into
|
|
17
|
+
* multipart/related. Shared here (mailx-types, zero deps) so both MIME
|
|
18
|
+
* assemblers can't drift apart.
|
|
19
|
+
*
|
|
20
|
+
* Non-base64 data: URIs (utf8 SVG etc.) are left in place — rare, small,
|
|
21
|
+
* and rewriting them would mean re-encoding; the size/interop problem this
|
|
22
|
+
* solves comes from pasted photos, which are always base64.
|
|
23
|
+
*/
|
|
24
|
+
/** Pull base64 `data:` images out of the HTML, replacing each src with a
|
|
25
|
+
* `cid:` reference. Identical data: URIs (the same image pasted twice)
|
|
26
|
+
* collapse to one part. `idDomain` scopes the Content-IDs (RFC 2392 wants
|
|
27
|
+
* msg-id-like uniqueness). */
|
|
28
|
+
export function extractInlineImages(html, idDomain) {
|
|
29
|
+
const images = [];
|
|
30
|
+
const byDataUri = new Map(); // data-uri → contentId
|
|
31
|
+
const stamp = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
const out = html.replace(/(<img\b[^>]*?\ssrc=)(["'])(data:(image\/[\w.+-]+);base64,([A-Za-z0-9+/=\s]+))\2/gi, (_m, prefix, quote, dataUri, mime, b64) => {
|
|
33
|
+
let cid = byDataUri.get(dataUri);
|
|
34
|
+
if (!cid) {
|
|
35
|
+
cid = `img${images.length + 1}.${stamp}@${idDomain}`;
|
|
36
|
+
byDataUri.set(dataUri, cid);
|
|
37
|
+
// Strip whitespace the editor may have wrapped into the URI;
|
|
38
|
+
// the send path re-wraps at 76 cols per RFC 2045.
|
|
39
|
+
images.push({ contentId: cid, mime, base64: b64.replace(/\s+/g, "") });
|
|
40
|
+
}
|
|
41
|
+
return `${prefix}${quote}cid:${cid}${quote}`;
|
|
42
|
+
});
|
|
43
|
+
return { html: out, images };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=mime-inline-images.js.map
|