Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / forward / ForwardModal.tsx
blob67354bc1b2c62c0084c8303310a78f0be90c8bce
1 import { useMemo, useState } from 'react';
3 import { c } from 'ttag';
5 import { useGetAddressKeys } from '@proton/account/addressKeys/hooks';
6 import { useAddresses } from '@proton/account/addresses/hooks';
7 import { useGetUser } from '@proton/account/user/hooks';
8 import { useGetUserKeys } from '@proton/account/userKeys/hooks';
9 import { Button, Href } from '@proton/atoms';
10 import Form from '@proton/components/components/form/Form';
11 import Icon from '@proton/components/components/icon/Icon';
12 import type { ModalProps } from '@proton/components/components/modalTwo/Modal';
13 import ModalTwo from '@proton/components/components/modalTwo/Modal';
14 import ModalTwoContent from '@proton/components/components/modalTwo/ModalContent';
15 import ModalTwoFooter from '@proton/components/components/modalTwo/ModalFooter';
16 import ModalTwoHeader from '@proton/components/components/modalTwo/ModalHeader';
17 import Option from '@proton/components/components/option/Option';
18 import SelectTwo from '@proton/components/components/selectTwo/SelectTwo';
19 import InputFieldTwo from '@proton/components/components/v2/field/InputField';
20 import useFormErrors from '@proton/components/components/v2/useFormErrors';
21 import useKTVerifier from '@proton/components/containers/keyTransparency/useKTVerifier';
22 import useApi from '@proton/components/hooks/useApi';
23 import useAuthentication from '@proton/components/hooks/useAuthentication';
24 import useEventManager from '@proton/components/hooks/useEventManager';
25 import useGetPublicKeysForInbox from '@proton/components/hooks/useGetPublicKeysForInbox';
26 import useNotifications from '@proton/components/hooks/useNotifications';
27 import type { PrivateKeyReference, PublicKeyReference } from '@proton/crypto/lib';
28 import { CryptoProxy } from '@proton/crypto/lib';
29 import useLoading from '@proton/hooks/useLoading';
30 import { useContactEmails } from '@proton/mail/contactEmails/hooks';
31 import type { SetupForwardingParameters } from '@proton/shared/lib/api/forwardings';
32 import { setupForwarding, updateForwardingFilter } from '@proton/shared/lib/api/forwardings';
33 import { ADDRESS_RECEIVE, KEYGEN_CONFIGS, KEYGEN_TYPES, RECIPIENT_TYPES } from '@proton/shared/lib/constants';
34 import { emailValidator, requiredValidator } from '@proton/shared/lib/helpers/formValidators';
35 import { getKnowledgeBaseUrl } from '@proton/shared/lib/helpers/url';
36 import type { Address, DecryptedKey, OutgoingAddressForwarding } from '@proton/shared/lib/interfaces';
37 import { ForwardingType } from '@proton/shared/lib/interfaces';
38 import type { ContactEmail } from '@proton/shared/lib/interfaces/contacts';
39 import { addAddressKeysProcess, getEmailFromKey, splitKeys } from '@proton/shared/lib/keys';
40 import illustration from '@proton/styles/assets/img/illustrations/forward-email-verification.svg';
41 import uniqueBy from '@proton/utils/uniqueBy';
43 import useAddressFlags from '../../hooks/useAddressFlags';
44 import type { Condition } from '../filters/interfaces';
45 import { FilterStatement } from '../filters/interfaces';
46 import ForwardConditions from './ForwardConditions';
47 import { getInternalParametersPrivate, getSieveParameters, getSieveTree } from './helpers';
49 interface Props extends ModalProps {
50     forward?: OutgoingAddressForwarding;
53 enum Step {
54     Setup,
55     Verification,
58 interface Model {
59     step: Step;
60     loading?: boolean;
61     edit?: boolean;
62     addressID: string;
63     isExternal?: boolean;
64     isInternal?: boolean;
65     forwardeeEmail: string;
66     forwardeePublicKey?: PublicKeyReference;
67     forwarderKey?: PrivateKeyReference;
68     forwarderAddressKeys?: DecryptedKey[];
69     keySupportE2EEForwarding?: boolean;
70     keyErrors?: string[];
71     statement: FilterStatement;
72     conditions: Condition[];
75 const getTitle = (model: Model) => {
76     const { step } = model;
78     if (step === Step.Setup) {
79         return c('email_forwarding_2023: Title').t`Set up forwarding`;
80     }
82     if (step === Step.Verification) {
83         return c('email_forwarding_2023: Title').t`Request confirmation`;
84     }
86     return '';
89 const getDefaultModel = ({ forward, addresses }: { addresses: Address[]; forward?: OutgoingAddressForwarding }) => {
90     const isEditing = !!forward;
91     const { statement, conditions } = isEditing
92         ? getSieveParameters(forward.Filter?.Tree || [])
93         : { statement: FilterStatement.ALL, conditions: [] };
95     const [firstAddress] = addresses;
96     return {
97         step: Step.Setup,
98         addressID: isEditing ? forward.ForwarderAddressID : firstAddress?.ID || '',
99         forwardeeEmail: isEditing ? forward.ForwardeeEmail : '',
100         statement,
101         conditions,
102     };
105 const compareContactEmailByEmail = (a: ContactEmail, b: ContactEmail) => {
106     return a.Email.localeCompare(b.Email);
109 const keyGenConfig = KEYGEN_CONFIGS[KEYGEN_TYPES.CURVE25519];
111 const ForwardModal = ({ forward, onClose, ...rest }: Props) => {
112     const isEditing = !!forward;
113     const [addresses = []] = useAddresses();
114     const [contactEmails = []] = useContactEmails();
115     const contactEmailsSorted = useMemo(() => {
116         const uniqueEmails = uniqueBy(contactEmails, ({ Email }) => Email.toLowerCase());
117         const sortedEmails = [...uniqueEmails].sort(compareContactEmailByEmail);
118         return sortedEmails;
119     }, [contactEmails]);
120     const api = useApi();
121     const getUser = useGetUser();
122     const silentApi = <T,>(config: any) => api<T>({ ...config, silence: true });
123     const { keyTransparencyVerify, keyTransparencyCommit } = useKTVerifier(silentApi, getUser);
124     const authentication = useAuthentication();
125     const getPublicKeysForInbox = useGetPublicKeysForInbox();
126     const getAddressKeys = useGetAddressKeys();
127     const getUserKeys = useGetUserKeys();
128     const { createNotification } = useNotifications();
129     const { call } = useEventManager();
130     const { validator, onFormSubmit } = useFormErrors();
131     const [loading, withLoading] = useLoading();
132     const filteredAddresses = addresses.filter(({ Receive }) => Receive === ADDRESS_RECEIVE.RECEIVE_YES);
133     const [model, setModel] = useState<Model>(getDefaultModel({ forward, addresses: filteredAddresses }));
134     const inputsDisabled = model.loading || isEditing;
135     const forwarderAddress = addresses.find(({ ID }) => ID === model.addressID);
136     const forwarderEmail = forwarderAddress?.Email || '';
137     const addressFlags = useAddressFlags(forwarderAddress);
138     const boldForwardeeEmail = <strong key="forwardee-email">{model.forwardeeEmail}</strong>;
139     const boldForwarderEmail = <strong key="forwarder-email">{forwarderEmail}</strong>;
140     const learnMoreLink = (
141         <Href href={getKnowledgeBaseUrl('/email-forwarding')}>{c('email_forwarding_2023: Link').t`Learn more`}</Href>
142     );
144     const generateNewKey = async () => {
145         if (!forwarderAddress) {
146             throw new Error('No address');
147         }
149         if (!model.forwarderAddressKeys) {
150             throw new Error('No forwarder address keys');
151         }
153         const userKeys = await getUserKeys();
154         const [newKey] = await addAddressKeysProcess({
155             api,
156             userKeys,
157             keyGenConfig,
158             addresses,
159             address: forwarderAddress,
160             addressKeys: model.forwarderAddressKeys,
161             keyPassword: authentication.getPassword(),
162             keyTransparencyVerify,
163         });
164         const { privateKey: forwarderKey } = newKey;
165         const [forwarderAddressKeys] = await Promise.all([
166             getAddressKeys(model.addressID),
167             keyTransparencyCommit(userKeys),
168         ]);
170         return {
171             forwarderKey,
172             forwarderAddressKeys,
173         };
174     };
176     const handleEdit = async () => {
177         if (isEditing) {
178             await api(
179                 updateForwardingFilter(
180                     forward.ID,
181                     getSieveTree({
182                         conditions: model.conditions,
183                         statement: model.statement,
184                         email: model.forwardeeEmail,
185                     }),
186                     forward.Filter?.Version || 2
187                 )
188             );
189             await call();
190             onClose?.();
191             createNotification({ text: c('email_forwarding_2023: Success').t`Changes saved` });
192         }
193     };
195     const handleSetup = async () => {
196         const [forwarderAddressKeys, forwardeeKeysConfig] = await Promise.all([
197             getAddressKeys(model.addressID),
198             getPublicKeysForInbox({ email: model.forwardeeEmail, lifetime: 0 }),
199         ]);
201         // Abort the setup if e.g. the given address is internal but does not exist
202         const apiErrors = forwardeeKeysConfig.Errors || [];
203         if (apiErrors.length > 0) {
204             apiErrors.forEach((error: string) => {
205                 createNotification({ text: error, type: 'error' });
206             });
207             return;
208         }
210         const isInternal = forwardeeKeysConfig.RecipientType === RECIPIENT_TYPES.TYPE_INTERNAL;
211         const isExternal = forwardeeKeysConfig.RecipientType === RECIPIENT_TYPES.TYPE_EXTERNAL;
212         const { privateKeys } = splitKeys(forwarderAddressKeys);
213         const [forwarderKey] = privateKeys;
214         const keySupportE2EEForwarding = await CryptoProxy.doesKeySupportE2EEForwarding({ forwarderKey });
215         let forwardeePublicKey: PublicKeyReference | undefined;
216         let forwardeeEmailFromPublicKey: string | undefined;
218         if (isInternal) {
219             // While forwarding could be setup with generic catch-all addresses, we disallow this as the catch-all address case is triggered
220             // if the forwardee is a private subuser who has yet to login (i.e.has missing keys).
221             // In such case, the admin public keys are temporarily returned instead, meaning that E2EE forwarding will be (permanently) setup with the admin, rather
222             // than the subuser, which is undesirable.
223             if (forwardeeKeysConfig.isCatchAll) {
224                 createNotification({ text: 'This address cannot be used as forwarding recipient', type: 'error' });
225                 return;
226             }
227             const [primaryForwardeeKey] = forwardeeKeysConfig.publicKeys;
228             forwardeePublicKey = await CryptoProxy.importPublicKey({
229                 armoredKey: primaryForwardeeKey.armoredKey,
230             });
231             forwardeeEmailFromPublicKey = getEmailFromKey(forwardeePublicKey);
232         }
234         setModel({
235             ...model,
236             forwarderAddressKeys,
237             keySupportE2EEForwarding,
238             keyErrors: forwardeeKeysConfig.Errors,
239             forwarderKey,
240             forwardeePublicKey,
241             forwardeeEmail: forwardeeEmailFromPublicKey || model.forwardeeEmail,
242             isExternal,
243             isInternal,
244             step: Step.Verification,
245         });
246     };
248     const handleVerification = async () => {
249         // Disable encryption if the email is external
250         if (model.isExternal && addressFlags?.encryptionDisabled === false) {
251             await addressFlags?.handleSetAddressFlags(true, addressFlags?.expectSignatureDisabled);
252         }
254         const params: SetupForwardingParameters = {
255             ForwarderAddressID: model.addressID,
256             ForwardeeEmail: model.forwardeeEmail,
257             Type: model.isInternal ? ForwardingType.InternalEncrypted : ForwardingType.ExternalUnencrypted,
258             Tree: getSieveTree({
259                 conditions: model.conditions,
260                 statement: model.statement,
261                 email: model.forwardeeEmail,
262             }),
263             Version: forward?.Filter?.Version || 2,
264         };
265         let requireNewKey = false;
266         if (model.isInternal && forwarderAddress?.Keys && model.forwardeePublicKey && model.forwarderKey) {
267             let forwarderKey = model.forwarderKey;
269             if (!model.keySupportE2EEForwarding) {
270                 // The forwarding material generation will fail if the address key is e.g. RSA instead of ECC 25519
271                 // So we generate automatically a new ECC 25519 key for the address
272                 const newProperties = await generateNewKey();
273                 // Save the new key in case something goes wrong later
274                 setModel({
275                     ...model,
276                     ...newProperties,
277                 });
278                 forwarderKey = newProperties.forwarderKey;
279             }
281             const { activationToken, forwardeeKey, proxyInstances } = await getInternalParametersPrivate(
282                 forwarderKey,
283                 [{ email: model.forwardeeEmail, name: model.forwardeeEmail }],
284                 model.forwardeePublicKey
285             );
286             params.ForwardeePrivateKey = forwardeeKey;
287             params.ActivationToken = activationToken;
288             params.ProxyInstances = proxyInstances;
289         }
291         await api(setupForwarding(params));
292         await call();
293         onClose?.();
294         createNotification({ text: c('email_forwarding_2023: Success').t`Email sent to ${model.forwardeeEmail}.` });
296         if (requireNewKey) {
297             createNotification({
298                 text: c('email_forwarding_2023: Success')
299                     .t`A new encryption key has been generated for ${forwarderEmail}.`,
300             });
301         }
302     };
304     const handleSubmit = async () => {
305         if (loading || !onFormSubmit()) {
306             return;
307         }
309         if (isEditing) {
310             return handleEdit();
311         }
313         if (model.step === Step.Setup) {
314             return handleSetup();
315         }
317         if (model.step === Step.Verification) {
318             return handleVerification();
319         }
320     };
322     const handleBack = () => {
323         setModel({ ...model, step: Step.Setup });
324     };
326     return (
327         <ModalTwo
328             as={Form}
329             onClose={onClose}
330             onSubmit={() => withLoading(handleSubmit())}
331             onReset={() => {
332                 onClose?.();
333             }}
334             {...rest}
335         >
336             <ModalTwoHeader title={getTitle(model)} />
337             <ModalTwoContent>
338                 {model.step === Step.Setup && (
339                     <>
340                         <InputFieldTwo
341                             id="from-select"
342                             as={SelectTwo}
343                             label={c('email_forwarding_2023: Label').t`Forward from`}
344                             value={model.addressID}
345                             onValue={(value: unknown) => setModel({ ...model, addressID: value as string })}
346                             disabled={inputsDisabled}
347                             disabledOnlyField={inputsDisabled}
348                             autoFocus
349                         >
350                             {(isEditing ? addresses : filteredAddresses).map(({ ID, Email, Receive }) => (
351                                 <Option
352                                     title={Email}
353                                     key={ID}
354                                     value={ID}
355                                     disabled={Receive !== ADDRESS_RECEIVE.RECEIVE_YES}
356                                 >
357                                     {Email}
358                                 </Option>
359                             ))}
360                         </InputFieldTwo>
361                         <InputFieldTwo
362                             id="to-input"
363                             label={c('email_forwarding_2023: Label').t`Forward to`}
364                             placeholder={c('email_forwarding_2023: Placeholder').t`Enter email address`}
365                             disabled={inputsDisabled}
366                             disabledOnlyField={inputsDisabled}
367                             readOnly={isEditing}
368                             list="contact-emails"
369                             type="email"
370                             error={validator([
371                                 requiredValidator(model.forwardeeEmail),
372                                 emailValidator(model.forwardeeEmail),
373                             ])}
374                             value={model.forwardeeEmail}
375                             onValue={(value: string) => setModel({ ...model, forwardeeEmail: value })}
376                             required
377                         />
378                         {contactEmailsSorted.length ? (
379                             <datalist id="contact-emails">
380                                 {contactEmailsSorted?.map((contactEmail) => (
381                                     <option key={contactEmail.ID} value={contactEmail.Email}>
382                                         {contactEmail.Email}
383                                     </option>
384                                 ))}
385                             </datalist>
386                         ) : null}
387                         <hr className="my-4" />
388                         <ForwardConditions
389                             conditions={model.conditions}
390                             statement={model.statement}
391                             validator={validator}
392                             onChangeStatement={(newStatement) => setModel({ ...model, statement: newStatement })}
393                             onChangeConditions={(newConditions) => setModel({ ...model, conditions: newConditions })}
394                         />
395                     </>
396                 )}
397                 {model.step === Step.Verification && (
398                     <>
399                         <div className="text-center">
400                             <img src={illustration} alt="" />
401                             <p>{c('email_forwarding_2023: Info')
402                                 .jt`A confirmation email will be sent to ${boldForwardeeEmail}`}</p>
403                             <p>{c('email_forwarding_2023: Info')
404                                 .t`Forwarding to this address will become active once the recipient accepts the forwarding.`}</p>
405                         </div>
406                         {model.isExternal ? (
407                             <div className="border rounded-lg p-4 flex flex-nowrap items-center mb-3">
408                                 <Icon name="exclamation-circle" className="shrink-0 color-danger" />
409                                 <p className="text-sm color-weak flex-1 pl-4 my-0">
410                                     {c('email_forwarding_2023: Info')
411                                         .jt`Forwarding to an address without end-to-end encryption will disable end-to-end encryption for your ${boldForwarderEmail} address, but zero-access encryption remains enabled. ${learnMoreLink}`}
412                                 </p>
413                             </div>
414                         ) : null}
415                         {model.isExternal || model.keySupportE2EEForwarding ? null : (
416                             <div className="border rounded-lg p-4 flex flex-nowrap items-center mb-3">
417                                 <Icon name="exclamation-circle" className="shrink-0 color-danger" />
418                                 <p className="text-sm color-weak flex-1 pl-4 my-0">
419                                     {c('email_forwarding_2023: Info')
420                                         .jt`A new encryption key will be generated for ${boldForwarderEmail}.`}
421                                 </p>
422                             </div>
423                         )}
424                         {model?.keyErrors?.length ? (
425                             <div className="border rounded-lg p-4 flex flex-nowrap items-center">
426                                 <Icon name="exclamation-circle" className="shrink-0 color-danger" />
427                                 <p className="text-sm color-weak flex-1 pl-4 my-0">{model.keyErrors.join(' ')}</p>
428                             </div>
429                         ) : null}
430                     </>
431                 )}
432             </ModalTwoContent>
433             <ModalTwoFooter>
434                 {model.step === Step.Setup && (
435                     <>
436                         <Button disabled={loading} type="reset">{c('email_forwarding_2023: Action').t`Cancel`}</Button>
437                         <Button loading={loading} color="norm" type="submit">
438                             {isEditing
439                                 ? c('email_forwarding_2023: Action').t`Save`
440                                 : c('email_forwarding_2023: Action').t`Next`}
441                         </Button>
442                     </>
443                 )}
444                 {model.step === Step.Verification && (
445                     <>
446                         <Button onClick={handleBack}>{c('email_forwarding_2023: Action').t`Back`}</Button>
447                         <Button loading={loading} color="norm" type="submit">{c('email_forwarding_2023: Action')
448                             .t`Send confirmation email`}</Button>
449                     </>
450                 )}
451             </ModalTwoFooter>
452         </ModalTwo>
453     );
456 export default ForwardModal;