@upyo/jmap 0.6.0-dev.318 → 0.6.0-dev.319
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/dist/index.cjs +124 -9
- package/dist/index.js +124 -9
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -22,6 +22,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
24
|
const __upyo_core = __toESM(require("@upyo/core"));
|
|
25
|
+
const __upyo_core_message_id = __toESM(require("@upyo/core/message-id"));
|
|
25
26
|
|
|
26
27
|
//#region src/errors.ts
|
|
27
28
|
/**
|
|
@@ -300,17 +301,44 @@ function getPriorityHeaders(priority) {
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
/**
|
|
303
|
-
*
|
|
304
|
+
* Header field names that must not be written as a raw header property.
|
|
305
|
+
*
|
|
306
|
+
* RFC 8621 §4.6 forbids two properties representing the same header field, and
|
|
307
|
+
* the `Email` here already carries a structured property for each of these.
|
|
308
|
+
* `Bcc` matters most: it travels in the submission envelope, so writing it into
|
|
309
|
+
* the message would disclose recipients meant to stay hidden. `Content-*`
|
|
310
|
+
* belongs on an `EmailBodyPart` rather than on the `Email`.
|
|
311
|
+
*/
|
|
312
|
+
const structuredHeaders = new Set([
|
|
313
|
+
"bcc",
|
|
314
|
+
"cc",
|
|
315
|
+
"from",
|
|
316
|
+
"mime-version",
|
|
317
|
+
"reply-to",
|
|
318
|
+
"subject",
|
|
319
|
+
"to"
|
|
320
|
+
]);
|
|
321
|
+
/**
|
|
322
|
+
* Extracts the custom headers of a message, dropping those the `Email` object
|
|
323
|
+
* expresses through a structured property of its own.
|
|
324
|
+
*
|
|
304
325
|
* @param message The message to extract headers from.
|
|
305
326
|
* @returns Array of JMAP headers.
|
|
327
|
+
* @throws {TypeError} If a header value contains a carriage return or line
|
|
328
|
+
* feed, which would forge further header fields.
|
|
306
329
|
* @since 0.4.0
|
|
307
330
|
*/
|
|
308
331
|
function extractCustomHeaders(message) {
|
|
309
332
|
const headers = [];
|
|
310
|
-
for (const [name, value] of message.headers.entries())
|
|
311
|
-
name
|
|
312
|
-
|
|
313
|
-
|
|
333
|
+
for (const [name, value] of message.headers.entries()) {
|
|
334
|
+
const lowered = name.toLowerCase();
|
|
335
|
+
if (structuredHeaders.has(lowered) || lowered.startsWith("content-")) continue;
|
|
336
|
+
if (/[\r\n]/.test(value)) throw new TypeError(`Header field ${name} must not contain a carriage return or line feed.`);
|
|
337
|
+
headers.push({
|
|
338
|
+
name,
|
|
339
|
+
value
|
|
340
|
+
});
|
|
341
|
+
}
|
|
314
342
|
return headers;
|
|
315
343
|
}
|
|
316
344
|
/**
|
|
@@ -418,13 +446,21 @@ function buildAttachmentParts(attachments, uploadedBlobs) {
|
|
|
418
446
|
* @param draftMailboxId The mailbox ID to store the draft in.
|
|
419
447
|
* @param uploadedBlobs Map of contentId to blobId for attachments.
|
|
420
448
|
* @returns The JMAP Email/set create object.
|
|
449
|
+
* @throws {TypeError} If the message carries an invalid message identifier or
|
|
450
|
+
* date, or a header value containing a carriage return or line feed.
|
|
451
|
+
* @throws {RangeError} If the year of the date is outside what an RFC 3339
|
|
452
|
+
* date-time can express.
|
|
421
453
|
* @since 0.4.0
|
|
422
454
|
*/
|
|
423
455
|
function convertMessage(message, draftMailboxId, uploadedBlobs) {
|
|
424
456
|
const { bodyStructure, bodyValues } = buildBodyStructure(message, uploadedBlobs);
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
457
|
+
const identity = resolveIdentity(message);
|
|
458
|
+
const headerProperties = {};
|
|
459
|
+
for (const { name, value } of getPriorityHeaders(message.priority)) headerProperties[`header:${name}`] = value;
|
|
460
|
+
for (const { name, value } of extractCustomHeaders(message)) {
|
|
461
|
+
if (identity.owned.has(name.toLowerCase())) continue;
|
|
462
|
+
headerProperties[`header:${name}`] = value;
|
|
463
|
+
}
|
|
428
464
|
const email = {
|
|
429
465
|
mailboxIds: { [draftMailboxId]: true },
|
|
430
466
|
from: [formatAddress(message.sender)],
|
|
@@ -435,10 +471,89 @@ function convertMessage(message, draftMailboxId, uploadedBlobs) {
|
|
|
435
471
|
...message.ccRecipients.length > 0 && { cc: message.ccRecipients.map(formatAddress) },
|
|
436
472
|
...message.bccRecipients.length > 0 && { bcc: message.bccRecipients.map(formatAddress) },
|
|
437
473
|
...message.replyRecipients.length > 0 && { replyTo: message.replyRecipients.map(formatAddress) },
|
|
438
|
-
...
|
|
474
|
+
...identity.properties,
|
|
475
|
+
...headerProperties
|
|
439
476
|
};
|
|
440
477
|
return email;
|
|
441
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Works out the identity and threading properties of an `Email` from the typed
|
|
481
|
+
* fields of a message.
|
|
482
|
+
*
|
|
483
|
+
* A field the message leaves unset is not owned, so a header supplied through
|
|
484
|
+
* {@link Message.headers} still reaches the server, and the server falls back
|
|
485
|
+
* to generating an identifier and a date of its own. A threading field set to
|
|
486
|
+
* an empty list is owned but writes no property, which suppresses such a
|
|
487
|
+
* header.
|
|
488
|
+
*
|
|
489
|
+
* @param message The message being converted.
|
|
490
|
+
* @returns The properties to set, and the field names the message owns.
|
|
491
|
+
* @throws {TypeError} If a message identifier is invalid, or the date is not a
|
|
492
|
+
* valid one.
|
|
493
|
+
* @throws {RangeError} If the year of the date is outside what an RFC 3339
|
|
494
|
+
* date-time can express.
|
|
495
|
+
*/
|
|
496
|
+
function resolveIdentity(message) {
|
|
497
|
+
const owned = /* @__PURE__ */ new Set();
|
|
498
|
+
const properties = {};
|
|
499
|
+
if (message.priority !== "normal") {
|
|
500
|
+
owned.add("x-priority");
|
|
501
|
+
owned.add("importance");
|
|
502
|
+
}
|
|
503
|
+
if (message.messageId != null) {
|
|
504
|
+
owned.add("message-id");
|
|
505
|
+
properties.messageId = [checkMessageId(message.messageId)];
|
|
506
|
+
}
|
|
507
|
+
if (message.date != null) {
|
|
508
|
+
owned.add("date");
|
|
509
|
+
properties.sentAt = formatSentAt(message.date);
|
|
510
|
+
}
|
|
511
|
+
if (message.inReplyTo != null) {
|
|
512
|
+
owned.add("in-reply-to");
|
|
513
|
+
if (message.inReplyTo.length > 0) properties.inReplyTo = message.inReplyTo.map(checkMessageId);
|
|
514
|
+
}
|
|
515
|
+
if (message.references != null) {
|
|
516
|
+
owned.add("references");
|
|
517
|
+
if (message.references.length > 0) properties.references = message.references.map(checkMessageId);
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
properties,
|
|
521
|
+
owned
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Checks a message identifier a message carries.
|
|
526
|
+
*
|
|
527
|
+
* `createMessage()` validates these, but `Message` is a structural type that a
|
|
528
|
+
* caller can build without it.
|
|
529
|
+
*
|
|
530
|
+
* @param id The identifier to check.
|
|
531
|
+
* @returns The identifier, unchanged.
|
|
532
|
+
* @throws {TypeError} If the value is not a valid message identifier.
|
|
533
|
+
*/
|
|
534
|
+
function checkMessageId(id) {
|
|
535
|
+
const parsed = (0, __upyo_core_message_id.parseMessageId)(id);
|
|
536
|
+
if (parsed == null) throw new TypeError(`Invalid message ID: ${JSON.stringify(id)}`);
|
|
537
|
+
return parsed;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Formats a date as the RFC 3339 date-time JMAP calls a `Date`.
|
|
541
|
+
*
|
|
542
|
+
* RFC 8620 §1.4 omits a fractional-seconds part that is zero, which
|
|
543
|
+
* `Date.toISOString()` always writes.
|
|
544
|
+
*
|
|
545
|
+
* @param date The date to format.
|
|
546
|
+
* @returns The formatted date.
|
|
547
|
+
* @throws {TypeError} If the date is not a valid one.
|
|
548
|
+
* @throws {RangeError} If its year cannot be written as four digits.
|
|
549
|
+
*/
|
|
550
|
+
function formatSentAt(date) {
|
|
551
|
+
const time = date instanceof Date ? date.getTime() : NaN;
|
|
552
|
+
if (Number.isNaN(time)) throw new TypeError(`Invalid date: ${JSON.stringify(date)}`);
|
|
553
|
+
const year = date.getUTCFullYear();
|
|
554
|
+
if (year < 1 || year > 9999) throw new RangeError(`Year out of range for an RFC 3339 date: ${year}`);
|
|
555
|
+
return date.toISOString().replace(".000Z", "Z");
|
|
556
|
+
}
|
|
442
557
|
|
|
443
558
|
//#endregion
|
|
444
559
|
//#region src/session.ts
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { combineSignals, createFailedReceipt, parseRetryAfter, readAttachmentContent } from "@upyo/core";
|
|
2
|
+
import { parseMessageId } from "@upyo/core/message-id";
|
|
2
3
|
|
|
3
4
|
//#region src/errors.ts
|
|
4
5
|
/**
|
|
@@ -277,17 +278,44 @@ function getPriorityHeaders(priority) {
|
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
/**
|
|
280
|
-
*
|
|
281
|
+
* Header field names that must not be written as a raw header property.
|
|
282
|
+
*
|
|
283
|
+
* RFC 8621 §4.6 forbids two properties representing the same header field, and
|
|
284
|
+
* the `Email` here already carries a structured property for each of these.
|
|
285
|
+
* `Bcc` matters most: it travels in the submission envelope, so writing it into
|
|
286
|
+
* the message would disclose recipients meant to stay hidden. `Content-*`
|
|
287
|
+
* belongs on an `EmailBodyPart` rather than on the `Email`.
|
|
288
|
+
*/
|
|
289
|
+
const structuredHeaders = new Set([
|
|
290
|
+
"bcc",
|
|
291
|
+
"cc",
|
|
292
|
+
"from",
|
|
293
|
+
"mime-version",
|
|
294
|
+
"reply-to",
|
|
295
|
+
"subject",
|
|
296
|
+
"to"
|
|
297
|
+
]);
|
|
298
|
+
/**
|
|
299
|
+
* Extracts the custom headers of a message, dropping those the `Email` object
|
|
300
|
+
* expresses through a structured property of its own.
|
|
301
|
+
*
|
|
281
302
|
* @param message The message to extract headers from.
|
|
282
303
|
* @returns Array of JMAP headers.
|
|
304
|
+
* @throws {TypeError} If a header value contains a carriage return or line
|
|
305
|
+
* feed, which would forge further header fields.
|
|
283
306
|
* @since 0.4.0
|
|
284
307
|
*/
|
|
285
308
|
function extractCustomHeaders(message) {
|
|
286
309
|
const headers = [];
|
|
287
|
-
for (const [name, value] of message.headers.entries())
|
|
288
|
-
name
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
for (const [name, value] of message.headers.entries()) {
|
|
311
|
+
const lowered = name.toLowerCase();
|
|
312
|
+
if (structuredHeaders.has(lowered) || lowered.startsWith("content-")) continue;
|
|
313
|
+
if (/[\r\n]/.test(value)) throw new TypeError(`Header field ${name} must not contain a carriage return or line feed.`);
|
|
314
|
+
headers.push({
|
|
315
|
+
name,
|
|
316
|
+
value
|
|
317
|
+
});
|
|
318
|
+
}
|
|
291
319
|
return headers;
|
|
292
320
|
}
|
|
293
321
|
/**
|
|
@@ -395,13 +423,21 @@ function buildAttachmentParts(attachments, uploadedBlobs) {
|
|
|
395
423
|
* @param draftMailboxId The mailbox ID to store the draft in.
|
|
396
424
|
* @param uploadedBlobs Map of contentId to blobId for attachments.
|
|
397
425
|
* @returns The JMAP Email/set create object.
|
|
426
|
+
* @throws {TypeError} If the message carries an invalid message identifier or
|
|
427
|
+
* date, or a header value containing a carriage return or line feed.
|
|
428
|
+
* @throws {RangeError} If the year of the date is outside what an RFC 3339
|
|
429
|
+
* date-time can express.
|
|
398
430
|
* @since 0.4.0
|
|
399
431
|
*/
|
|
400
432
|
function convertMessage(message, draftMailboxId, uploadedBlobs) {
|
|
401
433
|
const { bodyStructure, bodyValues } = buildBodyStructure(message, uploadedBlobs);
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
434
|
+
const identity = resolveIdentity(message);
|
|
435
|
+
const headerProperties = {};
|
|
436
|
+
for (const { name, value } of getPriorityHeaders(message.priority)) headerProperties[`header:${name}`] = value;
|
|
437
|
+
for (const { name, value } of extractCustomHeaders(message)) {
|
|
438
|
+
if (identity.owned.has(name.toLowerCase())) continue;
|
|
439
|
+
headerProperties[`header:${name}`] = value;
|
|
440
|
+
}
|
|
405
441
|
const email = {
|
|
406
442
|
mailboxIds: { [draftMailboxId]: true },
|
|
407
443
|
from: [formatAddress(message.sender)],
|
|
@@ -412,10 +448,89 @@ function convertMessage(message, draftMailboxId, uploadedBlobs) {
|
|
|
412
448
|
...message.ccRecipients.length > 0 && { cc: message.ccRecipients.map(formatAddress) },
|
|
413
449
|
...message.bccRecipients.length > 0 && { bcc: message.bccRecipients.map(formatAddress) },
|
|
414
450
|
...message.replyRecipients.length > 0 && { replyTo: message.replyRecipients.map(formatAddress) },
|
|
415
|
-
...
|
|
451
|
+
...identity.properties,
|
|
452
|
+
...headerProperties
|
|
416
453
|
};
|
|
417
454
|
return email;
|
|
418
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Works out the identity and threading properties of an `Email` from the typed
|
|
458
|
+
* fields of a message.
|
|
459
|
+
*
|
|
460
|
+
* A field the message leaves unset is not owned, so a header supplied through
|
|
461
|
+
* {@link Message.headers} still reaches the server, and the server falls back
|
|
462
|
+
* to generating an identifier and a date of its own. A threading field set to
|
|
463
|
+
* an empty list is owned but writes no property, which suppresses such a
|
|
464
|
+
* header.
|
|
465
|
+
*
|
|
466
|
+
* @param message The message being converted.
|
|
467
|
+
* @returns The properties to set, and the field names the message owns.
|
|
468
|
+
* @throws {TypeError} If a message identifier is invalid, or the date is not a
|
|
469
|
+
* valid one.
|
|
470
|
+
* @throws {RangeError} If the year of the date is outside what an RFC 3339
|
|
471
|
+
* date-time can express.
|
|
472
|
+
*/
|
|
473
|
+
function resolveIdentity(message) {
|
|
474
|
+
const owned = /* @__PURE__ */ new Set();
|
|
475
|
+
const properties = {};
|
|
476
|
+
if (message.priority !== "normal") {
|
|
477
|
+
owned.add("x-priority");
|
|
478
|
+
owned.add("importance");
|
|
479
|
+
}
|
|
480
|
+
if (message.messageId != null) {
|
|
481
|
+
owned.add("message-id");
|
|
482
|
+
properties.messageId = [checkMessageId(message.messageId)];
|
|
483
|
+
}
|
|
484
|
+
if (message.date != null) {
|
|
485
|
+
owned.add("date");
|
|
486
|
+
properties.sentAt = formatSentAt(message.date);
|
|
487
|
+
}
|
|
488
|
+
if (message.inReplyTo != null) {
|
|
489
|
+
owned.add("in-reply-to");
|
|
490
|
+
if (message.inReplyTo.length > 0) properties.inReplyTo = message.inReplyTo.map(checkMessageId);
|
|
491
|
+
}
|
|
492
|
+
if (message.references != null) {
|
|
493
|
+
owned.add("references");
|
|
494
|
+
if (message.references.length > 0) properties.references = message.references.map(checkMessageId);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
properties,
|
|
498
|
+
owned
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Checks a message identifier a message carries.
|
|
503
|
+
*
|
|
504
|
+
* `createMessage()` validates these, but `Message` is a structural type that a
|
|
505
|
+
* caller can build without it.
|
|
506
|
+
*
|
|
507
|
+
* @param id The identifier to check.
|
|
508
|
+
* @returns The identifier, unchanged.
|
|
509
|
+
* @throws {TypeError} If the value is not a valid message identifier.
|
|
510
|
+
*/
|
|
511
|
+
function checkMessageId(id) {
|
|
512
|
+
const parsed = parseMessageId(id);
|
|
513
|
+
if (parsed == null) throw new TypeError(`Invalid message ID: ${JSON.stringify(id)}`);
|
|
514
|
+
return parsed;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Formats a date as the RFC 3339 date-time JMAP calls a `Date`.
|
|
518
|
+
*
|
|
519
|
+
* RFC 8620 §1.4 omits a fractional-seconds part that is zero, which
|
|
520
|
+
* `Date.toISOString()` always writes.
|
|
521
|
+
*
|
|
522
|
+
* @param date The date to format.
|
|
523
|
+
* @returns The formatted date.
|
|
524
|
+
* @throws {TypeError} If the date is not a valid one.
|
|
525
|
+
* @throws {RangeError} If its year cannot be written as four digits.
|
|
526
|
+
*/
|
|
527
|
+
function formatSentAt(date) {
|
|
528
|
+
const time = date instanceof Date ? date.getTime() : NaN;
|
|
529
|
+
if (Number.isNaN(time)) throw new TypeError(`Invalid date: ${JSON.stringify(date)}`);
|
|
530
|
+
const year = date.getUTCFullYear();
|
|
531
|
+
if (year < 1 || year > 9999) throw new RangeError(`Year out of range for an RFC 3339 date: ${year}`);
|
|
532
|
+
return date.toISOString().replace(".000Z", "Z");
|
|
533
|
+
}
|
|
419
534
|
|
|
420
535
|
//#endregion
|
|
421
536
|
//#region src/session.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upyo/jmap",
|
|
3
|
-
"version": "0.6.0-dev.
|
|
3
|
+
"version": "0.6.0-dev.319",
|
|
4
4
|
"description": "JMAP transport for Upyo email library",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"email",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"sideEffects": false,
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@upyo/core": "0.6.0-dev.
|
|
56
|
+
"@upyo/core": "0.6.0-dev.319+2324fa8c"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"jmap-rfc-types": "^0.1.2",
|