Skip to content

Commit

Permalink
Domains: Implement multi-target email forwards (#98837)
Browse files Browse the repository at this point in the history
  • Loading branch information
alshakero authored Feb 4, 2025
1 parent 6f0f33d commit a1abc2c
Show file tree
Hide file tree
Showing 35 changed files with 1,012 additions and 754 deletions.
31 changes: 31 additions & 0 deletions client/data/emails/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ export type EmailAccountEmail = {
warnings: Warning[];
};

export type ResponseError = {
error:
| 'destination_failed'
| 'invalid_input'
| 'not_valid_destination'
| 'destination_failed'
| 'too_many_destinations'
| 'exceeded_mailbox_forwards'
| 'mailbox_too_long'
| 'not_valid_mailbox'
| 'empty_destination'
| 'same_destination_domain'
| 'forward_exists';
message:
| string
| {
error_message: string;
/**
* The index of the faulty email address in the `destinations` array
*/
index: number;
};
};

export type AlterDestinationParams = {
mailbox: string;
destination: string;
domain: string;
};

type EmailAccountDomain = {
domain: string;
is_primary: boolean;
Expand Down Expand Up @@ -45,4 +75,5 @@ export type Mailbox = {
mailbox: string;
warnings?: Warning[];
temporary?: boolean;
target: string;
};
27 changes: 16 additions & 11 deletions client/data/emails/use-add-email-forward-mutation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { useDispatch, useSelector } from 'calypso/state';
import { errorNotice } from 'calypso/state/notices/actions';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
import { getCacheKey as getEmailAccountsQueryKey } from './use-get-email-accounts-query';
import type { ResponseError } from './types';
import type { UseMutationOptions } from '@tanstack/react-query';

const ArrayOfFive = new Array( 5 );

type AddMailboxFormData = {
destination: string;
destinations: typeof ArrayOfFive;
mailbox: string;
};

Expand All @@ -38,7 +41,7 @@ export function useIsLoading() {
export default function useAddEmailForwardMutation(
domainName: string,
mutationOptions: Omit<
UseMutationOptions< any, unknown, AddMailboxFormData, Context >,
UseMutationOptions< any, ResponseError, AddMailboxFormData, Context >,
'mutationFn'
> = {}
) {
Expand All @@ -65,7 +68,7 @@ export default function useAddEmailForwardMutation(
};

mutationOptions.onMutate = async ( variables ) => {
const { mailbox, destination } = variables;
const { mailbox, destinations } = variables;
suppliedOnMutate?.( variables );

await queryClient.cancelQueries( { queryKey: emailAccountsQueryKey } );
Expand All @@ -79,16 +82,16 @@ export default function useAddEmailForwardMutation(
const newEmailForwards = orderBy(
[
...emailForwards,
{
...destinations.map( ( d ) => ( {
domain: domainName,
email_type: 'email_forward',
is_verified: false,
mailbox,
role: 'standard',
target: destination,
target: d,
temporary: true,
warnings: [],
},
} ) ),
],
[ 'mailbox' ],
[ 'asc' ]
Expand Down Expand Up @@ -135,7 +138,7 @@ export default function useAddEmailForwardMutation(
};
};

mutationOptions.onError = ( error, variables, context ) => {
mutationOptions.onError = ( error: ResponseError, variables, context ) => {
suppliedOnError?.( error, variables, context );

if ( context ) {
Expand Down Expand Up @@ -163,12 +166,14 @@ export default function useAddEmailForwardMutation(
);

if ( error ) {
const message =
typeof error.message === 'object' ? error.message.error_message : error.message;
errorMessage = translate(
'Failed to add email forward for {{strong}}%(emailAddress)s{{/strong}} with message "%(message)s". Please try again or {{contactSupportLink}}contact support{{/contactSupportLink}}.',
{
args: {
emailAddress: variables.mailbox,
message: error as string,
message,
},
components: noticeComponents,
}
Expand All @@ -178,11 +183,11 @@ export default function useAddEmailForwardMutation(
dispatch( errorNotice( errorMessage ) );
};

return useMutation< any, unknown, AddMailboxFormData, Context >( {
mutationFn: ( { mailbox, destination } ) =>
return useMutation< any, ResponseError, AddMailboxFormData, Context >( {
mutationFn: ( { mailbox, destinations } ) =>
wp.req.post( `/domains/${ encodeURIComponent( domainName ) }/email/new`, {
mailbox,
destination,
destinations,
} ),
...mutationOptions,
} );
Expand Down
21 changes: 13 additions & 8 deletions client/data/emails/use-remove-email-forward-mutation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useDispatch, useSelector } from 'calypso/state';
import { errorNotice } from 'calypso/state/notices/actions';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
import { getCacheKey as getEmailAccountsQueryKey } from './use-get-email-accounts-query';
import type { EmailAccountEmail } from './types';
import type { EmailAccountEmail, AlterDestinationParams, Mailbox } from './types';
import type { UseMutationOptions } from '@tanstack/react-query';

type Context = {
Expand All @@ -26,7 +26,7 @@ const MUTATION_KEY = 'removeEmailForward';
export default function useRemoveEmailForwardMutation(
domainName: string,
mutationOptions: Omit<
UseMutationOptions< any, unknown, EmailAccountEmail, Context >,
UseMutationOptions< any, unknown, AlterDestinationParams, Context >,
'mutationFn'
> = {}
) {
Expand Down Expand Up @@ -70,7 +70,11 @@ export default function useRemoveEmailForwardMutation(
{
...previousEmailAccountsQueryData.accounts[ 0 ],
emails: emailForwards.filter(
( forward: EmailAccountEmail ) => forward.mailbox !== emailForward.mailbox
( forward: EmailAccountEmail ) =>
! (
forward.mailbox === emailForward.mailbox &&
forward.target === emailForward.destination
)
),
},
],
Expand Down Expand Up @@ -140,13 +144,14 @@ export default function useRemoveEmailForwardMutation(
dispatch( errorMessage );
};

return useMutation< any, unknown, EmailAccountEmail, Context >( {
mutationFn: ( { mailbox } ) =>
wp.req.post(
return useMutation< Mailbox, unknown, AlterDestinationParams, Context >( {
mutationFn: ( { mailbox, destination } ) => {
return wp.req.post(
`/domains/${ encodeURIComponent( domainName ) }/email/${ encodeURIComponent(
mailbox
) }/delete`
),
) }/${ encodeURIComponent( destination ) }/delete`
);
},
...mutationOptions,
} );
}
16 changes: 9 additions & 7 deletions client/data/emails/use-resend-verify-email-forward-mutation.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { CALYPSO_CONTACT } from '@automattic/urls';
import { useMutation } from '@tanstack/react-query';
import { useTranslate } from 'i18n-calypso';
import { getEmailForwardAddress } from 'calypso/lib/emails';
import wp from 'calypso/lib/wp';
import { useDispatch } from 'calypso/state';
import { errorNotice, successNotice } from 'calypso/state/notices/actions';
import type { EmailAccountEmail } from './types';
import type { AlterDestinationParams } from './types';
import type { UseMutationOptions } from '@tanstack/react-query';

const MUTATION_KEY = 'reverifyEmailForward';
Expand All @@ -18,7 +17,10 @@ const MUTATION_KEY = 'reverifyEmailForward';
*/
export default function useResendVerifyEmailForwardMutation(
domainName: string,
mutationOptions: Omit< UseMutationOptions< any, unknown, EmailAccountEmail >, 'mutationFn' > = {}
mutationOptions: Omit<
UseMutationOptions< any, unknown, AlterDestinationParams >,
'mutationFn'
> = {}
) {
const dispatch = useDispatch();
const translate = useTranslate();
Expand All @@ -31,7 +33,7 @@ export default function useResendVerifyEmailForwardMutation(
mutationOptions.onSuccess = ( data, emailForward, context ) => {
suppliedOnSuccess?.( data, emailForward, context );

const destination = getEmailForwardAddress( emailForward );
const { destination } = emailForward;

const successMessage = translate(
'Successfully sent confirmation email for %(email)s to %(destination)s.',
Expand Down Expand Up @@ -69,12 +71,12 @@ export default function useResendVerifyEmailForwardMutation(
dispatch( errorNotice( failureMessage ) );
};

return useMutation< any, unknown, EmailAccountEmail >( {
mutationFn: ( { mailbox } ) =>
return useMutation< any, unknown, AlterDestinationParams >( {
mutationFn: ( { mailbox, destination } ) =>
wp.req.post(
`/domains/${ encodeURIComponent( domainName ) }/email/${ encodeURIComponent(
mailbox
) }/resend-verification`
) }/${ encodeURIComponent( destination ) }/resend-verification`
),
...mutationOptions,
} );
Expand Down

This file was deleted.

35 changes: 0 additions & 35 deletions client/lib/domains/email-forwarding/index.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,3 @@
import emailValidator from 'email-validator';
import { mapValues } from 'lodash';
import { hasDuplicatedEmailForwards } from 'calypso/lib/domains/email-forwarding/has-duplicated-email-forwards';

function validateAllFields( fieldValues, existingEmailForwards = [] ) {
return mapValues( fieldValues, ( value, fieldName ) => {
const isValid = validateField( {
value,
name: fieldName,
} );

if ( ! isValid ) {
return [ 'Invalid' ];
}

if ( fieldName !== 'mailbox' ) {
return [];
}

return hasDuplicatedEmailForwards( value, existingEmailForwards ) ? [ 'Duplicated' ] : [];
} );
}

function validateField( { name, value } ) {
switch ( name ) {
case 'mailbox':
return /^[a-z0-9._+-]{1,64}$/i.test( value ) && ! /(^\.)|(\.{2,})|(\.$)/.test( value );
case 'destination':
return emailValidator.validate( value );
default:
return true;
}
}

export { getEmailForwardsCount } from './get-email-forwards-count';
export { hasEmailForwards } from './has-email-forwards';
export { getDomainsWithEmailForwards } from './get-domains-with-email-forwards';
export { validateAllFields };
80 changes: 80 additions & 0 deletions client/my-sites/email/email-forwarding/actions-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ConfirmationDialog } from '@automattic/components';
import {
__experimentalHeading as Heading,
__experimentalText as Text,
__experimentalVStack as VStack,
DropdownMenu,
} from '@wordpress/components';
import { rotateLeft, trash, moreHorizontalMobile } from '@wordpress/icons';
import { useTranslate } from 'i18n-calypso';
import { useState } from 'react';
import { getEmailForwardAddress } from 'calypso/lib/emails';
import { useResend, useRemove } from '../hooks';
import type { Mailbox } from '../../../../data/emails/types';
import './style.scss';

export const ActionsMenu = ( { mailbox }: { mailbox: Mailbox } ) => {
const [ isOpen, setIsOpen ] = useState( false );
const remove = useRemove( { mailbox } );
const resend = useResend( { mailbox } );
const translate = useTranslate();

const handleConfirm = () => {
remove( mailbox.mailbox, mailbox.domain, getEmailForwardAddress( mailbox ) );
setIsOpen( false );
};

const handleCancel = () => {
setIsOpen( false );
};

return (
<>
<ConfirmationDialog
isOpen={ isOpen }
onConfirm={ handleConfirm }
onCancel={ handleCancel }
cancelButtonText={ translate( 'Cancel' ) }
confirmButtonText={ translate( 'Remove' ) }
>
<VStack>
<Heading level={ 3 }>
{ translate( 'Are you sure you want to remove this email forward?' ) }
</Heading>
<Text>
{ translate(
"This will remove it from our records and if it's not used in another forward, it will require reverification if added again."
) }
</Text>
</VStack>
</ConfirmationDialog>
<DropdownMenu
icon={ moreHorizontalMobile }
label={ translate( 'More options' ) }
controls={
mailbox.warnings?.length
? [
{
title: 'Resend',
icon: rotateLeft,
onClick: () =>
resend( mailbox.mailbox, mailbox.domain, getEmailForwardAddress( mailbox ) ),
},
{
title: 'Remove',
icon: trash,
onClick: () => setIsOpen( true ),
},
]
: [
{
title: 'Remove',
icon: trash,
onClick: () => setIsOpen( true ),
},
]
}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Delete me once https://github.com/Automattic/wp-calypso/pull/98095#discussion_r1937657191 is addressed.
.email-forward-list__actions {
.components-dropdown.components-dropdown-menu {
flex-grow: unset;
}
}
Loading

0 comments on commit a1abc2c

Please sign in to comment.