diff --git a/.gitignore b/.gitignore index 612321ca5..28c4fb858 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /junit.xml /coverage .DS_Store +.tool-versions .vscode/ coverage zextras-carbonio-*.tgz diff --git a/.idea/prettier.xml b/.idea/prettier.xml index 727b8b533..60c07b5ac 100644 --- a/.idea/prettier.xml +++ b/.idea/prettier.xml @@ -1,6 +1,7 @@ + diff --git a/src/app-utils/test/add-shell-components.test.tsx b/src/app-utils/test/add-shell-components.test.tsx index 0410a0698..ade9f25fe 100644 --- a/src/app-utils/test/add-shell-components.test.tsx +++ b/src/app-utils/test/add-shell-components.test.tsx @@ -60,7 +60,10 @@ describe('addShellComponents', () => { { id: 'recover_messages', label: 'label.recover_messages' }, { id: 'signatures', label: 'signatures.signature_heading' }, { id: 'using_signatures', label: 'label.using_signatures' }, - { id: 'filters', label: 'filters.filters' } + { id: 'filters', label: 'filters.filters' }, + { id: 'trusted_addresses', label: 'label.trusted_addresses' }, + { id: 'allowed_addresses', label: 'label.allowed_addresses' }, + { id: 'blocked_addresses', label: 'label.blocked_addresses' } ] }) ); @@ -80,7 +83,10 @@ describe('addShellComponents', () => { { id: 'receiving_messages', label: 'label.receive_message' }, { id: 'signatures', label: 'signatures.signature_heading' }, { id: 'using_signatures', label: 'label.using_signatures' }, - { id: 'filters', label: 'filters.filters' } + { id: 'filters', label: 'filters.filters' }, + { id: 'trusted_addresses', label: 'label.trusted_addresses' }, + { id: 'allowed_addresses', label: 'label.allowed_addresses' }, + { id: 'blocked_addresses', label: 'label.blocked_addresses' } ] }) ); diff --git a/src/carbonio-ui-commons b/src/carbonio-ui-commons index 268f90cde..ccc87ebc6 160000 --- a/src/carbonio-ui-commons +++ b/src/carbonio-ui-commons @@ -1 +1 @@ -Subproject commit 268f90cdeebfb0540b6cd5286c82f19f8e1e1dc6 +Subproject commit ccc87ebc6b31bc3ad443a8387d8f5a0324b43b75 diff --git a/src/constants/index.ts b/src/constants/index.ts index 6eb833d2d..a7783ef26 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -8,12 +8,13 @@ import { FOLDERS } from '@zextras/carbonio-shell-ui'; import { TFunction } from 'i18next'; export const MAILS_ROUTE = 'mails'; + +export const MAILS_BOARD_VIEW_ID = 'mails_editor_board_view'; + export const BACKUP_SEARCH_ROUTE = 'backup-search'; export const MAIL_APP_ID = 'carbonio-mails-ui'; -export const MAILS_BOARD_VIEW_ID = 'mails_editor_board_view'; - export const NO_ACCOUNT_NAME = 'No account'; export const RECOVER_MESSAGES_INTERVAL = 3; diff --git a/src/store/actions/share-folder.ts b/src/store/actions/share-folder.ts index 63bd0735b..8268fb9aa 100644 --- a/src/store/actions/share-folder.ts +++ b/src/store/actions/share-folder.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { createAsyncThunk } from '@reduxjs/toolkit'; -import { Account, BatchRequest, soapFetch } from '@zextras/carbonio-shell-ui'; -import { BatchResponse } from '@zextras/carbonio-shell-ui/lib/types/network'; +import { soapFetch } from '@zextras/carbonio-shell-ui'; +import type { Account, BatchRequest, BatchResponse } from '@zextras/carbonio-shell-ui'; import { trim } from 'lodash'; import { Folder } from '../../carbonio-ui-commons/types/folder'; diff --git a/src/types/editor/index.d.ts b/src/types/editor/index.d.ts index 631de44fe..bbf26a9e3 100644 --- a/src/types/editor/index.d.ts +++ b/src/types/editor/index.d.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import type { Folder } from '../../carbonio-ui-commons/types/folder'; -import { EDIT_VIEW_CLOSING_REASONS } from '../../constants'; +import { EDIT_VIEW_CLOSING_REASONS, EditViewActions } from '../../constants'; import { type AppDispatch } from '../../store/redux'; import { SavedAttachment, UnsavedAttachment } from '../attachments'; import type { MailMessage } from '../messages'; diff --git a/src/views/app/detail-panel/edit/tests/edit-view.test.tsx b/src/views/app/detail-panel/edit/tests/edit-view.test.tsx index 04ee59f23..6a1be2a94 100644 --- a/src/views/app/detail-panel/edit/tests/edit-view.test.tsx +++ b/src/views/app/detail-panel/edit/tests/edit-view.test.tsx @@ -33,6 +33,10 @@ import { setupTest } from '../../../../../carbonio-ui-commons/test/test-setup'; import { EditViewActions, MAILS_ROUTE } from '../../../../../constants'; import * as useQueryParam from '../../../../../hooks/use-query-param'; import * as saveDraftAction from '../../../../../store/actions/save-draft'; +import { + GetSignaturesRequest, + GetSignaturesResponse +} from '../../../../../store/actions/signatures'; import { addEditor } from '../../../../../store/zustand/editor'; import { generateEditAsNewEditor, @@ -374,6 +378,10 @@ describe('Edit view', () => { await firstSaveDraftInterceptor; const draftSavingInterceptor = aSuccessfullSaveDraft(); + createSoapAPIInterceptor('GetSignatures', { + signature: [], + _jsns: 'urn:zimbraAccount' + }); const subject = faker.lorem.sentence(5); // Get the default identity address diff --git a/src/views/settings/components/trustee-list-item.tsx b/src/views/settings/components/senders-list-item.tsx similarity index 82% rename from src/views/settings/components/trustee-list-item.tsx rename to src/views/settings/components/senders-list-item.tsx index 050122e6c..d6c73659c 100644 --- a/src/views/settings/components/trustee-list-item.tsx +++ b/src/views/settings/components/senders-list-item.tsx @@ -21,19 +21,24 @@ const ListItem = styled(Row)` } `; -// TODO remove the any after the DS -const TrusteeListItem: FC = ({ item, onRemove }): ReactElement => { +export type SendersListItemProps = { + value: string; + onRemove: (sender: string) => void; +}; + +export const SendersListItem: FC = ({ value, onRemove }): ReactElement => { const [hovered, setHovered] = useState(false); const onMouseEnter = useCallback(() => setHovered(true), []); const onMouseLeave = useCallback(() => setHovered(false), []); const onClick = useCallback(() => { - onRemove(item.value); - }, [item, onRemove]); + onRemove(value); + }, [onRemove, value]); return ( = ({ item, onRemove }): ReactElement => { > - {item.value} + {value} @@ -59,5 +64,3 @@ const TrusteeListItem: FC = ({ item, onRemove }): ReactElement => { ); }; - -export default TrusteeListItem; diff --git a/src/views/settings/components/utils.js b/src/views/settings/components/utils.js index 23e7deff0..77da82863 100644 --- a/src/views/settings/components/utils.js +++ b/src/views/settings/components/utils.js @@ -9,12 +9,18 @@ import { filter, find, isEqual, isObject, map, reduce, transform } from 'lodash' import { NO_SIGNATURE_ID } from '../../../helpers/signatures'; +const arraysProps = [ + 'zimbraPrefMailTrustedSenderList', + 'amavisWhitelistSender', + 'amavisBlacklistSender' +]; + export const differenceObject = (object, base) => { // eslint-disable-next-line no-shadow function changes(object, base) { return transform(object, (result, value, key) => { if (!isEqual(value, base[key])) { - if (key === 'zimbraPrefMailTrustedSenderList') { + if (arraysProps.includes(key)) { // eslint-disable-next-line no-param-reassign result[key] = value; } else { diff --git a/src/views/settings/save-settings.tsx b/src/views/settings/save-settings.tsx new file mode 100644 index 000000000..2f28e057f --- /dev/null +++ b/src/views/settings/save-settings.tsx @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2024 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { Identity, updateAccount, updateSettings, xmlSoapFetch } from '@zextras/carbonio-shell-ui'; +import { isArray, map } from 'lodash'; + +import { MAIL_APP_ID } from '../../constants'; + +type AccountSettings = { + [key: string]: string | number | Array | undefined; +}; + +type AccountSettingsPrefs = AccountSettings; +type AccountSettingsAttrs = AccountSettings; +type IdentityAttrs = AccountSettings; + +type PropsMods = Record; +type PrefsMods = Record & AccountSettingsPrefs; +type AttrsMods = Record & AccountSettingsAttrs; + +type IdentityMods = { + modifyList?: Record }>; + deleteList?: string[]; + createList?: { prefs: Partial }[]; +}; + +interface Mods extends Record | undefined> { + props?: PropsMods; + prefs?: PrefsMods; + attrs?: AttrsMods; + identity?: IdentityMods; +} + +export type SaveSettingsResponse = { + CreateIdentityResponse?: { + identity: [Identity]; + }[]; +}; + +function getRequestForProps(props: PropsMods | undefined, appId: string): string { + return props + ? `${map( + props, + (prop, key) => `${prop.value}` + )}` + : ''; +} + +function getRequestForAmavisSendersListAttrs(attrs: AttrsMods | undefined): string { + return attrs?.amavisWhitelistSender || attrs?.amavisBlacklistSender + ? `${ + attrs?.amavisWhitelistSender && isArray(attrs?.amavisWhitelistSender) + ? `${attrs?.amavisWhitelistSender + .map((email) => `${email}`) + .join('')}` + : '' + }${ + attrs?.amavisBlacklistSender && isArray(attrs?.amavisBlacklistSender) + ? `${attrs?.amavisBlacklistSender + .map((email) => `${email}`) + .join('')}` + : '' + }` + : ''; +} + +function getRequestForIdentities(identity: IdentityMods | undefined): string { + return `${ + identity?.modifyList + ? map( + identity.modifyList, + (item) => + `${map(item.prefs, (value, key) => `${value}`).join( + '' + )}` + ).join('') + : '' + }`; +} + +export const saveSettings = ( + mods: Mods, + appId = MAIL_APP_ID +): Promise<{ + CreateIdentityResponse?: { + identity: [Identity]; + }[]; +}> => + xmlSoapFetch( + 'Batch', + ` + ${getRequestForProps(mods.props, appId)} + ${getRequestForAmavisSendersListAttrs(mods.attrs)} + ${getRequestForIdentities(mods.identity)} + ` + ).then((resp) => { + updateSettings(mods); + if (mods.identity) { + updateAccount( + mods.identity, + resp.CreateIdentityResponse?.map((item) => item?.identity[0]) ?? [] + ); + } + return resp; + }); diff --git a/src/views/settings/senders-list.tsx b/src/views/settings/senders-list.tsx new file mode 100644 index 000000000..26d93d755 --- /dev/null +++ b/src/views/settings/senders-list.tsx @@ -0,0 +1,219 @@ +/* + * SPDX-FileCopyrightText: 2022 Zextras + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import React, { useMemo, useState, useCallback } from 'react'; + +import { + Container, + Divider, + TextWithTooltip, + Padding, + Tooltip, + Button, + Row, + Input, + ListV2, + Text +} from '@zextras/carbonio-design-system'; +import { t } from '@zextras/carbonio-shell-ui'; +import { filter } from 'lodash'; + +import { SendersListItem } from './components/senders-list-item'; +import Heading from './components/settings-heading'; +import { allowedSendersSubSection, blockedSendersSubSection } from './subsections'; +import type { InputProps } from '../../types'; +import { isValidEmail } from '../search/parts/utils'; + +export type ListType = 'Allowed' | 'Blocked'; + +export type SendersListProps = InputProps & { + listType: ListType; + showConflictText?: boolean; +}; + +function getMessage(listType: ListType): string { + return listType === 'Allowed' + ? t( + 'messages.allowed_addresses', + 'Mails sent from addresses on your allowed senders list will always bypass your spam filter and land directly in your inbox.' + ) + : t( + 'messages.blocked_addresses', + 'Mails sent from addresses on the blocked senders list will be automatically moved to your spam folder.' + ); +} + +function getPrefName(listType: ListType): string { + return listType === 'Allowed' ? 'amavisWhitelistSender' : 'amavisBlacklistSender'; +} + +export function getList(list: string | string[] | undefined): string[] { + if (!list) { + return []; + } + + return Array.isArray(list) ? list : [list]; +} + +export const SendersList = ({ + settingsObj, + updateSettings, + listType, + showConflictText = false +}: SendersListProps): React.JSX.Element => { + const [address, setAddress] = useState(''); + const [sendersList, setSendersList] = useState( + listType === 'Allowed' + ? getList(settingsObj?.amavisWhitelistSender) + : getList(settingsObj?.amavisBlacklistSender) + ); + const sectionTitle = useMemo( + () => (listType === 'Allowed' ? allowedSendersSubSection() : blockedSendersSubSection()), + [listType] + ); + + const message = useMemo(() => getMessage(listType), [listType]); + + const onAdd = (): void => { + updateSettings({ + target: { + name: getPrefName(listType), + value: [...sendersList, address] + } + }); + setAddress(''); + setSendersList([...sendersList, address]); + }; + + const itemsCount = sendersList?.length || 0; + const maxItems = + (listType === 'Allowed' + ? settingsObj?.zimbraMailWhitelistMaxNumEntries + : settingsObj?.zimbraMailBlacklistMaxNumEntries) || 100; + + const isInsertEnabled = useMemo(() => itemsCount < maxItems, [itemsCount, maxItems]); + + const isInputValid = useMemo(() => isValidEmail(address) || address === '', [address]); + const isAddEnabled = useMemo( + () => isValidEmail(address) && isInsertEnabled, + [address, isInsertEnabled] + ); + + const warningMessage = useMemo( + () => + isInputValid + ? '' + : t('messages.invalid_sender_address', 'Please enter only e-mail addresses'), + [isInputValid] + ); + + const onRemove = useCallback( + (item: string) => { + const newList = filter(sendersList, (add) => add !== item); + + updateSettings({ + target: { + name: getPrefName(listType), + value: newList + } + }); + setSendersList(newList); + }, + [sendersList, updateSettings, listType] + ); + + return ( + + + + + + + {message} + + + + + + + ): void => + setAddress(e.target.value) + } + disabled={!isInsertEnabled} + /> + + + +