@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.
Files changed (227) hide show
  1. package/apps/shared/calendar/calendarReminders.ts +35 -0
  2. package/apps/shared/calendar/sealedEvents.ts +42 -9
  3. package/apps/shared/calendar/useCalendarReminders.ts +28 -9
  4. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
  5. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +29 -1
  6. package/apps/shared/components/calendar/EventDetails.tsx +13 -2
  7. package/apps/shared/components/calendar/EventEditor.tsx +124 -17
  8. package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
  9. package/apps/shared/components/calendar/EventModal.tsx +19 -2
  10. package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
  11. package/apps/shared/components/calendar/eventForm.ts +2 -0
  12. package/apps/shared/components/contacts/ContactCard.tsx +13 -1
  13. package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
  14. package/apps/shared/components/contacts/ContactForm.tsx +56 -7
  15. package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
  16. package/apps/shared/components/contacts/contactCardData.ts +6 -3
  17. package/apps/shared/components/contacts/contactKeys.ts +3 -3
  18. package/apps/shared/components/layout/AppShell.tsx +34 -5
  19. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +11 -4
  20. package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +37 -6
  21. package/apps/shared/components/mail/InviteCard.tsx +11 -1
  22. package/apps/shared/components/mail/MessageDetailPane.tsx +25 -14
  23. package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
  24. package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
  25. package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
  26. package/apps/shared/components/mail/invite/inviteStore.ts +11 -1
  27. package/apps/shared/components/mail/pinnedSigners.ts +14 -0
  28. package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
  29. package/apps/shared/components/settings/KeyExportSetting.tsx +4 -1
  30. package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
  31. package/apps/shared/components/settings/ZeroAccessSetting.tsx +13 -2
  32. package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
  33. package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
  34. package/apps/shared/mail/outbox/sendJob.ts +349 -50
  35. package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
  36. package/apps/shared/search/LocalIndexLifecycle.tsx +62 -33
  37. package/apps/shared/search/localIndexBuilder.ts +35 -24
  38. package/apps/shared/search/localIndexRpcClient.ts +91 -11
  39. package/apps/shared/search/localIndexSchema.ts +18 -2
  40. package/apps/shared/search/localIndexTransport.ts +6 -3
  41. package/apps/shared/search/localIndexWorker.ts +35 -18
  42. package/apps/shared/search/searchTier2.ts +5 -0
  43. package/apps/shared/search/zeroAccessIndexSync.ts +232 -40
  44. package/apps/shared/search/zeroAccessSyncState.ts +70 -0
  45. package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
  46. package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
  47. package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
  48. package/apps/www/_shell.tsx +2 -0
  49. package/apps/www/calendar/index.tsx +49 -5
  50. package/apps/www/contacts/index.tsx +67 -11
  51. package/apps/www/index.tsx +10 -1
  52. package/apps/www/settings/encryption/index.tsx +16 -1
  53. package/apps/www/settings/privacy/index.tsx +87 -38
  54. package/apps/www/tasks/index.tsx +24 -6
  55. package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
  56. package/dist/apps/shared/calendar/calendarReminders.js +26 -0
  57. package/dist/apps/shared/calendar/sealedEvents.d.ts +3 -2
  58. package/dist/apps/shared/calendar/sealedEvents.js +40 -9
  59. package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
  60. package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
  61. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
  62. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +9 -2
  63. package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
  64. package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
  65. package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
  66. package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
  67. package/dist/apps/shared/components/calendar/EventModal.js +15 -2
  68. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
  69. package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
  70. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
  71. package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
  72. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
  73. package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
  74. package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
  75. package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
  76. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
  77. package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
  78. package/dist/apps/shared/components/contacts/contactCardData.js +6 -3
  79. package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
  80. package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
  81. package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
  82. package/dist/apps/shared/components/layout/AppShell.js +17 -6
  83. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -4
  84. package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +3 -1
  85. package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +30 -4
  86. package/dist/apps/shared/components/mail/InviteCard.js +11 -1
  87. package/dist/apps/shared/components/mail/MessageDetailPane.js +15 -11
  88. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
  89. package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
  90. package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
  91. package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
  92. package/dist/apps/shared/components/mail/invite/inviteStore.js +11 -1
  93. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
  94. package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
  95. package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
  96. package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
  97. package/dist/apps/shared/components/settings/KeyExportSetting.js +3 -1
  98. package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
  99. package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
  100. package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +2 -0
  101. package/dist/apps/shared/components/settings/ZeroAccessSetting.js +12 -3
  102. package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
  103. package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
  104. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
  105. package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
  106. package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
  107. package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
  108. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
  109. package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
  110. package/dist/apps/shared/search/LocalIndexLifecycle.js +55 -33
  111. package/dist/apps/shared/search/localIndexBuilder.d.ts +4 -1
  112. package/dist/apps/shared/search/localIndexBuilder.js +31 -20
  113. package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
  114. package/dist/apps/shared/search/localIndexRpcClient.js +126 -10
  115. package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
  116. package/dist/apps/shared/search/localIndexSchema.js +16 -2
  117. package/dist/apps/shared/search/localIndexTransport.d.ts +6 -3
  118. package/dist/apps/shared/search/localIndexWorker.d.ts +9 -0
  119. package/dist/apps/shared/search/localIndexWorker.js +22 -15
  120. package/dist/apps/shared/search/searchTier2.js +5 -0
  121. package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +37 -6
  122. package/dist/apps/shared/search/zeroAccessIndexSync.js +199 -37
  123. package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
  124. package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
  125. package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
  126. package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
  127. package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
  128. package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
  129. package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
  130. package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
  131. package/dist/apps/www/_shell.d.ts +1 -1
  132. package/dist/apps/www/_shell.js +2 -2
  133. package/dist/apps/www/calendar/index.js +47 -5
  134. package/dist/apps/www/contacts/index.js +64 -11
  135. package/dist/apps/www/index.js +10 -1
  136. package/dist/apps/www/settings/encryption/index.js +14 -4
  137. package/dist/apps/www/settings/privacy/index.js +46 -5
  138. package/dist/apps/www/tasks/index.js +21 -7
  139. package/dist/lib/admin/domainsApi.d.ts +1 -1
  140. package/dist/lib/calendar/calendarApi.d.ts +46 -6
  141. package/dist/lib/calendar/calendarApi.js +110 -17
  142. package/dist/lib/calendar/calendarMutations.d.ts +20 -7
  143. package/dist/lib/calendar/calendarMutations.js +73 -14
  144. package/dist/lib/calendar/inviteApi.d.ts +17 -3
  145. package/dist/lib/calendar/inviteApi.js +61 -4
  146. package/dist/lib/calendar/protectedEvents.d.ts +31 -0
  147. package/dist/lib/calendar/protectedEvents.js +113 -0
  148. package/dist/lib/calendar/recurrence.d.ts +2 -0
  149. package/dist/lib/calendar/recurrence.js +16 -0
  150. package/dist/lib/calendar/sealedInvites.d.ts +6 -8
  151. package/dist/lib/calendar/sealedInvites.js +43 -6
  152. package/dist/lib/contacts/contactsApi.d.ts +29 -3
  153. package/dist/lib/contacts/contactsApi.js +23 -7
  154. package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
  155. package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
  156. package/dist/lib/crypto/composeSecurity.d.ts +11 -3
  157. package/dist/lib/crypto/composeSecurity.js +13 -5
  158. package/dist/lib/crypto/keyExport.js +2 -2
  159. package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
  160. package/dist/lib/crypto/keyPairMatch.js +65 -0
  161. package/dist/lib/crypto/keySession.d.ts +19 -2
  162. package/dist/lib/crypto/keySession.js +89 -13
  163. package/dist/lib/crypto/keyvaultApi.d.ts +41 -5
  164. package/dist/lib/crypto/keyvaultApi.js +13 -3
  165. package/dist/lib/crypto/meetingKeys.d.ts +25 -0
  166. package/dist/lib/crypto/meetingKeys.js +87 -0
  167. package/dist/lib/crypto/messageSecurity.d.ts +26 -2
  168. package/dist/lib/crypto/messageSecurity.js +122 -10
  169. package/dist/lib/crypto/openPgpKey.d.ts +39 -0
  170. package/dist/lib/crypto/openPgpKey.js +89 -0
  171. package/dist/lib/crypto/openpgp.d.ts +47 -0
  172. package/dist/lib/crypto/openpgp.js +88 -0
  173. package/dist/lib/crypto/pgpMessage.d.ts +51 -0
  174. package/dist/lib/crypto/pgpMessage.js +271 -0
  175. package/dist/lib/crypto/sharedMailboxKeys.d.ts +15 -3
  176. package/dist/lib/crypto/sharedMailboxKeys.js +102 -29
  177. package/dist/lib/crypto/smimeMessage.d.ts +21 -0
  178. package/dist/lib/crypto/smimeMessage.js +11 -7
  179. package/dist/lib/crypto/zeroAccessKey.js +5 -0
  180. package/dist/lib/mail/mailApi.d.ts +33 -0
  181. package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
  182. package/dist/lib/mail/mailboxImportApi.js +15 -0
  183. package/dist/lib/search/zeroAccessIndexApi.d.ts +4 -0
  184. package/dist/lib/tasks/tasksApi.d.ts +11 -1
  185. package/dist/lib/tasks/tasksApi.js +10 -4
  186. package/dist/lib/util/clientPlatform.d.ts +14 -0
  187. package/dist/lib/util/clientPlatform.js +26 -0
  188. package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
  189. package/dist/lib/videoconf/protectedMeetings.js +181 -0
  190. package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
  191. package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
  192. package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
  193. package/dist/lib/zeroAccess/sealedMoves.js +63 -0
  194. package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
  195. package/dist/lib/zeroAccess/sealedRecords.js +136 -0
  196. package/lib/admin/domainsApi.ts +1 -1
  197. package/lib/calendar/calendarApi.ts +140 -17
  198. package/lib/calendar/calendarMutations.ts +79 -12
  199. package/lib/calendar/inviteApi.ts +75 -5
  200. package/lib/calendar/protectedEvents.ts +143 -0
  201. package/lib/calendar/recurrence.ts +17 -0
  202. package/lib/calendar/sealedInvites.ts +47 -7
  203. package/lib/contacts/contactsApi.ts +43 -17
  204. package/lib/contacts/useContactPhotoSrc.ts +103 -13
  205. package/lib/crypto/composeSecurity.ts +20 -5
  206. package/lib/crypto/keyExport.ts +2 -2
  207. package/lib/crypto/keyPairMatch.ts +69 -0
  208. package/lib/crypto/keySession.ts +102 -12
  209. package/lib/crypto/keyvaultApi.ts +49 -7
  210. package/lib/crypto/meetingKeys.ts +118 -0
  211. package/lib/crypto/messageSecurity.ts +174 -13
  212. package/lib/crypto/openPgpKey.ts +118 -0
  213. package/lib/crypto/openpgp.ts +115 -0
  214. package/lib/crypto/pgpMessage.ts +336 -0
  215. package/lib/crypto/sharedMailboxKeys.ts +106 -28
  216. package/lib/crypto/smimeMessage.ts +11 -7
  217. package/lib/crypto/zeroAccessKey.ts +5 -0
  218. package/lib/mail/mailApi.ts +27 -0
  219. package/lib/mail/mailboxImportApi.ts +24 -0
  220. package/lib/search/zeroAccessIndexApi.ts +4 -0
  221. package/lib/tasks/tasksApi.ts +19 -5
  222. package/lib/util/clientPlatform.ts +50 -0
  223. package/lib/videoconf/protectedMeetings.ts +221 -0
  224. package/lib/videoconf/videoMeetingsApi.ts +15 -4
  225. package/lib/zeroAccess/sealedMoves.ts +79 -0
  226. package/lib/zeroAccess/sealedRecords.ts +153 -0
  227. package/package.json +12 -3
