@rapidmx/web-client 0.36.0 → 0.37.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/apps/shared/calendar/calendarReminders.ts +35 -0
- package/apps/shared/calendar/sealedEvents.ts +42 -9
- package/apps/shared/calendar/useCalendarReminders.ts +28 -9
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +29 -1
- package/apps/shared/components/calendar/EventDetails.tsx +13 -2
- package/apps/shared/components/calendar/EventEditor.tsx +124 -17
- package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
- package/apps/shared/components/calendar/EventModal.tsx +19 -2
- package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
- package/apps/shared/components/calendar/eventForm.ts +2 -0
- package/apps/shared/components/contacts/ContactCard.tsx +13 -1
- package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
- package/apps/shared/components/contacts/ContactForm.tsx +56 -7
- package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
- package/apps/shared/components/contacts/contactCardData.ts +6 -3
- package/apps/shared/components/contacts/contactKeys.ts +3 -3
- package/apps/shared/components/layout/AppShell.tsx +34 -5
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +11 -4
- package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +37 -6
- package/apps/shared/components/mail/InviteCard.tsx +11 -1
- package/apps/shared/components/mail/MessageDetailPane.tsx +25 -14
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
- package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
- package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +11 -1
- package/apps/shared/components/mail/pinnedSigners.ts +14 -0
- package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
- package/apps/shared/components/settings/KeyExportSetting.tsx +4 -1
- package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
- package/apps/shared/components/settings/ZeroAccessSetting.tsx +13 -2
- package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
- package/apps/shared/mail/outbox/sendJob.ts +349 -50
- package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
- package/apps/shared/search/LocalIndexLifecycle.tsx +62 -33
- package/apps/shared/search/localIndexBuilder.ts +35 -24
- package/apps/shared/search/localIndexRpcClient.ts +91 -11
- package/apps/shared/search/localIndexSchema.ts +18 -2
- package/apps/shared/search/localIndexTransport.ts +6 -3
- package/apps/shared/search/localIndexWorker.ts +35 -18
- package/apps/shared/search/searchTier2.ts +5 -0
- package/apps/shared/search/zeroAccessIndexSync.ts +232 -40
- package/apps/shared/search/zeroAccessSyncState.ts +70 -0
- package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
- package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
- package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
- package/apps/www/_shell.tsx +2 -0
- package/apps/www/calendar/index.tsx +49 -5
- package/apps/www/contacts/index.tsx +67 -11
- package/apps/www/index.tsx +10 -1
- package/apps/www/settings/encryption/index.tsx +16 -1
- package/apps/www/settings/privacy/index.tsx +87 -38
- package/apps/www/tasks/index.tsx +24 -6
- package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
- package/dist/apps/shared/calendar/calendarReminders.js +26 -0
- package/dist/apps/shared/calendar/sealedEvents.d.ts +3 -2
- package/dist/apps/shared/calendar/sealedEvents.js +40 -9
- package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
- package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +9 -2
- package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
- package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
- package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
- package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventModal.js +15 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
- package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
- package/dist/apps/shared/components/contacts/contactCardData.js +6 -3
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
- package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
- package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
- package/dist/apps/shared/components/layout/AppShell.js +17 -6
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -4
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +3 -1
- package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +30 -4
- package/dist/apps/shared/components/mail/InviteCard.js +11 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +15 -11
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +11 -1
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
- package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
- package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
- package/dist/apps/shared/components/settings/KeyExportSetting.js +3 -1
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
- package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +2 -0
- package/dist/apps/shared/components/settings/ZeroAccessSetting.js +12 -3
- package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
- package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
- package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
- package/dist/apps/shared/search/LocalIndexLifecycle.js +55 -33
- package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -1
- package/dist/apps/shared/search/localIndexBuilder.js +31 -20
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
- package/dist/apps/shared/search/localIndexRpcClient.js +126 -10
- package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
- package/dist/apps/shared/search/localIndexSchema.js +16 -2
- package/dist/apps/shared/search/localIndexTransport.d.ts +6 -3
- package/dist/apps/shared/search/localIndexWorker.d.ts +9 -0
- package/dist/apps/shared/search/localIndexWorker.js +22 -15
- package/dist/apps/shared/search/searchTier2.js +5 -0
- package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +37 -6
- package/dist/apps/shared/search/zeroAccessIndexSync.js +199 -37
- package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
- package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
- package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
- package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
- package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
- package/dist/apps/www/_shell.d.ts +1 -1
- package/dist/apps/www/_shell.js +2 -2
- package/dist/apps/www/calendar/index.js +47 -5
- package/dist/apps/www/contacts/index.js +64 -11
- package/dist/apps/www/index.js +10 -1
- package/dist/apps/www/settings/encryption/index.js +14 -4
- package/dist/apps/www/settings/privacy/index.js +46 -5
- package/dist/apps/www/tasks/index.js +21 -7
- package/dist/lib/admin/domainsApi.d.ts +1 -1
- package/dist/lib/calendar/calendarApi.d.ts +46 -6
- package/dist/lib/calendar/calendarApi.js +110 -17
- package/dist/lib/calendar/calendarMutations.d.ts +20 -7
- package/dist/lib/calendar/calendarMutations.js +73 -14
- package/dist/lib/calendar/inviteApi.d.ts +17 -3
- package/dist/lib/calendar/inviteApi.js +61 -4
- package/dist/lib/calendar/protectedEvents.d.ts +31 -0
- package/dist/lib/calendar/protectedEvents.js +113 -0
- package/dist/lib/calendar/recurrence.d.ts +2 -0
- package/dist/lib/calendar/recurrence.js +16 -0
- package/dist/lib/calendar/sealedInvites.d.ts +6 -8
- package/dist/lib/calendar/sealedInvites.js +43 -6
- package/dist/lib/contacts/contactsApi.d.ts +29 -3
- package/dist/lib/contacts/contactsApi.js +23 -7
- package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
- package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
- package/dist/lib/crypto/composeSecurity.d.ts +11 -3
- package/dist/lib/crypto/composeSecurity.js +13 -5
- package/dist/lib/crypto/keyExport.js +2 -2
- package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
- package/dist/lib/crypto/keyPairMatch.js +65 -0
- package/dist/lib/crypto/keySession.d.ts +19 -2
- package/dist/lib/crypto/keySession.js +89 -13
- package/dist/lib/crypto/keyvaultApi.d.ts +41 -5
- package/dist/lib/crypto/keyvaultApi.js +13 -3
- package/dist/lib/crypto/meetingKeys.d.ts +25 -0
- package/dist/lib/crypto/meetingKeys.js +87 -0
- package/dist/lib/crypto/messageSecurity.d.ts +26 -2
- package/dist/lib/crypto/messageSecurity.js +122 -10
- package/dist/lib/crypto/openPgpKey.d.ts +39 -0
- package/dist/lib/crypto/openPgpKey.js +89 -0
- package/dist/lib/crypto/openpgp.d.ts +47 -0
- package/dist/lib/crypto/openpgp.js +88 -0
- package/dist/lib/crypto/pgpMessage.d.ts +51 -0
- package/dist/lib/crypto/pgpMessage.js +271 -0
- package/dist/lib/crypto/sharedMailboxKeys.d.ts +15 -3
- package/dist/lib/crypto/sharedMailboxKeys.js +102 -29
- package/dist/lib/crypto/smimeMessage.d.ts +21 -0
- package/dist/lib/crypto/smimeMessage.js +11 -7
- package/dist/lib/crypto/zeroAccessKey.js +5 -0
- package/dist/lib/mail/mailApi.d.ts +33 -0
- package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
- package/dist/lib/mail/mailboxImportApi.js +15 -0
- package/dist/lib/search/zeroAccessIndexApi.d.ts +4 -0
- package/dist/lib/tasks/tasksApi.d.ts +11 -1
- package/dist/lib/tasks/tasksApi.js +10 -4
- package/dist/lib/util/clientPlatform.d.ts +14 -0
- package/dist/lib/util/clientPlatform.js +26 -0
- package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
- package/dist/lib/videoconf/protectedMeetings.js +181 -0
- package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
- package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
- package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
- package/dist/lib/zeroAccess/sealedMoves.js +63 -0
- package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
- package/dist/lib/zeroAccess/sealedRecords.js +136 -0
- package/lib/admin/domainsApi.ts +1 -1
- package/lib/calendar/calendarApi.ts +140 -17
- package/lib/calendar/calendarMutations.ts +79 -12
- package/lib/calendar/inviteApi.ts +75 -5
- package/lib/calendar/protectedEvents.ts +143 -0
- package/lib/calendar/recurrence.ts +17 -0
- package/lib/calendar/sealedInvites.ts +47 -7
- package/lib/contacts/contactsApi.ts +43 -17
- package/lib/contacts/useContactPhotoSrc.ts +103 -13
- package/lib/crypto/composeSecurity.ts +20 -5
- package/lib/crypto/keyExport.ts +2 -2
- package/lib/crypto/keyPairMatch.ts +69 -0
- package/lib/crypto/keySession.ts +102 -12
- package/lib/crypto/keyvaultApi.ts +49 -7
- package/lib/crypto/meetingKeys.ts +118 -0
- package/lib/crypto/messageSecurity.ts +174 -13
- package/lib/crypto/openPgpKey.ts +118 -0
- package/lib/crypto/openpgp.ts +115 -0
- package/lib/crypto/pgpMessage.ts +336 -0
- package/lib/crypto/sharedMailboxKeys.ts +106 -28
- package/lib/crypto/smimeMessage.ts +11 -7
- package/lib/crypto/zeroAccessKey.ts +5 -0
- package/lib/mail/mailApi.ts +27 -0
- package/lib/mail/mailboxImportApi.ts +24 -0
- package/lib/search/zeroAccessIndexApi.ts +4 -0
- package/lib/tasks/tasksApi.ts +19 -5
- package/lib/util/clientPlatform.ts +50 -0
- package/lib/videoconf/protectedMeetings.ts +221 -0
- package/lib/videoconf/videoMeetingsApi.ts +15 -4
- package/lib/zeroAccess/sealedMoves.ts +79 -0
- package/lib/zeroAccess/sealedRecords.ts +153 -0
- package/package.json +12 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
6
|
-
import { assembleDraft, assembleDraftRaw, forgetDraftImages, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "../../../../lib/mail/mailApi.js";
|
|
6
|
+
import { assembleDraft, assembleDraftRaw, createDraft, deleteMessage, forgetDraftImages, getMailbox, getMessage, queueMessageSend, sendMessage, setMessageRequestReceipt, } from "../../../../lib/mail/mailApi.js";
|
|
7
7
|
import { describeSendFailure } from "../../../../lib/mail/sendFailure.js";
|
|
8
8
|
import { resolveRecipientEncryption } from "../../../../lib/crypto/composeSecurity.js";
|
|
9
9
|
import { getUnlockedKeys } from "../../../../lib/crypto/keySession.js";
|
|
@@ -16,6 +16,7 @@ import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingS
|
|
|
16
16
|
import { failOutgoing, forgetOutgoing, markOutgoingSending, markOutgoingSent, trackOutgoing } from "./outgoingReplies.js";
|
|
17
17
|
import { openComposeFromOutside, requestUnlockFromOutside } from "./composeBridge.js";
|
|
18
18
|
import { sendState } from "./sendState.js";
|
|
19
|
+
import { notifySendFailure } from "./sendOutcomes.js";
|
|
19
20
|
/** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
|
|
20
21
|
export const CONTEXT_GRACE_MS = 4000;
|
|
21
22
|
export function registerOutboxCounter(tracker, onQueued) {
|
|
@@ -29,19 +30,69 @@ export function registerOutboxCounter(tracker, onQueued) {
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
const RETAINED_LIMIT = 25;
|
|
32
|
-
/** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server
|
|
33
|
+
/** The request a message was queued from, while this tab still holds it: what lets "Open draft" and "Retry" work on a failure the server
|
|
34
|
+
* reports later. A message sent as several copies (`SendRequest.copyUids`) is held under each copy still to go, from just before it is handed
|
|
35
|
+
* over - how the first went can be reported while the next is still being handed over. */
|
|
33
36
|
export function retainedRequest(draftUid) {
|
|
34
37
|
return sendState.retained.get(draftUid);
|
|
35
38
|
}
|
|
36
|
-
/**
|
|
39
|
+
/** Stops holding the request of a message sent as several copies under copy `uid` - never another message's request held there since. */
|
|
40
|
+
function release(fates, uid) {
|
|
41
|
+
if (sendState.retained.get(uid)?.copyFates === fates) {
|
|
42
|
+
sendState.retained.delete(uid);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Records that copy `uid` of a message sent as several (`fates`) has gone. Whether it was the last of them to go, each counted once, so the
|
|
46
|
+
* message is confirmed now - always, for a message sent as one. */
|
|
47
|
+
function copyWent(fates, uid) {
|
|
48
|
+
if (!fates) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
release(fates, uid);
|
|
52
|
+
if (fates.get(uid) === "sent") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
fates.set(uid, "sent");
|
|
56
|
+
return [...fates.values()].every((fate) => fate === "sent");
|
|
57
|
+
}
|
|
58
|
+
/** Drops the request a message (or one of its copies) was queued from, and the pictures remembered as attached to its draft: it has gone, so
|
|
59
|
+
* neither is wanted again. Whether the message has now gone in full - the last of its copies to go. */
|
|
37
60
|
export function forgetRetainedRequest(draftUid) {
|
|
61
|
+
const request = sendState.retained.get(draftUid);
|
|
38
62
|
sendState.retained.delete(draftUid);
|
|
39
63
|
forgetDraftImages(draftUid);
|
|
64
|
+
return copyWent(request?.copyFates, draftUid);
|
|
65
|
+
}
|
|
66
|
+
/** Records that the server couldn't relay copy `uid` of the message `request` sent as several copies (nothing for a message sent as one). */
|
|
67
|
+
export function copyFailed(request, uid) {
|
|
68
|
+
request?.copyFates?.set(uid, "failed");
|
|
69
|
+
}
|
|
70
|
+
/** Copy `uid` of the message `request` sent as several copies is handed over again (a Retry): how it goes is to be reported once more, and
|
|
71
|
+
* finds the request held under it. Nothing for a message sent as one. */
|
|
72
|
+
export function copyRetried(request, uid) {
|
|
73
|
+
if (request?.copyFates) {
|
|
74
|
+
request.copyFates.set(uid, "pending");
|
|
75
|
+
retain(request, [uid]);
|
|
76
|
+
}
|
|
40
77
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
/** Every copy of the message `request` sent as several, when none of them has gone nor may still go - each failed, or was never handed over:
|
|
79
|
+
* nobody has the message. `undefined` while one has gone or may, and for a message sent as one. */
|
|
80
|
+
export function unsentCopies(request) {
|
|
81
|
+
const fates = request?.copyFates;
|
|
82
|
+
return fates && [...fates.values()].every((fate) => fate === "failed" || fate === "unsent") ? [...fates.keys()] : undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Stops holding a message sent as several copies under any of them: it is being written again. */
|
|
85
|
+
export function forgetCopies(fates) {
|
|
86
|
+
for (const uid of fates.keys()) {
|
|
87
|
+
release(fates, uid);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function retain(request, uids) {
|
|
91
|
+
for (const uid of uids) {
|
|
92
|
+
sendState.retained.delete(uid);
|
|
93
|
+
sendState.retained.set(uid, request);
|
|
94
|
+
}
|
|
95
|
+
while (sendState.retained.size > RETAINED_LIMIT) {
|
|
45
96
|
sendState.retained.delete(sendState.retained.keys().next().value);
|
|
46
97
|
}
|
|
47
98
|
}
|
|
@@ -56,6 +107,11 @@ export function readSendKeys(mailboxUid, signEnabled, mailbox) {
|
|
|
56
107
|
canEncryptSelf: !!unlocked?.encryptionPrivateKey && !!unlocked.encryptionCertDer,
|
|
57
108
|
};
|
|
58
109
|
}
|
|
110
|
+
/** Whether the sender can make an OpenPGP copy (it is encrypted to them too, with their own OpenPGP key) - or can't be told yet, the
|
|
111
|
+
* mailbox being locked: `decideSend()` then asks for the unlock rather than settling for another format, or none. */
|
|
112
|
+
export function senderCanUseOpenPgp(unlocked) {
|
|
113
|
+
return !unlocked || !!unlocked.openPgpKeys?.length;
|
|
114
|
+
}
|
|
59
115
|
/** Whether this sender could encrypt at all: an encryption key unlocked here, enrolled on the mailbox, or offered earlier this session. */
|
|
60
116
|
export function senderHasEncryptionKey(request, canEncryptSelf) {
|
|
61
117
|
return canEncryptSelf || request.offeredEncrypt || !!findActivePublicKey(request.mailbox?.keys ?? [], "encrypt");
|
|
@@ -102,11 +158,15 @@ async function completeContext(request, client) {
|
|
|
102
158
|
}
|
|
103
159
|
return { mailbox, policy };
|
|
104
160
|
}
|
|
105
|
-
/** Builds the message's final content into the draft (plain, signed, or signed and encrypted in this browser) and returns the stored
|
|
106
|
-
|
|
161
|
+
/** Builds the message's final content into the draft (plain, signed, or signed and encrypted in this browser) and returns the stored
|
|
162
|
+
* message(s) to send: one, or - for an encrypted message whose recipients need both formats (restapi `specs/end-to-end_encryption.md`
|
|
163
|
+
* *OpenPGP Interoperability*) - an S/MIME copy (this draft) and an OpenPGP copy (a second draft), each listing every recipient but
|
|
164
|
+
* delivered only to the recipients of its own format (`deliverTo`). Never a plaintext copy. The second draft is added to `created` as soon as
|
|
165
|
+
* it exists, so an attempt that fails before handing it over can remove it. */
|
|
166
|
+
async function assemble(request, mailbox, mode, statuses, withSignature, client, created) {
|
|
107
167
|
const { draft, to, cc, bcc, subject, html } = request;
|
|
108
168
|
if (mode === "plain") {
|
|
109
|
-
return assembleDraft(draft.uid, { to, cc, bcc, subject, html, intent: "send" }, client);
|
|
169
|
+
return [await assembleDraft(draft.uid, { to, cc, bcc, subject, html, intent: "send" }, client)];
|
|
110
170
|
}
|
|
111
171
|
const keys = readSendKeys(request.mailboxUid, request.signEnabled, mailbox);
|
|
112
172
|
const own = mailbox;
|
|
@@ -127,31 +187,185 @@ async function assemble(request, mailbox, mode, statuses, withSignature, client)
|
|
|
127
187
|
const { applyBaselineOuterHeaders, assembleOutboundMime, buildEncryptedMessage, buildSignedOnlyMessage } = await import("../../../../lib/crypto/smimeMessage.js");
|
|
128
188
|
const encrypt = mode === "encrypt";
|
|
129
189
|
const outerHeaders = encrypt ? applyBaselineOuterHeaders(protectedHeaders) : protectedHeaders;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
190
|
+
if (!encrypt) {
|
|
191
|
+
const signedPart = await buildSignedOnlyMessage(bodyContentType, html, protectedHeaders, signing.certDer, signing.privateKey);
|
|
192
|
+
return [await assembleDraftRaw(draft.uid, { to, cc, bcc, subject: outerHeaders.subject, rawMime: assembleOutboundMime(outerHeaders, signedPart), intent: "send" }, client)];
|
|
193
|
+
}
|
|
194
|
+
const smime = statuses.filter((status) => status.format !== "openpgp");
|
|
195
|
+
const pgp = statuses.filter((status) => status.format === "openpgp");
|
|
196
|
+
const deliverTo = (group) => (smime.length > 0 && pgp.length > 0 ? { deliverTo: group.map((status) => status.address) } : {});
|
|
197
|
+
const sent = [];
|
|
198
|
+
if (smime.length > 0) {
|
|
199
|
+
const part = await buildEncryptedMessage(bodyContentType, html, protectedHeaders, outerHeaders, [keys.unlocked.encryptionCertDer, ...smime.map((status) => fromBase64(status.encryptCert.publicKey))], signing);
|
|
200
|
+
const rawMime = assembleOutboundMime(outerHeaders, part);
|
|
201
|
+
sent.push(await assembleDraftRaw(draft.uid, { to, cc, bcc, subject: outerHeaders.subject, rawMime, intent: "send", ...deliverTo(smime) }, client));
|
|
202
|
+
}
|
|
203
|
+
if (pgp.length > 0) {
|
|
204
|
+
// OpenPGP.js and the PGP/MIME builder load only for a message that has OpenPGP recipients.
|
|
205
|
+
const [{ buildPgpEncryptedMessage, readKeysBase64 }, { armorPublicKey }] = await Promise.all([
|
|
206
|
+
import("../../../../lib/crypto/pgpMessage.js"),
|
|
207
|
+
import("../../../../lib/crypto/openpgp.js"),
|
|
208
|
+
]);
|
|
209
|
+
const ownKey = keys.unlocked.openPgpKeys[0].privateKey;
|
|
210
|
+
const recipientKeys = await readKeysBase64(pgp.map((status) => status.openPgpKey.publicKey));
|
|
211
|
+
// Its own Message-ID: a second message, even though it is the same one for its readers.
|
|
212
|
+
const pgpHeaders = { ...protectedHeaders, messageId: `<${crypto.randomUUID()}@${domain}>` };
|
|
213
|
+
const pgpOuter = applyBaselineOuterHeaders(pgpHeaders);
|
|
214
|
+
// Signed with the sender's OpenPGP key (what an OpenPGP reader can check) whenever the message is signed, and carrying the
|
|
215
|
+
// sender's public key, so a correspondent without it can verify and reply encrypted.
|
|
216
|
+
const part = await buildPgpEncryptedMessage(bodyContentType, html, pgpHeaders, pgpOuter, [ownKey.toPublic(), ...recipientKeys], withSignature ? ownKey : undefined, armorPublicKey(ownKey.toPublic()));
|
|
217
|
+
const rawMime = assembleOutboundMime(pgpOuter, part);
|
|
218
|
+
let target = draft;
|
|
219
|
+
if (smime.length > 0) {
|
|
220
|
+
target = await createDraft(request.mailboxUid, draft.folderUid, undefined, client);
|
|
221
|
+
created.push(target);
|
|
222
|
+
}
|
|
223
|
+
sent.push(await assembleDraftRaw(target.uid, { to, cc, bcc, subject: pgpOuter.subject, rawMime, intent: "send", ...deliverTo(pgp) }, client));
|
|
224
|
+
}
|
|
225
|
+
return sent;
|
|
135
226
|
}
|
|
136
227
|
function failureOf(err, fallback) {
|
|
137
228
|
const failure = describeSendFailure(err, fallback);
|
|
138
229
|
return { message: failure.message, details: failure.lines };
|
|
139
230
|
}
|
|
140
|
-
/** Whether the server
|
|
141
|
-
|
|
231
|
+
/** Whether `fresh`, `copy` as the server holds it now, was accepted (out of Drafts, or leased). */
|
|
232
|
+
function isAccepted(copy, fresh) {
|
|
233
|
+
return fresh.folderUid !== copy.folderUid || !!fresh.scheduledSendLeaseExpiresAt || !!fresh.scheduledSendRelayedAt;
|
|
234
|
+
}
|
|
235
|
+
/** Whether the server already holds `copy` as accepted - the answer to "did that request that never returned actually land?". */
|
|
236
|
+
async function accepted(copy, client) {
|
|
142
237
|
try {
|
|
143
|
-
|
|
144
|
-
return fresh.folderUid !== request.draft.folderUid || !!fresh.scheduledSendLeaseExpiresAt || !!fresh.scheduledSendRelayedAt;
|
|
238
|
+
return isAccepted(copy, await getMessage(copy.uid, client));
|
|
145
239
|
}
|
|
146
240
|
catch {
|
|
147
241
|
return false;
|
|
148
242
|
}
|
|
149
243
|
}
|
|
244
|
+
/** Deletes the draft of a copy of a message sent as several (the OpenPGP one's) that is not to be handed over any more - unless the server
|
|
245
|
+
* holds it as accepted after all. Best effort: never rejects. */
|
|
246
|
+
async function discardCopy(copy, client) {
|
|
247
|
+
try {
|
|
248
|
+
const fresh = await getMessage(copy.uid, client);
|
|
249
|
+
if (!isAccepted(copy, fresh)) {
|
|
250
|
+
await deleteMessage(fresh.uid, fresh.version, client);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Left in Drafts, where it can be deleted by hand.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/** Whether the server already holds every copy of `request`'s message still to go (its draft, when none was built yet) as accepted. */
|
|
258
|
+
async function alreadyAccepted(request, client) {
|
|
259
|
+
for (const copy of request.pendingCopies ?? [request.draft]) {
|
|
260
|
+
if (!(await accepted(copy, client))) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/** Hands `copies` over to the server - queued, or scheduled - one after the other. One that fails stops the rest: the failure carries the
|
|
267
|
+
* copies still to go, so a retry never sends one twice (an encrypted message's other copy may already be on its way to its recipients). */
|
|
268
|
+
async function handOver(request, copies, client) {
|
|
269
|
+
// A retry hands over only what was left: the request already names every copy.
|
|
270
|
+
const copyUids = request.copyUids ?? (copies.length > 1 ? copies.map((copy) => copy.uid) : undefined);
|
|
271
|
+
const copyFates = request.copyFates ?? (copyUids && new Map(copyUids.map((uid) => [uid, "unsent"])));
|
|
272
|
+
// An immediate send of several copies is held under each before any is handed over: how the first went can be reported while the next
|
|
273
|
+
// is still on its way to the server, and must find the others - a failure that offered "Open draft" would send their recipients the
|
|
274
|
+
// message again, a success not counted would leave the message never confirmed.
|
|
275
|
+
if (copyFates && !request.scheduledSendTime) {
|
|
276
|
+
for (const copy of copies) {
|
|
277
|
+
copyFates.set(copy.uid, "pending");
|
|
278
|
+
}
|
|
279
|
+
retain({ ...request, copyUids, copyFates }, copies.map((copy) => copy.uid));
|
|
280
|
+
}
|
|
281
|
+
let queued = false;
|
|
282
|
+
let completed = false;
|
|
283
|
+
for (let index = 0; index < copies.length; index++) {
|
|
284
|
+
const copy = copies[index];
|
|
285
|
+
try {
|
|
286
|
+
if (request.scheduledSendTime) {
|
|
287
|
+
await sendMessage(copy.uid, { scheduledSendTime: request.scheduledSendTime }, client);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const handed = await queueMessageSend(copy.uid, client);
|
|
291
|
+
queued = handed.queued || queued;
|
|
292
|
+
// A server with no queue relayed it before answering: one of several copies has gone already.
|
|
293
|
+
if (!handed.queued && copyFates) {
|
|
294
|
+
completed = copyWent(copyFates, copy.uid) || completed;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
// A request that never came back may still have landed: never offer to send it twice if it did.
|
|
300
|
+
if (!(err instanceof ApiRequestError) && (await accepted(copy, client))) {
|
|
301
|
+
queued = true;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const failure = failureOf(err, request.scheduledSendTime ? "Could not schedule this message." : "Could not send this message.");
|
|
305
|
+
// This copy and the ones after it weren't handed over: nothing of them is to be reported.
|
|
306
|
+
if (copyFates) {
|
|
307
|
+
for (const left of copies.slice(index)) {
|
|
308
|
+
copyFates.set(left.uid, "unsent");
|
|
309
|
+
release(copyFates, left.uid);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// The server has failed a copy handed over earlier (its report came while this one was on its way), and none is sent or on its
|
|
313
|
+
// way: nobody has the message - the whole message's failure, not some recipients'.
|
|
314
|
+
const fates = [...(copyFates?.values() ?? [])];
|
|
315
|
+
const nobodyHasIt = fates.includes("failed") && fates.every((fate) => fate === "failed" || fate === "unsent");
|
|
316
|
+
// A copy that went in this attempt, or in an earlier one this retries.
|
|
317
|
+
const partial = !nobodyHasIt && (index > 0 || !!request.partial);
|
|
318
|
+
return {
|
|
319
|
+
kind: "failed",
|
|
320
|
+
failure: partial
|
|
321
|
+
? { ...failure, partial, message: `Only some of its recipients ${request.scheduledSendTime ? "will get it" : "were sent it"} - the copy for the others didn't go. ${failure.message}` }
|
|
322
|
+
: failure,
|
|
323
|
+
// Nothing went of a single copy: a retry decides and assembles again from the request, like the first attempt (the stored
|
|
324
|
+
// draft may have been opened and autosaved plain since). Several copies are kept even when none went, so a mixed-format
|
|
325
|
+
// retry doesn't create another OpenPGP draft.
|
|
326
|
+
pendingCopies: partial || copies.length > 1 ? copies.slice(index) : undefined,
|
|
327
|
+
copyUids,
|
|
328
|
+
copyFates,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (request.scheduledSendTime) {
|
|
333
|
+
return { kind: "scheduled", copyUids, copyFates };
|
|
334
|
+
}
|
|
335
|
+
// Several copies: the message has gone only when this hand-over saw the last of them go - otherwise the server's report for the last copy
|
|
336
|
+
// to go confirms it.
|
|
337
|
+
return { kind: (copyFates ? completed : !queued) ? "sent" : "queued", copyUids, copyFates };
|
|
338
|
+
}
|
|
150
339
|
/** The whole client-side path of one send: settle, decide, assemble, hand over. Never rejects. `client` is the explicit `ApiClient` (from
|
|
151
340
|
* `useApiClient()`) the window that built `request` was rendered under, `undefined` for the default global cookie-based session. */
|
|
152
341
|
async function attempt(request, client) {
|
|
342
|
+
// The copies' drafts `assemble()` created (the OpenPGP one's): an attempt that fails before handing them over deletes them - its Retry
|
|
343
|
+
// assembles the message again, creating its own, and one left behind would reach its recipients a second time if sent from Drafts.
|
|
344
|
+
const created = [];
|
|
153
345
|
try {
|
|
154
346
|
await request.saved?.catch(() => undefined);
|
|
347
|
+
// Built already by the attempt that failed part-way: only what didn't go is handed over. A copy whose request seemed to fail but
|
|
348
|
+
// landed (its answer was lost, and so was the check after it) has gone since: handed over again it is refused, and the copies after
|
|
349
|
+
// it would never go - it is left out, and what fails now is a failure of only some of the recipients.
|
|
350
|
+
if (request.pendingCopies?.length) {
|
|
351
|
+
setPendingStage(request.draft.uid, "queuing");
|
|
352
|
+
const remaining = [];
|
|
353
|
+
let completed = false;
|
|
354
|
+
for (const copy of request.pendingCopies) {
|
|
355
|
+
if (!(await accepted(copy, client))) {
|
|
356
|
+
remaining.push(copy);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// How it went has most likely been reported already, to nothing waiting for it: it isn't waited for now.
|
|
360
|
+
completed = copyWent(request.copyFates, copy.uid) || completed;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (remaining.length === 0) {
|
|
364
|
+
const { copyUids, copyFates } = request;
|
|
365
|
+
return { kind: request.scheduledSendTime ? "scheduled" : completed ? "sent" : "queued", copyUids, copyFates };
|
|
366
|
+
}
|
|
367
|
+
return await handOver(remaining.length < request.pendingCopies.length ? { ...request, partial: true } : request, remaining, client);
|
|
368
|
+
}
|
|
155
369
|
const all = [...request.to, ...request.cc, ...request.bcc];
|
|
156
370
|
let mailbox = request.mailbox;
|
|
157
371
|
let policy = request.policy;
|
|
@@ -166,9 +380,16 @@ async function attempt(request, client) {
|
|
|
166
380
|
const own = mailbox;
|
|
167
381
|
const ownPrefersMutual = own.encryptPreference?.preferEncrypt === "mutual";
|
|
168
382
|
const lookups = await Promise.all(all.map((recipient) => lookupKeys(request.mailboxUid, recipient.address, client).then((lookup) => ({ lookup }), () => null)));
|
|
383
|
+
const ownOpenPgp = senderCanUseOpenPgp(keys.unlocked);
|
|
169
384
|
statuses = all.map((recipient, index) => {
|
|
170
385
|
const found = lookups[index];
|
|
171
|
-
|
|
386
|
+
// Without an OpenPGP key of the sender's, a recipient with both formats gets S/MIME, whatever they prefer.
|
|
387
|
+
const status = found
|
|
388
|
+
? resolveRecipientEncryption(own.primarySmtpAddress, ownPrefersMutual, policy ?? OPEN_POLICY, recipient.address, found.lookup, ownOpenPgp)
|
|
389
|
+
: undefined;
|
|
390
|
+
// An OpenPGP copy is encrypted to the sender too, with their own OpenPGP key: a recipient with only an OpenPGP key can't
|
|
391
|
+
// be encrypted to while the unlocked session has none.
|
|
392
|
+
return status?.format === "openpgp" && !ownOpenPgp ? { ...status, canEncrypt: false, autoEncrypt: false } : status;
|
|
172
393
|
});
|
|
173
394
|
}
|
|
174
395
|
lookupsComplete = true;
|
|
@@ -180,26 +401,17 @@ async function attempt(request, client) {
|
|
|
180
401
|
if (decision.action === "rejected") {
|
|
181
402
|
return { kind: "failed", failure: { message: decision.message, details: [] } };
|
|
182
403
|
}
|
|
183
|
-
const assembled = await assemble(request, mailbox, decision.action, statuses, decision.action === "sign" || (decision.action === "encrypt" && decision.sign), client);
|
|
184
|
-
const
|
|
404
|
+
const assembled = await assemble(request, mailbox, decision.action, statuses, decision.action === "sign" || (decision.action === "encrypt" && decision.sign), client, created);
|
|
405
|
+
const copies = request.requestReceipt ? await Promise.all(assembled.map((copy) => setMessageRequestReceipt(copy, true, client))) : assembled;
|
|
185
406
|
setPendingStage(request.draft.uid, "queuing");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return { kind: "scheduled" };
|
|
190
|
-
}
|
|
191
|
-
const result = await queueMessageSend(withReceipt.uid, client);
|
|
192
|
-
return { kind: result.queued ? "queued" : "sent" };
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
// A request that never came back may still have landed: never offer to send it twice if it did.
|
|
196
|
-
if (!(err instanceof ApiRequestError) && (await alreadyAccepted(request, client))) {
|
|
197
|
-
return { kind: request.scheduledSendTime ? "scheduled" : "queued" };
|
|
198
|
-
}
|
|
199
|
-
throw err;
|
|
200
|
-
}
|
|
407
|
+
// Handed over from here: a copy the hand-over doesn't deliver is its failure's to retry.
|
|
408
|
+
created.length = 0;
|
|
409
|
+
return await handOver(request, copies, client);
|
|
201
410
|
}
|
|
202
411
|
catch (err) {
|
|
412
|
+
for (const copy of created) {
|
|
413
|
+
await discardCopy(copy, client);
|
|
414
|
+
}
|
|
203
415
|
return { kind: "failed", failure: failureOf(err, request.scheduledSendTime ? "Could not schedule this message." : "Could not send this message.") };
|
|
204
416
|
}
|
|
205
417
|
}
|
|
@@ -229,18 +441,42 @@ export function openDraftFromRequest(request) {
|
|
|
229
441
|
encryptRequested: request.encryptRequested,
|
|
230
442
|
},
|
|
231
443
|
});
|
|
232
|
-
// Being edited again, it is no longer a message on its way in the conversation
|
|
444
|
+
// Being edited again, it is no longer a message on its way in the conversation - and the failure's Retry, wherever it was clicked
|
|
445
|
+
// from, must not hand over the draft the window is now editing (and autosaving).
|
|
233
446
|
if (opened) {
|
|
234
447
|
forgetOutgoing(request.draft.uid);
|
|
448
|
+
dismiss(failureNotificationId(request.draft.uid));
|
|
235
449
|
}
|
|
236
450
|
return opened;
|
|
237
451
|
}
|
|
238
452
|
function failureNotificationId(draftUid) {
|
|
239
453
|
return `send-failed:${draftUid}`;
|
|
240
454
|
}
|
|
455
|
+
/** "Open draft" for a message sent as several copies none of which went, each still a draft (`unsentCopies()`): the message is written again in
|
|
456
|
+
* its own draft, and the other copies' drafts (the OpenPGP one's) are deleted, best effort - it is encrypted afresh when it is sent again,
|
|
457
|
+
* and a copy left in Drafts would reach its recipients a second time if ever sent from there. */
|
|
458
|
+
function openDraftDroppingCopies(request, client) {
|
|
459
|
+
if (!openDraftFromRequest(request)) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
forgetCopies(request.copyFates);
|
|
463
|
+
for (const copy of request.pendingCopies.filter((copy) => copy.uid !== request.draft.uid)) {
|
|
464
|
+
forgetOutgoing(copy.uid);
|
|
465
|
+
void discardCopy(copy, client);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
241
468
|
/** The sticky "This message wasn't sent" pop-up for a send that failed on this side, with what can be done about it. */
|
|
242
469
|
function notifyFailure(request, failure, client) {
|
|
243
470
|
const uid = request.draft.uid;
|
|
471
|
+
// A message sent as several copies, none of which went: each was never handed over, or the server failed it (its report came while the
|
|
472
|
+
// next was being handed over). Then it is the whole message's failure as the server reports one: one pop-up, replacing the one its report
|
|
473
|
+
// showed, whose Retry hands every copy over again - the failed one from Outbox too - and whose "Open draft" writes the message again.
|
|
474
|
+
const copies = unsentCopies(request);
|
|
475
|
+
if (copies && [...request.copyFates.values()].includes("failed")) {
|
|
476
|
+
const recipients = [...request.to, ...request.cc, ...request.bcc].map((recipient) => recipient.address);
|
|
477
|
+
notifySendFailure({ uid, mailboxUid: request.mailboxUid, subject: request.subject, recipients }, { message: failure.message, lines: failure.details }, request, client);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
244
480
|
const actions = [];
|
|
245
481
|
const { block } = failure;
|
|
246
482
|
if (block?.keysLocked && request.mailbox?.keys?.length) {
|
|
@@ -257,7 +493,10 @@ function notifyFailure(request, failure, client) {
|
|
|
257
493
|
else {
|
|
258
494
|
actions.push({ label: "Retry", onClick: () => void retrySend(request, client) });
|
|
259
495
|
}
|
|
260
|
-
|
|
496
|
+
// Part of it went: editing and sending the draft again would send those recipients a second copy.
|
|
497
|
+
if (!failure.partial) {
|
|
498
|
+
actions.push({ label: "Open draft", onClick: () => void (copies ? openDraftDroppingCopies(request, client) : openDraftFromRequest(request)) });
|
|
499
|
+
}
|
|
261
500
|
notify({
|
|
262
501
|
id: failureNotificationId(uid),
|
|
263
502
|
kind: "error",
|
|
@@ -301,16 +540,22 @@ export function startSend(request, client) {
|
|
|
301
540
|
finishPendingSend(uid);
|
|
302
541
|
if (outcome.kind === "failed") {
|
|
303
542
|
tracker?.revert();
|
|
304
|
-
notifyFailure(
|
|
543
|
+
notifyFailure(outcome.pendingCopies
|
|
544
|
+
? { ...request, pendingCopies: outcome.pendingCopies, partial: outcome.failure.partial, copyUids: outcome.copyUids, copyFates: outcome.copyFates }
|
|
545
|
+
: request, outcome.failure, client);
|
|
305
546
|
return;
|
|
306
547
|
}
|
|
307
548
|
tracker?.settle();
|
|
308
549
|
if (outcome.kind === "queued") {
|
|
309
|
-
|
|
550
|
+
// A message sent as several copies is held under each already, from before it was handed over.
|
|
551
|
+
if (!outcome.copyFates) {
|
|
552
|
+
retain(request, [uid]);
|
|
553
|
+
}
|
|
310
554
|
sendState.queuedListener?.(request.mailboxUid);
|
|
311
555
|
}
|
|
312
556
|
else if (outcome.kind === "sent") {
|
|
313
|
-
// A server with no queue relayed it before answering
|
|
557
|
+
// A server with no queue relayed it before answering (or, of several copies, the last of them went in this attempt): the Sent
|
|
558
|
+
// Items copy exists already.
|
|
314
559
|
markOutgoingSent(uid);
|
|
315
560
|
forgetDraftImages(uid);
|
|
316
561
|
notifySent();
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { SendEvent } from "../../../../lib/mail/sendEvents.js";
|
|
2
2
|
import type { ApiClient } from "../../../../lib/util/api.js";
|
|
3
|
+
import { SendRequest } from "./sendJob.js";
|
|
4
|
+
/** The message a failure is about: as the server reports it (a `SendEvent`), or as this tab sent it. */
|
|
5
|
+
export type FailedSend = Pick<SendEvent, "uid" | "mailboxUid" | "subject" | "recipients">;
|
|
3
6
|
/** Shows what a send event means to the user. `client` is the explicit `ApiClient` (from `useApiClient()`) whichever code observed this push
|
|
4
7
|
* event was rendered under - `undefined` (the default) for every existing caller, which keeps using the default global cookie-based session
|
|
5
8
|
* exactly as before. */
|
|
6
9
|
export declare function handleSendEvent(event: SendEvent, client?: ApiClient): void;
|
|
10
|
+
/**
|
|
11
|
+
* The sticky "This message wasn't sent" for a message that failed, `failure` saying why, with what can be done about it - for one the server
|
|
12
|
+
* failed to relay, and for a message sent as several copies (`SendRequest.copyUids`) of which this tab couldn't hand over the rest after the
|
|
13
|
+
* server had failed one (`sendJob.ts`'s `notifyFailure()`). `request`: the one it was queued from, when this tab holds it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function notifySendFailure(event: FailedSend, failure: {
|
|
16
|
+
message: string;
|
|
17
|
+
lines: string[];
|
|
18
|
+
}, request: SendRequest | undefined, client: ApiClient | undefined): void;
|