1 import type { Dispatch, SetStateAction } from 'react';
2 import { useCallback, useEffect, useMemo, useState } from 'react';
4 import { c, msgid } from 'ttag';
6 import { useGetEncryptionPreferences, useKeyTransparencyContext } from '@proton/components';
7 import { useModalTwo } from '@proton/components/components/modalTwo/useModalTwo';
8 import type { PublicKeyReference } from '@proton/crypto';
9 import useIsMounted from '@proton/hooks/useIsMounted';
10 import { processApiRequestsSafe } from '@proton/shared/lib/api/helpers/safeApiRequests';
11 import { validateEmailAddress } from '@proton/shared/lib/helpers/email';
12 import { omit } from '@proton/shared/lib/helpers/object';
13 import type { KeyTransparencyActivation } from '@proton/shared/lib/interfaces';
14 import type { Recipient } from '@proton/shared/lib/interfaces/Address';
15 import type { ContactEmail } from '@proton/shared/lib/interfaces/contacts';
16 import type { GetEncryptionPreferences } from '@proton/shared/lib/interfaces/hooks/GetEncryptionPreferences';
17 import { ENCRYPTION_PREFERENCES_ERROR_TYPES } from '@proton/shared/lib/mail/encryptionPreferences';
18 import { getRecipientsAddresses } from '@proton/shared/lib/mail/messages';
19 import getSendPreferences from '@proton/shared/lib/mail/send/getSendPreferences';
20 import isTruthy from '@proton/utils/isTruthy';
21 import noop from '@proton/utils/noop';
23 import AskForKeyPinningModal from '../components/composer/addresses/AskForKeyPinningModal';
24 import ContactResignModal from '../components/message/modals/ContactResignModal';
25 import { getSendStatusIcon } from '../helpers/message/icon';
26 import type { MapSendInfo } from '../models/crypto';
27 import { STATUS_ICONS_FILLS } from '../models/crypto';
28 import type { ContactsMap } from '../store/contacts/contactsTypes';
29 import type { MessageState } from '../store/messages/messagesTypes';
30 import { useContactsMap } from './contact/useContacts';
32 const { PRIMARY_NOT_PINNED, CONTACT_SIGNATURE_NOT_VERIFIED } = ENCRYPTION_PREFERENCES_ERROR_TYPES;
34 const getSignText = (n: number, contactNames: string, contactAddresses: string) => {
35 return c('Info').ngettext(
36 msgid`The verification of ${contactNames} has failed: the contact is not signed correctly.
37 This may be the result of a password reset.
38 You must re-sign the contact in order to send a message to ${contactAddresses} or edit the contact.`,
39 `The verification of ${contactNames} has failed: the contacts are not signed correctly.
40 This may be the result of a password reset.
41 You must re-sign the contacts in order to send a message to ${contactAddresses} or edit the contacts.`,
46 export interface MessageSendInfo {
47 message: MessageState;
48 mapSendInfo: MapSendInfo;
49 setMapSendInfo: Dispatch<SetStateAction<MapSendInfo>>;
52 export const useMessageSendInfo = (message: MessageState) => {
53 const isMounted = useIsMounted();
54 // Map of send preferences and send icons for each recipient
55 const [mapSendInfo, setMapSendInfo] = useState<MapSendInfo>({});
56 const safeSetMapSendInfo = (value: SetStateAction<MapSendInfo>) => isMounted() && setMapSendInfo(value);
58 // Use memo is ok there but not really effective as any message change will update the ref
59 const messageSendInfo: MessageSendInfo = useMemo(
63 setMapSendInfo: safeSetMapSendInfo,
65 [message, mapSendInfo]
68 return messageSendInfo;
71 export const useUpdateRecipientSendInfo = (
72 messageSendInfo: MessageSendInfo | undefined,
76 const getEncryptionPreferences = useGetEncryptionPreferences();
77 const contactsMap = useContactsMap();
78 const emailAddress = recipient.Address;
79 const { ktActivation } = useKeyTransparencyContext();
81 const [askForKeyPinningModal, handleShowAskForKeyPinningModal] = useModalTwo(AskForKeyPinningModal);
83 const [contactResignModal, handleContactResignModal] = useModalTwo(ContactResignModal);
85 const handleRemove = () => {
86 if (messageSendInfo) {
87 const { setMapSendInfo } = messageSendInfo;
88 setMapSendInfo((mapSendInfo) => omit(mapSendInfo, [emailAddress]));
94 const updateRecipientIcon = async (): Promise<void> => {
95 // Inactive if no send info or data already present
96 if (!messageSendInfo || messageSendInfo.mapSendInfo[emailAddress]) {
99 const { message, setMapSendInfo } = messageSendInfo;
100 const emailValidation = validateEmailAddress(emailAddress);
102 // Prevent sending request if email is not even valid
103 if (!emailValidation) {
104 setMapSendInfo((mapSendInfo) => ({
107 encryptionPreferenceError: ENCRYPTION_PREFERENCES_ERROR_TYPES.EMAIL_ADDRESS_ERROR,
108 sendPreferences: undefined,
110 colorClassName: 'color-danger',
112 fill: STATUS_ICONS_FILLS.FAIL,
113 text: c('Composer email icon').t`The address might be misspelled`,
117 emailAddressWarnings: [],
118 contactSignatureInfo: undefined,
123 setMapSendInfo((mapSendInfo) => ({
130 const encryptionPreferences = await getEncryptionPreferences({
133 contactEmailsMap: contactsMap,
135 const sendPreferences = getSendPreferences(encryptionPreferences, message.data);
137 if (sendPreferences.error?.type === CONTACT_SIGNATURE_NOT_VERIFIED) {
138 if (!recipient.ContactID) {
141 const contact = { contactID: recipient.ContactID };
143 const contactAddress = recipient.Address;
144 const contactName = recipient.Name || contactAddress;
146 const text = getSignText(1, contactName, contactAddress);
148 await handleContactResignModal({
149 title: c('Title').t`Re-sign contact`,
151 onNotResign: onRemove,
152 onError: handleRemove,
156 return updateRecipientIcon();
159 if (sendPreferences.error?.type === PRIMARY_NOT_PINNED) {
160 if (!recipient.ContactID) {
165 contactID: recipient.ContactID,
167 isInternal: encryptionPreferences.isInternal,
168 bePinnedPublicKey: encryptionPreferences.sendKey as PublicKeyReference,
172 await handleShowAskForKeyPinningModal({
174 onNotTrust: handleRemove,
175 onError: handleRemove,
178 return updateRecipientIcon();
180 const sendIcon = getSendStatusIcon(sendPreferences);
181 const contactSignatureInfo = {
182 isVerified: encryptionPreferences.isContactSignatureVerified,
183 creationTime: encryptionPreferences.contactSignatureTimestamp,
186 setMapSendInfo((mapSendInfo) => ({
193 encryptionPreferenceError: encryptionPreferences.error?.type,
194 emailAddressWarnings: encryptionPreferences.emailAddressWarnings || [],
195 contactSignatureInfo,
200 void updateRecipientIcon();
201 }, [emailAddress, contactsMap, ktActivation]);
203 return { handleRemove, askForKeyPinningModal, contactResignModal };
206 interface LoadParams {
207 emailAddress: string;
210 abortController: AbortController;
211 checkForError: boolean;
214 export const useUpdateGroupSendInfo = (
215 messageSendInfo: MessageSendInfo | undefined,
216 contacts: ContactEmail[],
219 const getEncryptionPreferences = useGetEncryptionPreferences();
220 const contactsMap = useContactsMap();
221 const emailsInGroup = contacts.map(({ Email }) => Email);
223 const [askForKeyPinningModal, handleShowAskForKeyPinningModal] = useModalTwo(AskForKeyPinningModal);
225 const [contactResignModal, handleContactResignModal] = useModalTwo(ContactResignModal);
227 const handleRemove = () => {
228 if (messageSendInfo) {
229 const { setMapSendInfo } = messageSendInfo;
230 setMapSendInfo((mapSendInfo) => omit(mapSendInfo, emailsInGroup));
235 const { ktActivation } = useKeyTransparencyContext();
238 const abortController = new AbortController();
239 // loadSendIcon tries to load the corresponding icon for an email address. If all goes well, it returns nothing.
240 // If there are errors, it returns an error type and information about the email address that failed
241 const loadSendIcon = async ({
248 const { signal } = abortController;
249 const icon = messageSendInfo?.mapSendInfo[emailAddress]?.sendIcon;
250 const emailValidation = validateEmailAddress(emailAddress);
256 !!messageSendInfo.mapSendInfo[emailAddress] ||
262 const { message, setMapSendInfo } = messageSendInfo;
263 if (!signal.aborted) {
264 setMapSendInfo((mapSendInfo) => {
265 const sendInfo = mapSendInfo[emailAddress];
268 [emailAddress]: { ...sendInfo, loading: true, emailValidation },
272 const encryptionPreferences = await getEncryptionPreferences({
275 contactEmailsMap: contactsMap,
277 const sendPreferences = getSendPreferences(encryptionPreferences, message.data);
278 const sendIcon = getSendStatusIcon(sendPreferences);
279 const contactSignatureInfo = {
280 isVerified: encryptionPreferences.isContactSignatureVerified,
281 creationTime: encryptionPreferences.contactSignatureTimestamp,
283 if (!signal.aborted) {
284 setMapSendInfo((mapSendInfo) => ({
291 emailAddressWarnings: encryptionPreferences.emailAddressWarnings || [],
292 contactSignatureInfo,
296 if (checkForError && sendPreferences.error) {
298 error: sendPreferences.error,
303 isInternal: encryptionPreferences.isInternal,
304 bePinnedPublicKey: encryptionPreferences.sendKey as PublicKeyReference,
310 const loadSendIcons = async ({
313 }: Pick<LoadParams, 'abortController' | 'checkForError'>): Promise<void> => {
314 const requests = contacts.map(
315 ({ Email, ContactID, Name }) =>
319 contactID: ContactID,
325 // the routes called in requests support 100 calls every 10 seconds
326 const results = await processApiRequestsSafe(requests, 100, 10 * 1000);
327 const contactsResign = results
329 .filter(({ error: { type } }) => type === CONTACT_SIGNATURE_NOT_VERIFIED)
330 .map(({ contact }) => contact);
331 const totalContactsResign = contactsResign.length;
332 if (totalContactsResign) {
333 const title = c('Title').ngettext(msgid`Re-sign contact`, `Re-sign contacts`, totalContactsResign);
334 const contactNames = contactsResign.map(({ contactName }) => contactName).join(', ');
335 const contactAddresses = contactsResign.map(({ emailAddress }) => emailAddress).join(', ');
337 const text = getSignText(totalContactsResign, contactNames, contactAddresses);
339 await handleContactResignModal({
341 contacts: contactsResign,
347 return loadSendIcons({ abortController, checkForError: false });
349 const contactsKeyPinning = results
351 .filter(({ error: { type } }) => type === PRIMARY_NOT_PINNED)
352 .map(({ contact }) => contact);
353 if (contactsKeyPinning.length) {
354 await handleShowAskForKeyPinningModal({
355 contacts: contactsKeyPinning,
360 return loadSendIcons({ abortController, checkForError: false });
364 void loadSendIcons({ abortController, checkForError: true });
367 abortController.abort();
371 return { handleRemove, askForKeyPinningModal, contactResignModal };
374 const getUpdatedSendInfo = async (
375 emailAddress: string,
376 message: MessageState,
377 setMapSendInfo: Dispatch<SetStateAction<MapSendInfo>>,
378 getEncryptionPreferences: GetEncryptionPreferences,
379 ktActivation: KeyTransparencyActivation,
380 contactsMap: ContactsMap
382 const encryptionPreferences = await getEncryptionPreferences({
385 contactEmailsMap: contactsMap,
387 const sendPreferences = getSendPreferences(encryptionPreferences, message.data);
388 const sendIcon = getSendStatusIcon(sendPreferences);
389 const contactSignatureInfo = {
390 isVerified: encryptionPreferences.isContactSignatureVerified,
391 creationTime: encryptionPreferences.contactSignatureTimestamp,
393 const updatedSendInfo = {
397 emailAddressWarnings: encryptionPreferences.emailAddressWarnings || [],
398 contactSignatureInfo,
400 setMapSendInfo((mapSendInfo) => {
401 const sendInfo = mapSendInfo[emailAddress];
403 return { ...mapSendInfo };
407 [emailAddress]: { ...sendInfo, ...updatedSendInfo },
412 export const useReloadSendInfo = () => {
413 const getEncryptionPreferences = useGetEncryptionPreferences();
414 const contactsMap = useContactsMap();
415 const { ktActivation } = useKeyTransparencyContext();
418 async (messageSendInfo: MessageSendInfo | undefined, message: MessageState) => {
419 const { mapSendInfo, setMapSendInfo } = messageSendInfo || {};
421 if (mapSendInfo === undefined || !setMapSendInfo || !message.data) {
425 const recipients = getRecipientsAddresses(message.data);
426 const requests = recipients.map(
427 (emailAddress) => () =>
432 getEncryptionPreferences,
437 const loadingMapSendInfo = recipients.reduce(
438 (acc, emailAddress) => {
439 const sendInfo = acc[emailAddress] || { emailValidation: validateEmailAddress(emailAddress) };
440 acc[emailAddress] = { ...sendInfo, loading: true };
445 setMapSendInfo(loadingMapSendInfo);
446 // the routes called in requests support 100 calls every 10 seconds
447 await processApiRequestsSafe(requests, 100, 10 * 1000);
449 [getEncryptionPreferences, contactsMap, ktActivation]