@@ -5,7 +5,9 @@
5
5
  /** Pure helpers behind `useCalendarReminders.ts` - kept apart from the hook so the push-payload parsing, the snooze
6
6
  * arithmetic and the pop-up's wording are each testable without a React tree or a socket. */
7
7
 
8
+ import { getCalendarEvent } from "../../../lib/calendar/calendarApi.js";
8
9
  import type { PushEvent } from "../../../lib/mail/pushClient.js";
10
+ import { openSealedEvent } from "./sealedEvents.js";
9
11
 
10
12
  /** How long "Snooze" waits before the alarm comes back. */
11
13
  export const SNOOZE_MS = 5 * 60_000;
@@ -19,6 +21,39 @@ export interface CalendarReminderNotice {
19
21
  /** The event's own `location`, verbatim - `undefined`/`null` when the event has none. Not necessarily a URL;
20
22
  * see `joinMeetingUrl()` for when it's shown as a join link. */
21
23
  location?: string | null;
24
+ /** The event is of a calendar zero-access encryption protects, or was made from a zero-access invitation: `title` and `location` are
25
+ * placeholders (`openedReminder()`). */
26
+ protected?: boolean;
27
+ /** Set by `openedReminder()` on a protected notice it opened: the placeholder title the push carried, for anything recorded beyond
28
+ * this tab's memory (the notification history is kept in `sessionStorage` and outlives a mailbox lock). */
29
+ sealedTitle?: string;
30
+ /** Set with `sealedTitle`: the placeholder location the push carried, which `sealedReminder()` puts back. */
31
+ sealedLocation?: string | null;
32
+ }
33
+
34
+ /** `notice` with a protected event's real title and location, read from the event itself (opened with the mailbox's keys); `notice` as it
35
+ * came when the event can't be read or opened - the alarm still goes off, with the placeholder. Never rejects. */
36
+ export async function openedReminder(notice: CalendarReminderNotice): Promise<CalendarReminderNotice> {
37
+ if (!notice.protected) {
38
+ return notice;
39
+ }
40
+ try {
41
+ // `getCalendarEvent()` opens sealed fields; an event made from a zero-access invitation keeps its content in `sealedContent`.
42
+ const event = await openSealedEvent(await getCalendarEvent(notice.eventUid));
43
+ return { ...notice, title: event.title, location: event.location ?? null, sealedTitle: notice.title, sealedLocation: notice.location ?? null };
44
+ } catch {
45
+ return notice;
46
+ }
47
+ }
48
+
49
+ /** `notice` as the push carried it, when `openedReminder()` opened it - for an alarm still shown, or snoozed, when the mailbox locks: its real
50
+ * title and location go with the keys that opened them. `openedReminder()` opens it again (it is still `protected`). */
51
+ export function sealedReminder(notice: CalendarReminderNotice): CalendarReminderNotice {
52
+ if (notice.sealedTitle === undefined) {
53
+ return notice;
54
+ }
55
+ const { sealedTitle, sealedLocation, ...rest } = notice;
56
+ return { ...rest, title: sealedTitle, location: sealedLocation ?? null };
22
57
  }
23
58
 
24
59
  function isReminderNotice(data: unknown): data is CalendarReminderNotice {
@@ -9,8 +9,9 @@
9
9
  * decryption code is loaded only when a sealed event is actually seen.
10
10
  */
11
11
  import type { CalendarEvent } from "../../../lib/calendar/calendarApi.js";
12
- import { getUnlockedKeys } from "../../../lib/crypto/keySession.js";
12
+ import { getUnlockedKeys, subscribeKeySession } from "../../../lib/crypto/keySession.js";
13
13
  import { fromBase64 } from "../../../lib/crypto/encoding.js";
14
+ import { markSealedContentOpened } from "../../../lib/calendar/protectedEvents.js";
14
15
  import { onSigningOut } from "../components/mail/compose/composeFlushRegistry.js";
15
16
 
16
17
  /** What `sealedContent.sealed` decrypts to. */
@@ -24,31 +25,55 @@ interface SealedEventFields {
24
25
  /** Opened fields by sealed text - the same sealed content always opens to the same fields. Cleared with `clearSealedEvents()`. */
25
26
  const opened: Map<string, Partial<CalendarEvent>> = new Map();
26
27
 
27
- /** Forgets every opened event (sign-out, or the test seam). */
28
+ /** Forgets every opened event (a mailbox locking, sign-out, or the test seam). */
28
29
  export function clearSealedEvents(): void {
29
30
  opened.clear();
30
31
  }
31
32
 
33
+ let watchingKeys = false;
34
+
35
+ /** Opened fields go when keys are locked, so nothing opened outlives its key session. Subscribed when the first sealed event is seen,
36
+ * not on import. */
37
+ function forgetOnLock(): void {
38
+ if (watchingKeys) {
39
+ return;
40
+ }
41
+ watchingKeys = true;
42
+ subscribeKeySession((event) => {
43
+ if (event.state === "locked") {
44
+ clearSealedEvents();
45
+ }
46
+ });
47
+ }
48
+
32
49
  async function openFields(event: CalendarEvent): Promise<Partial<CalendarEvent> | undefined> {
33
50
  const sealed: string = event.sealedContent!.sealed;
34
- const known = opened.get(sealed);
35
- if (known) {
36
- return known;
37
- }
51
+ // Checked before the cache: a locked mailbox's events show their placeholders, whatever was opened before.
38
52
  const keys = getUnlockedKeys(event.mailboxUid)?.zeroAccessKeys;
39
53
  if (!keys?.length) {
40
54
  return undefined;
41
55
  }
56
+ const known = opened.get(sealed);
57
+ if (known) {
58
+ return known;
59
+ }
42
60
  try {
43
61
  const { decryptEnvelopedDataWithKeys } = await import("../../../lib/crypto/smime.js");
44
62
  const fields = JSON.parse(new TextDecoder().decode(await decryptEnvelopedDataWithKeys(fromBase64(sealed), keys))) as SealedEventFields;
45
63
  const text = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
46
64
  const result: Partial<CalendarEvent> = {
47
- title: text(fields.title) ?? event.title,
65
+ // An empty sealed title is no title: a newer revision without SUMMARY over a row that keeps its title sealed field by field
66
+ // (`sealedFields.title`) is sealed with an empty one, and the title opened from that copy (or the row's own) shows instead.
67
+ // Left out here, not copied from `event`, as the cache is shared by every row with this sealed content.
68
+ ...(text(fields.title) ? { title: text(fields.title) } : {}),
48
69
  location: text(fields.location),
49
70
  description: text(fields.description) ?? null,
50
71
  descriptionHtml: text(fields.descriptionHtml) ?? null,
51
72
  };
73
+ // The mailbox locked while this was opened: the lock's clear has already run, so it is neither kept nor shown.
74
+ if (!getUnlockedKeys(event.mailboxUid)) {
75
+ return undefined;
76
+ }
52
77
  opened.set(sealed, result);
53
78
  return result;
54
79
  } catch {
@@ -56,18 +81,26 @@ async function openFields(event: CalendarEvent): Promise<Partial<CalendarEvent>
56
81
  }
57
82
  }
58
83
 
59
- /** `events` with every sealed one this device can open shown with its real title, location and description. Never rejects. */
84
+ /** `events` with every sealed one this device can open shown with its real title, location and description (and marked opened,
85
+ * `isSealedContentOpened()`). Never rejects. */
60
86
  export async function openSealedEvents(events: CalendarEvent[]): Promise<CalendarEvent[]> {
61
87
  if (!events.some((event) => event.sealedContent)) {
62
88
  return events;
63
89
  }
90
+ forgetOnLock();
64
91
  return Promise.all(
65
92
  events.map(async (event) => {
66
93
  if (!event.sealedContent) {
67
94
  return event;
68
95
  }
69
96
  const fields = await openFields(event);
70
- return fields ? { ...event, ...fields } : event;
97
+ if (!fields) {
98
+ return event;
99
+ }
100
+ // Marked, so a reply or change request names it by its real title (`invitationContentOf()`). An empty sealed title with no
101
+ // per-field copy to show instead (an invitation without SUMMARY) is an untitled event: opened, it never shows the row's `[...]`.
102
+ const untitled = fields.title === undefined && !event.sealedFields?.title ? { title: "" } : {};
103
+ return markSealedContentOpened({ ...event, ...fields, ...untitled });
71
104
  }),
72
105
  );
73
106
  }
@@ -7,7 +7,8 @@ import { getPushClient } from "../../../lib/mail/pushClient.js";
7
7
  import { getNotificationsEnabled } from "../notifications/preferences.js";
8
8
  import { playNotificationSound } from "../notifications/sounds.js";
9
9
  import { notify } from "../notifications/store.js";
10
- import { CalendarReminderNotice, SNOOZE_MS, calendarReminderOf, reminderMessage, reminderNotificationId } from "./calendarReminders.js";
10
+ import { useKeysLocked } from "../zeroAccess/useKeysLocked.js";
11
+ import { CalendarReminderNotice, SNOOZE_MS, calendarReminderOf, openedReminder, reminderMessage, reminderNotificationId, sealedReminder } from "./calendarReminders.js";
11
12
 
12
13
  export interface UseCalendarRemindersOptions {
13
14
  /** Nothing is shown without a signed-in user. */
@@ -40,19 +41,26 @@ export interface CalendarAlarms {
40
41
  * with the bell again - after exactly `SNOOZE_MS`. A snooze is this tab's own `setTimeout`, cleared on unmount or sign-out; like the alarm itself (the
41
42
  * push event is never replayed), it does not survive the tab closing.
42
43
  *
43
- * With pop-ups turned off (`getNotificationsEnabled()`) no dialog is shown and nothing is heard: the alarm is only recorded in "Recent notifications".
44
+ * With pop-ups turned off (`getNotificationsEnabled()`) no dialog is shown and nothing is heard: the alarm is only recorded in "Recent notifications"
45
+ * - a protected event's under its placeholder title, never the opened one (the history is kept in `sessionStorage`, past a lock).
44
46
  */
45
47
  export function useCalendarReminders({ userUid, enabled }: UseCalendarRemindersOptions): CalendarAlarms {
46
48
  const [queue, setQueue] = useState<CalendarReminderNotice[]>([]);
47
49
  // The queue as of the last render, for the callbacks below to read (a state updater must not start a timer: React may run it twice).
48
50
  const queueRef = useRef(queue);
49
51
  queueRef.current = queue;
50
- const timers = useRef(new Set<ReturnType<typeof setTimeout>>());
52
+ // Each snoozed alarm by its timer: what it shows when it comes back (a lock meanwhile puts back its placeholders).
53
+ const timers = useRef(new Map<ReturnType<typeof setTimeout>, CalendarReminderNotice>());
51
54
 
52
55
  const present = useCallback((notice: CalendarReminderNotice) => {
53
56
  if (!getNotificationsEnabled()) {
54
- // Off: only the history keeps it.
55
- notify({ id: reminderNotificationId(notice.eventUid, notice.startDate), kind: "calendar", title: notice.title, message: reminderMessage(notice.startDate) });
57
+ // Off: only the history keeps it - with a protected event's placeholder title, since the history is stored in sessionStorage.
58
+ notify({
59
+ id: reminderNotificationId(notice.eventUid, notice.startDate),
60
+ kind: "calendar",
61
+ title: notice.sealedTitle ?? notice.title,
62
+ message: reminderMessage(notice.startDate),
63
+ });
56
64
  return;
57
65
  }
58
66
  const id = reminderNotificationId(notice.eventUid, notice.startDate);
@@ -67,13 +75,13 @@ export function useCalendarReminders({ userUid, enabled }: UseCalendarRemindersO
67
75
  const offEvent = getPushClient().onEvent((event) => {
68
76
  const notice = calendarReminderOf(event);
69
77
  if (notice) {
70
- present(notice);
78
+ void openedReminder(notice).then(present);
71
79
  }
72
80
  });
73
81
  const pending = timers.current;
74
82
  return () => {
75
83
  offEvent();
76
- for (const timer of pending) {
84
+ for (const timer of pending.keys()) {
77
85
  clearTimeout(timer);
78
86
  }
79
87
  pending.clear();
@@ -81,15 +89,26 @@ export function useCalendarReminders({ userUid, enabled }: UseCalendarRemindersO
81
89
  };
82
90
  }, [enabled, userUid, present]);
83
91
 
92
+ // A protected event's alarm shows its title and location opened with the mailbox's keys: on a lock - a notice doesn't say which mailbox,
93
+ // so any - the alarms shown or snoozed go back to the placeholders the push carried.
94
+ useKeysLocked(() => {
95
+ setQueue((current) => current.map(sealedReminder));
96
+ for (const [timer, notice] of timers.current) {
97
+ timers.current.set(timer, sealedReminder(notice));
98
+ }
99
+ });
100
+
84
101
  const dismiss = useCallback(() => setQueue((current) => current.slice(1)), []);
85
102
  const snooze = useCallback(() => {
86
103
  const first = queueRef.current[0];
87
104
  if (first) {
88
105
  const timer = setTimeout(() => {
106
+ const notice = timers.current.get(timer)!;
89
107
  timers.current.delete(timer);
90
- present(first);
108
+ // Put back to its placeholders by a lock: opened again if the mailbox is unlocked now.
109
+ void (notice.protected && notice.sealedTitle === undefined ? openedReminder(notice) : Promise.resolve(notice)).then(present);
91
110
  }, SNOOZE_MS);
92
- timers.current.add(timer);
111
+ timers.current.set(timer, first);
93
112
  }
94
113
  setQueue((current) => current.slice(1));
95
114
  }, [present]);
@@ -15,6 +15,7 @@ const RECORD_TYPE_LABELS: Record<DnsRecordCheck["type"], string> = {
15
15
  spf: "SPF",
16
16
  dkim: "DKIM",
17
17
  dmarc: "DMARC",
18
+ openpgpkey_cname: "OpenPGP keys (CNAME)",
18
19
  autodiscover_cname: "Autodiscover (CNAME)",
19
20
  autodiscover_srv: "Autodiscover (SRV)",
20
21
  };
@@ -26,6 +27,7 @@ const RECORD_TYPE_NAMES: Record<DnsRecordCheck["type"], string> = {
26
27
  spf: "SPF",
27
28
  dkim: "DKIM",
28
29
  dmarc: "DMARC",
30
+ openpgpkey_cname: "OpenPGP keys CNAME",
29
31
  autodiscover_cname: "Autodiscover CNAME",
30
32
  autodiscover_srv: "Autodiscover SRV",
31
33
  };
@@ -33,6 +35,8 @@ const RECORD_TYPE_NAMES: Record<DnsRecordCheck["type"], string> = {
33
35
  /** A one-line "why this matters", shown under a checklist row's label - only for record types unfamiliar
34
36
  * enough to need one; the well-known mail records (ownership/MX/SPF/DKIM/DMARC) don't. */
35
37
  const RECORD_TYPE_HELP: Partial<Record<DnsRecordCheck["type"], string>> = {
38
+ openpgpkey_cname:
39
+ "So Proton Mail and other OpenPGP apps can find your users' OpenPGP keys (Web Key Directory) and send them encrypted mail. This hostname also needs to be covered by this server's TLS certificate.",
36
40
  autodiscover_cname:
37
41
  "So EAS and Outlook clients can find this server automatically from just an email address. This hostname also needs to be covered by this server's TLS certificate.",
38
42
  autodiscover_srv:
@@ -56,8 +56,12 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
56
56
  const [savedValues, setSavedValues] = useState<EncryptionPolicy>(policy);
57
57
  // A server without zero-access encryption sends no `allowZeroAccess`; the checkbox is then not shown at all.
58
58
  const zeroAccessSupported: boolean = policy.allowZeroAccess !== undefined;
59
+ // Likewise `allowOpenPgp`, from a server without OpenPGP support.
60
+ const openPgpSupported: boolean = policy.allowOpenPgp !== undefined;
59
61
  const dirty: boolean =
60
- TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]) || (zeroAccessSupported && values.allowZeroAccess !== savedValues.allowZeroAccess);
62
+ TIERS.some((tier) => values[tier.key] !== savedValues[tier.key]) ||
63
+ (zeroAccessSupported && values.allowZeroAccess !== savedValues.allowZeroAccess) ||
64
+ (openPgpSupported && values.allowOpenPgp !== savedValues.allowOpenPgp);
61
65
  useEffect(() => onDirtyChange?.(dirty), [dirty]);
62
66
 
63
67
  // Tiers this save would newly set to "Never encrypt", which switches encryption off for everyone's mail of that kind: confirmed first.
@@ -164,6 +168,30 @@ export default function EncryptionPolicyForm({ policy, onChange, onDirtyChange,
164
168
  </label>
165
169
  </div>
166
170
  )}
171
+ {openPgpSupported && (
172
+ <div className="mt-2 pt-4 border-t border-border">
173
+ <span className="font-semibold text-sm block mb-1">OpenPGP</span>
174
+ <label className="flex items-start gap-2 text-sm">
175
+ <input
176
+ type="checkbox"
177
+ className="mt-1"
178
+ checked={values.allowOpenPgp !== false}
179
+ onChange={(e) => {
180
+ setSaved(false);
181
+ setValues({ ...values, allowOpenPgp: e.target.checked });
182
+ }}
183
+ />
184
+ <span>
185
+ <span className="font-semibold block">Allow OpenPGP</span>
186
+ <span className="text-xs text-text-muted">
187
+ Gives every mailbox an OpenPGP key and publishes it, so people using Proton Mail, Thunderbird and other
188
+ OpenPGP apps can send encrypted mail to your users, and your users to them. RapidMX still uses S/MIME
189
+ whenever a recipient can receive it. Off, no OpenPGP keys are created or published.
190
+ </span>
191
+ </span>
192
+ </label>
193
+ </div>
194
+ )}
167
195
  <div>
168
196
  <Button type="submit" loading={saving} disabled={saving} className="!w-auto">
169
197
  Save
@@ -18,10 +18,13 @@ import {
18
18
  import { ApiRequestError } from "../../../../lib/util/api.js";
19
19
  import { useApiClient } from "../../../../lib/util/apiClientContext.js";
20
20
  import { AttendeeResponseInput, guestPermissionsOf, respondToEvent, visibilityOf } from "../../../../lib/calendar/calendarApi.js";
21
+ import { invitationContentOf } from "../../../../lib/calendar/protectedEvents.js";
22
+ import { organizerJoinLink } from "../../../../lib/videoconf/protectedMeetings.js";
21
23
  import { htmlToPlainText } from "../../../../lib/calendar/eventDescription.js";
22
24
  import { deleteEventOccurrence, deleteEventSeries } from "../../../../lib/calendar/calendarMutations.js";
23
25
  import { CalendarOccurrence, describeRecurrence } from "../../../../lib/calendar/recurrence.js";
24
26
  import { getVideoMeeting } from "../../../../lib/videoconf/videoMeetingsApi.js";
27
+ import { useKeysLocked } from "../../zeroAccess/useKeysLocked.js";
25
28
  import { describeTimeZone, deviceTimeZone } from "../../../../lib/util/timeZone.js";
26
29
  import Alert from "../../../../lib/components/feedback/Alert.js";
27
30
  import Button from "../../../../lib/components/buttons/Button.js";
@@ -109,7 +112,7 @@ export default function EventDetails({
109
112
  }
110
113
  let cancelled = false;
111
114
  void getVideoMeeting(videoMeetingUid, client)
112
- .then((meeting) => meeting.organizerJoinUrl ?? null)
115
+ .then(organizerJoinLink)
113
116
  .catch(() => null)
114
117
  .then((url) => {
115
118
  if (!cancelled) {
@@ -120,6 +123,13 @@ export default function EventDetails({
120
123
  cancelled = true;
121
124
  };
122
125
  }, [videoMeetingUid, client]);
126
+ // A protected meeting's link carries its key after `#`, opened with the mailbox's keys: it goes when they lock, whether or not the
127
+ // event itself is protected (a meeting is keyed whenever the mailbox has zero-access, its calendar protected or not).
128
+ useKeysLocked((locked) => {
129
+ if (locked === occurrence.mailboxUid) {
130
+ setOrganizerJoinUrl((url) => (url?.includes("#") ? null : url));
131
+ }
132
+ });
123
133
  // The same scheme allow-list a reminder's own location gets: a hostile or malformed join link never reaches `window.open()`.
124
134
  const joinUrl = joinMeetingUrl(organizerJoinUrl);
125
135
 
@@ -146,7 +156,7 @@ export default function EventDetails({
146
156
  setError(null);
147
157
  setBusy(true);
148
158
  try {
149
- await respondToEvent(occurrence.uid, responseStatus, client);
159
+ await respondToEvent(occurrence.uid, responseStatus, client, occurrence);
150
160
  // A decline soft-deletes the mailbox's own copy server-side - treat it the same as a
151
161
  // delete rather than a save so the calendar view drops it immediately.
152
162
  if (responseStatus === "declined") {
@@ -356,6 +366,7 @@ export default function EventDetails({
356
366
  description: occurrence.description,
357
367
  descriptionHtml: occurrence.descriptionHtml,
358
368
  guestAddresses: occurrence.attendees.map((a) => a.address),
369
+ invitationContent: invitationContentOf(occurrence),
359
370
  }}
360
371
  canChange={canChange}
361
372
  canInvite={canInvite}