@remit/web-client 0.0.1
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.
- package/harness/build.mjs +84 -0
- package/harness/index.html +37 -0
- package/harness/vite-preset.ts +29 -0
- package/index.html +47 -0
- package/package.json +147 -0
- package/public/config.js +5 -0
- package/public/locales/de/common.json +1 -0
- package/public/locales/de/errors.json +1 -0
- package/public/locales/de/mail.json +13 -0
- package/public/locales/de/settings.json +1 -0
- package/public/locales/en/common.json +26 -0
- package/public/locales/en/errors.json +1 -0
- package/public/locales/en/mail.json +69 -0
- package/public/locales/en/settings.json +1 -0
- package/public/locales/fr/common.json +1 -0
- package/public/locales/fr/errors.json +1 -0
- package/public/locales/fr/mail.json +13 -0
- package/public/locales/fr/settings.json +1 -0
- package/public/locales/nl/common.json +1 -0
- package/public/locales/nl/errors.json +1 -0
- package/public/locales/nl/mail.json +13 -0
- package/public/locales/nl/settings.json +1 -0
- package/src/auth/AccountMenu.tsx +58 -0
- package/src/auth/AccountSession.tsx +19 -0
- package/src/auth/BetterAuthShell.tsx +198 -0
- package/src/auth/account-menu-mode.test.ts +64 -0
- package/src/auth/account-menu-mode.ts +28 -0
- package/src/auth/amplify-config.test.ts +158 -0
- package/src/auth/amplify-config.ts +60 -0
- package/src/auth/auth-error.test.ts +109 -0
- package/src/auth/auth-error.ts +107 -0
- package/src/auth/auth-interceptor.test.ts +86 -0
- package/src/auth/auth-interceptor.ts +23 -0
- package/src/auth/auth.css +334 -0
- package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
- package/src/auth/better-auth-config.ts +85 -0
- package/src/auth/better-auth-provider.tsx +20 -0
- package/src/auth/cognito/CognitoAccount.tsx +48 -0
- package/src/auth/cognito/CognitoShell.tsx +266 -0
- package/src/auth/cognito/cognito-token.test.ts +110 -0
- package/src/auth/cognito/cognito-token.ts +47 -0
- package/src/auth/cognito/cognito.css +21 -0
- package/src/auth/cognito-provider.tsx +22 -0
- package/src/auth/combined-provider.tsx +42 -0
- package/src/auth/composition.test.ts +136 -0
- package/src/auth/provider.tsx +63 -0
- package/src/auth/sign-out-visibility.test.ts +40 -0
- package/src/auth/sign-out-visibility.ts +10 -0
- package/src/auth/sign-up-disabled.test.ts +23 -0
- package/src/components/compose/AddressField.tsx +237 -0
- package/src/components/compose/ComposeBody.tsx +36 -0
- package/src/components/compose/ComposeForm.tsx +676 -0
- package/src/components/compose/ComposeProvider.tsx +133 -0
- package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
- package/src/components/compose/FromSelector.tsx +67 -0
- package/src/components/compose/FullCompose.tsx +82 -0
- package/src/components/compose/InlineCompose.tsx +29 -0
- package/src/components/compose/MobileComposeSheet.tsx +152 -0
- package/src/components/compose/PlateEditor.tsx +86 -0
- package/src/components/compose/PlateToolbar.tsx +110 -0
- package/src/components/compose/SubjectField.tsx +19 -0
- package/src/components/compose/draft-discard-banner.test.ts +76 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/sanitize-quote-html.ts +23 -0
- package/src/components/layout/AppShellSkeleton.tsx +53 -0
- package/src/components/layout/ComposeFab.tsx +86 -0
- package/src/components/layout/Drawer.tsx +105 -0
- package/src/components/mail/AutoMovedIndicator.tsx +48 -0
- package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
- package/src/components/mail/BlockedImagesNotice.tsx +41 -0
- package/src/components/mail/BriefPane.tsx +289 -0
- package/src/components/mail/ConversationView.tsx +462 -0
- package/src/components/mail/DailyBrief.tsx +451 -0
- package/src/components/mail/DraftsView.tsx +279 -0
- package/src/components/mail/FlaggedList.tsx +170 -0
- package/src/components/mail/FlaggedPane.tsx +217 -0
- package/src/components/mail/IntelligencePane.test.ts +123 -0
- package/src/components/mail/IntelligencePane.tsx +441 -0
- package/src/components/mail/MailListHeader.tsx +187 -0
- package/src/components/mail/MailNav.tsx +70 -0
- package/src/components/mail/MailSidebarAdapter.tsx +351 -0
- package/src/components/mail/MailViewChrome.tsx +95 -0
- package/src/components/mail/MailboxPane.tsx +1219 -0
- package/src/components/mail/MessageActionMenu.tsx +368 -0
- package/src/components/mail/MessageBody.tsx +207 -0
- package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
- package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
- package/src/components/mail/MessageCard.tsx +401 -0
- package/src/components/mail/MessageDetail.tsx +153 -0
- package/src/components/mail/MessageList.tsx +804 -0
- package/src/components/mail/MessageListItem.tsx +267 -0
- package/src/components/mail/MessageToolbar.render.test.ts +57 -0
- package/src/components/mail/MessageToolbar.tsx +166 -0
- package/src/components/mail/MobileMessageBar.tsx +142 -0
- package/src/components/mail/MoveToTrigger.tsx +250 -0
- package/src/components/mail/NavMenuButton.tsx +25 -0
- package/src/components/mail/OutboxPane.tsx +531 -0
- package/src/components/mail/PullToRefresh.tsx +24 -0
- package/src/components/mail/RawMessageView.tsx +84 -0
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
- package/src/components/mail/SelectionToolbar.tsx +139 -0
- package/src/components/mail/SpamRescue.render.test.ts +35 -0
- package/src/components/mail/SpamRescue.tsx +138 -0
- package/src/components/mail/SwipeableMessageRow.tsx +148 -0
- package/src/components/mail/message-body-error-banner-content.ts +87 -0
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
- package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
- package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
- package/src/components/mail/organize/OrganizePanel.tsx +344 -0
- package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
- package/src/components/settings/AccountFormPanel.test.ts +72 -0
- package/src/components/settings/AccountFormPanel.tsx +942 -0
- package/src/components/settings/DangerZone.tsx +30 -0
- package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
- package/src/components/settings/DeleteAccountDialog.tsx +224 -0
- package/src/components/settings/FiltersList.render.test.ts +71 -0
- package/src/components/settings/FiltersList.tsx +93 -0
- package/src/components/settings/account-form-helpers.test.ts +172 -0
- package/src/components/settings/account-form-helpers.ts +105 -0
- package/src/components/ui/AppVersion.tsx +50 -0
- package/src/components/ui/BugReportButton.tsx +45 -0
- package/src/components/ui/ConfirmDialog.tsx +125 -0
- package/src/components/ui/DropdownMenu.tsx +105 -0
- package/src/components/ui/EmptyState.tsx +11 -0
- package/src/components/ui/ErrorBanner.tsx +88 -0
- package/src/components/ui/ErrorBannerProvider.tsx +77 -0
- package/src/components/ui/ErrorBannerStack.tsx +34 -0
- package/src/components/ui/ErrorState.tsx +80 -0
- package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
- package/src/components/ui/FatalErrorOverlay.tsx +151 -0
- package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
- package/src/components/ui/SlidePanel.tsx +72 -0
- package/src/components/ui/error-banners.test.ts +207 -0
- package/src/components/ui/error-banners.ts +111 -0
- package/src/hooks/queries/keys.ts +38 -0
- package/src/hooks/useArchiveMailbox.ts +104 -0
- package/src/hooks/useAutoMovedBadge.ts +77 -0
- package/src/hooks/useCurrentMailboxName.ts +99 -0
- package/src/hooks/useDebouncedValue.ts +12 -0
- package/src/hooks/useDeleteMessages.test.ts +103 -0
- package/src/hooks/useDeleteMessages.ts +226 -0
- package/src/hooks/useFilters.ts +112 -0
- package/src/hooks/useIntelligenceData.test.ts +269 -0
- package/src/hooks/useIntelligenceData.ts +284 -0
- package/src/hooks/useIsDark.ts +27 -0
- package/src/hooks/useKeyboardNavigation.ts +192 -0
- package/src/hooks/useLayoutTier.test.ts +47 -0
- package/src/hooks/useLayoutTier.ts +44 -0
- package/src/hooks/useLongPress.test.ts +291 -0
- package/src/hooks/useLongPress.ts +87 -0
- package/src/hooks/useMailboxAccount.ts +62 -0
- package/src/hooks/useMailboxNameIndex.ts +32 -0
- package/src/hooks/useMarkAsRead.test.ts +165 -0
- package/src/hooks/useMarkAsRead.ts +319 -0
- package/src/hooks/useMediaQuery.ts +39 -0
- package/src/hooks/useMessageBodyContent.test.ts +370 -0
- package/src/hooks/useMessageBodyContent.ts +380 -0
- package/src/hooks/useMoveMessages.test.ts +63 -0
- package/src/hooks/useMoveMessages.ts +221 -0
- package/src/hooks/useOrganizeJob.ts +90 -0
- package/src/hooks/useOrganizePreview.ts +40 -0
- package/src/hooks/useRescueCandidates.test.ts +217 -0
- package/src/hooks/useRescueCandidates.ts +48 -0
- package/src/hooks/useSaveDraft.ts +118 -0
- package/src/hooks/useSelection.test.ts +75 -0
- package/src/hooks/useSelection.ts +211 -0
- package/src/hooks/useSemanticSearch.ts +94 -0
- package/src/hooks/useSignature.ts +69 -0
- package/src/hooks/useStaleAccountSync.test.ts +211 -0
- package/src/hooks/useStaleAccountSync.ts +246 -0
- package/src/hooks/useSwipeNavigation.test.ts +35 -0
- package/src/hooks/useSwipeNavigation.ts +99 -0
- package/src/hooks/useToggleStar.ts +187 -0
- package/src/hooks/useToggleTrusted.test.ts +102 -0
- package/src/hooks/useToggleTrusted.ts +161 -0
- package/src/hooks/useTriageKeyboard.ts +103 -0
- package/src/hooks/useTriggerSync.test.ts +24 -0
- package/src/hooks/useTriggerSync.ts +58 -0
- package/src/hooks/useUpdateAddressFlags.ts +125 -0
- package/src/hooks/useVisualViewport.test.ts +127 -0
- package/src/hooks/useVisualViewport.ts +51 -0
- package/src/index.css +19 -0
- package/src/lib/account-order.test.ts +84 -0
- package/src/lib/account-order.ts +13 -0
- package/src/lib/adjacent-message.test.ts +43 -0
- package/src/lib/adjacent-message.ts +20 -0
- package/src/lib/api.ts +82 -0
- package/src/lib/app-info.ts +15 -0
- package/src/lib/auto-moved.test.ts +140 -0
- package/src/lib/auto-moved.ts +68 -0
- package/src/lib/autodiscovery.test.ts +94 -0
- package/src/lib/autodiscovery.ts +354 -0
- package/src/lib/brief.test.ts +366 -0
- package/src/lib/brief.ts +180 -0
- package/src/lib/bug-report.test.ts +213 -0
- package/src/lib/bug-report.ts +176 -0
- package/src/lib/client.ts +36 -0
- package/src/lib/console-errors.test.ts +64 -0
- package/src/lib/console-errors.ts +92 -0
- package/src/lib/conversation-target.test.ts +68 -0
- package/src/lib/conversation-target.ts +59 -0
- package/src/lib/display-category.ts +20 -0
- package/src/lib/drafts.test.ts +241 -0
- package/src/lib/drafts.ts +121 -0
- package/src/lib/error-classifier.test.ts +133 -0
- package/src/lib/error-classifier.ts +96 -0
- package/src/lib/fatal-error.test.ts +97 -0
- package/src/lib/fatal-error.ts +165 -0
- package/src/lib/folder-roles.test.ts +113 -0
- package/src/lib/folder-roles.ts +112 -0
- package/src/lib/format.test.ts +115 -0
- package/src/lib/format.ts +198 -0
- package/src/lib/i18n.ts +36 -0
- package/src/lib/intelligence-pref.test.ts +57 -0
- package/src/lib/intelligence-pref.ts +25 -0
- package/src/lib/keymap-dispatch.test.ts +178 -0
- package/src/lib/keymap-dispatch.ts +187 -0
- package/src/lib/keymap.test.ts +49 -0
- package/src/lib/keymap.ts +178 -0
- package/src/lib/mail-context.ts +64 -0
- package/src/lib/mail-route.test.ts +80 -0
- package/src/lib/mail-route.ts +44 -0
- package/src/lib/message-body-source.test.ts +98 -0
- package/src/lib/message-body-source.ts +48 -0
- package/src/lib/move-targets.test.ts +195 -0
- package/src/lib/move-targets.ts +74 -0
- package/src/lib/onboarding-completion.test.ts +64 -0
- package/src/lib/onboarding-completion.ts +23 -0
- package/src/lib/organize/filter-status.test.ts +99 -0
- package/src/lib/organize/filter-status.ts +86 -0
- package/src/lib/organize/organize-copy.test.ts +93 -0
- package/src/lib/organize/organize-copy.ts +61 -0
- package/src/lib/organize/organize-model.test.ts +104 -0
- package/src/lib/organize/organize-model.ts +105 -0
- package/src/lib/organize/organize-poll.test.ts +37 -0
- package/src/lib/organize/organize-poll.ts +28 -0
- package/src/lib/outbox-status.test.ts +64 -0
- package/src/lib/outbox-status.ts +75 -0
- package/src/lib/plate-serializer.ts +164 -0
- package/src/lib/provider-presets.test.ts +96 -0
- package/src/lib/provider-presets.ts +81 -0
- package/src/lib/query-error-handler.test.ts +144 -0
- package/src/lib/query-error-handler.ts +34 -0
- package/src/lib/query-escalation.integration.test.ts +138 -0
- package/src/lib/recent-searches.test.ts +53 -0
- package/src/lib/recent-searches.ts +34 -0
- package/src/lib/rescue-candidates.test.ts +94 -0
- package/src/lib/rescue-candidates.ts +35 -0
- package/src/lib/rescue-telemetry.test.ts +75 -0
- package/src/lib/rescue-telemetry.ts +55 -0
- package/src/lib/rum-adapter.test.ts +299 -0
- package/src/lib/rum-adapter.ts +107 -0
- package/src/lib/saved-searches.test.ts +80 -0
- package/src/lib/saved-searches.ts +52 -0
- package/src/lib/search-pending.test.ts +61 -0
- package/src/lib/search-pending.ts +24 -0
- package/src/lib/search-query.test.ts +35 -0
- package/src/lib/search-query.ts +11 -0
- package/src/lib/search-result.test.ts +160 -0
- package/src/lib/search-result.ts +102 -0
- package/src/lib/search-schemas.ts +34 -0
- package/src/lib/search-token-index.test.ts +112 -0
- package/src/lib/search-token-index.ts +56 -0
- package/src/lib/search-tokens.test.ts +218 -0
- package/src/lib/search-tokens.ts +133 -0
- package/src/lib/telemetry-context.tsx +9 -0
- package/src/lib/telemetry.ts +17 -0
- package/src/lib/theme-preference.ts +46 -0
- package/src/lib/theme.ts +33 -0
- package/src/lib/utils.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/routeTree.gen.ts +392 -0
- package/src/router.test.ts +81 -0
- package/src/router.tsx +31 -0
- package/src/routes/__root.tsx +69 -0
- package/src/routes/index.tsx +7 -0
- package/src/routes/mail/$mailboxId.tsx +27 -0
- package/src/routes/mail/flagged.tsx +37 -0
- package/src/routes/mail/index.tsx +41 -0
- package/src/routes/mail/outbox.tsx +13 -0
- package/src/routes/mail.tsx +413 -0
- package/src/routes/onboarding.tsx +35 -0
- package/src/routes/settings/-accounts.test.ts +65 -0
- package/src/routes/settings/accounts.tsx +542 -0
- package/src/routes/settings/advanced.tsx +55 -0
- package/src/routes/settings/appearance.tsx +140 -0
- package/src/routes/settings/filters.tsx +141 -0
- package/src/routes/settings/folders.tsx +216 -0
- package/src/routes/settings/index.tsx +5 -0
- package/src/routes/settings/senders.tsx +466 -0
- package/src/routes/settings/suggested-vips.tsx +10 -0
- package/src/routes/settings.tsx +66 -0
- package/src/runtime-config.ts +75 -0
- package/src/shell/index.tsx +98 -0
- package/src/types/index.ts +73 -0
- package/src/vite-env.d.ts +4 -0
- package/vite.base.ts +43 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mailboxOperationsListMailboxesQueryKey,
|
|
3
|
+
messageBulkOperationsDeleteMessagesMutation,
|
|
4
|
+
threadDetailOperationsListThreadMessagesQueryKey,
|
|
5
|
+
threadOperationsListThreadsQueryKey,
|
|
6
|
+
threadOperationsSearchThreadsQueryKey,
|
|
7
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
8
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
9
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
10
|
+
import { useCallback } from "react";
|
|
11
|
+
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
12
|
+
import { formatErrorDetail } from "@/components/ui/error-banners";
|
|
13
|
+
|
|
14
|
+
interface UseDeleteMessagesOptions {
|
|
15
|
+
mailboxId: string;
|
|
16
|
+
threadId?: string;
|
|
17
|
+
accountId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Called once the optimistic removal has been applied. Use this to
|
|
20
|
+
* navigate away from a now-empty thread (e.g. clear `selectedMessageId`)
|
|
21
|
+
* before the server response arrives.
|
|
22
|
+
*/
|
|
23
|
+
onAfterOptimisticRemove?: (messageIds: string[]) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ThreadMessagesData {
|
|
27
|
+
items: RemitImapThreadMessageResponse[];
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ThreadsListPage {
|
|
32
|
+
items: RemitImapThreadMessageResponse[];
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ThreadsListData {
|
|
37
|
+
pages: ThreadsListPage[];
|
|
38
|
+
pageParams: Array<string | undefined>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SnapshotEntry<T> {
|
|
42
|
+
queryKey: readonly unknown[];
|
|
43
|
+
data: T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface DeleteContext {
|
|
47
|
+
threadMessagesPrefix: readonly unknown[];
|
|
48
|
+
threadsListPrefix: readonly unknown[];
|
|
49
|
+
threadsSearchPrefix: readonly unknown[];
|
|
50
|
+
previousThreadMessages: SnapshotEntry<ThreadMessagesData>[];
|
|
51
|
+
previousThreadsList: SnapshotEntry<ThreadsListData>[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pure helper: drop the messages in `messageIds` from a single page's items.
|
|
56
|
+
*
|
|
57
|
+
* Exported so the optimistic-removal math can be exercised without rendering
|
|
58
|
+
* React. Other identity is preserved — only items whose `messageId` is in
|
|
59
|
+
* the set are removed.
|
|
60
|
+
*/
|
|
61
|
+
export const removeMessagesFromItems = (
|
|
62
|
+
items: RemitImapThreadMessageResponse[],
|
|
63
|
+
messageIds: Set<string>,
|
|
64
|
+
): RemitImapThreadMessageResponse[] =>
|
|
65
|
+
items.filter((item) => !messageIds.has(item.messageId));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Pure helper: drop soft-deleted rows from a flattened thread list.
|
|
69
|
+
*
|
|
70
|
+
* Belt-and-braces guard against #212. The backend already excludes
|
|
71
|
+
* `isDeleted: true` rows from the inbox listing, but if a regression slips
|
|
72
|
+
* back in (or an eventual-consistency window briefly returns a soft-deleted
|
|
73
|
+
* row) the UI must not show it.
|
|
74
|
+
*/
|
|
75
|
+
export const dropDeletedThreads = (
|
|
76
|
+
items: RemitImapThreadMessageResponse[],
|
|
77
|
+
): RemitImapThreadMessageResponse[] =>
|
|
78
|
+
items.filter((item) => item.isDeleted !== true);
|
|
79
|
+
|
|
80
|
+
export const useDeleteMessages = ({
|
|
81
|
+
mailboxId,
|
|
82
|
+
threadId,
|
|
83
|
+
accountId,
|
|
84
|
+
onAfterOptimisticRemove,
|
|
85
|
+
}: UseDeleteMessagesOptions) => {
|
|
86
|
+
const queryClient = useQueryClient();
|
|
87
|
+
const { pushError } = useErrorBanners();
|
|
88
|
+
|
|
89
|
+
const { mutate, isPending } = useMutation({
|
|
90
|
+
...messageBulkOperationsDeleteMessagesMutation(),
|
|
91
|
+
onMutate: async (variables): Promise<DeleteContext> => {
|
|
92
|
+
const messageIds = new Set(variables.body.messageIds ?? []);
|
|
93
|
+
|
|
94
|
+
const threadMessagesPrefix = threadId
|
|
95
|
+
? threadDetailOperationsListThreadMessagesQueryKey({
|
|
96
|
+
path: { threadId },
|
|
97
|
+
})
|
|
98
|
+
: [];
|
|
99
|
+
const threadsListPrefix = threadOperationsListThreadsQueryKey({
|
|
100
|
+
path: { mailboxId },
|
|
101
|
+
});
|
|
102
|
+
const threadsSearchPrefix = threadOperationsSearchThreadsQueryKey({
|
|
103
|
+
path: { mailboxId },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await Promise.all([
|
|
107
|
+
queryClient.cancelQueries({ queryKey: threadMessagesPrefix }),
|
|
108
|
+
queryClient.cancelQueries({ queryKey: threadsListPrefix }),
|
|
109
|
+
queryClient.cancelQueries({ queryKey: threadsSearchPrefix }),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const previousThreadMessages = threadId
|
|
113
|
+
? queryClient
|
|
114
|
+
.getQueriesData<ThreadMessagesData>({
|
|
115
|
+
queryKey: threadMessagesPrefix,
|
|
116
|
+
})
|
|
117
|
+
.filter(
|
|
118
|
+
(entry): entry is [readonly unknown[], ThreadMessagesData] =>
|
|
119
|
+
entry[1] !== undefined,
|
|
120
|
+
)
|
|
121
|
+
.map(([queryKey, data]) => ({ queryKey, data }))
|
|
122
|
+
: [];
|
|
123
|
+
|
|
124
|
+
const previousThreadsList = queryClient
|
|
125
|
+
.getQueriesData<ThreadsListData>({ queryKey: threadsListPrefix })
|
|
126
|
+
.concat(
|
|
127
|
+
queryClient.getQueriesData<ThreadsListData>({
|
|
128
|
+
queryKey: threadsSearchPrefix,
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
.filter(
|
|
132
|
+
(entry): entry is [readonly unknown[], ThreadsListData] =>
|
|
133
|
+
entry[1] !== undefined,
|
|
134
|
+
)
|
|
135
|
+
.map(([queryKey, data]) => ({ queryKey, data }));
|
|
136
|
+
|
|
137
|
+
if (threadId) {
|
|
138
|
+
queryClient.setQueriesData<ThreadMessagesData>(
|
|
139
|
+
{ queryKey: threadMessagesPrefix },
|
|
140
|
+
(old) => {
|
|
141
|
+
if (!old) return old;
|
|
142
|
+
return {
|
|
143
|
+
...old,
|
|
144
|
+
items: removeMessagesFromItems(old.items, messageIds),
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const patchListData = (old: ThreadsListData | undefined) => {
|
|
151
|
+
if (!old) return old;
|
|
152
|
+
return {
|
|
153
|
+
...old,
|
|
154
|
+
pages: old.pages.map((page) => ({
|
|
155
|
+
...page,
|
|
156
|
+
items: removeMessagesFromItems(page.items, messageIds),
|
|
157
|
+
})),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
queryClient.setQueriesData<ThreadsListData>(
|
|
162
|
+
{ queryKey: threadsListPrefix },
|
|
163
|
+
patchListData,
|
|
164
|
+
);
|
|
165
|
+
queryClient.setQueriesData<ThreadsListData>(
|
|
166
|
+
{ queryKey: threadsSearchPrefix },
|
|
167
|
+
patchListData,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
onAfterOptimisticRemove?.(Array.from(messageIds));
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
threadMessagesPrefix,
|
|
174
|
+
threadsListPrefix,
|
|
175
|
+
threadsSearchPrefix,
|
|
176
|
+
previousThreadMessages,
|
|
177
|
+
previousThreadsList,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
onError: (err, vars, context) => {
|
|
181
|
+
if (context) {
|
|
182
|
+
for (const entry of context.previousThreadMessages) {
|
|
183
|
+
queryClient.setQueryData(entry.queryKey, entry.data);
|
|
184
|
+
}
|
|
185
|
+
for (const entry of context.previousThreadsList) {
|
|
186
|
+
queryClient.setQueryData(entry.queryKey, entry.data);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const count = vars.body.messageIds?.length ?? 0;
|
|
190
|
+
pushError({
|
|
191
|
+
title:
|
|
192
|
+
count > 1
|
|
193
|
+
? `Couldn't delete ${count} messages`
|
|
194
|
+
: "Couldn't delete this message",
|
|
195
|
+
detail: formatErrorDetail(err),
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
onSettled: (_data, _err, _vars, context) => {
|
|
199
|
+
if (!context) return;
|
|
200
|
+
if (threadId) {
|
|
201
|
+
queryClient.invalidateQueries({
|
|
202
|
+
queryKey: context.threadMessagesPrefix,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
queryClient.invalidateQueries({ queryKey: context.threadsListPrefix });
|
|
206
|
+
queryClient.invalidateQueries({ queryKey: context.threadsSearchPrefix });
|
|
207
|
+
if (accountId) {
|
|
208
|
+
queryClient.invalidateQueries({
|
|
209
|
+
queryKey: mailboxOperationsListMailboxesQueryKey({
|
|
210
|
+
path: { accountId },
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const deleteMessages = useCallback(
|
|
218
|
+
(messageIds: string[]) => {
|
|
219
|
+
if (messageIds.length === 0) return;
|
|
220
|
+
mutate({ body: { messageIds } });
|
|
221
|
+
},
|
|
222
|
+
[mutate],
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return { deleteMessages, isPending };
|
|
226
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
filterDetailOperationsDeleteFilterMutation,
|
|
3
|
+
filterOperationsCreateFilterMutation,
|
|
4
|
+
filterOperationsListFiltersOptions,
|
|
5
|
+
filterOperationsListFiltersQueryKey,
|
|
6
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
7
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { useCallback } from "react";
|
|
9
|
+
import {
|
|
10
|
+
buildCreateFilterInput,
|
|
11
|
+
type OrganizeDraft,
|
|
12
|
+
type OrganizeScope,
|
|
13
|
+
} from "@/lib/organize/organize-model";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* List the account's standing filters (Standing + Temporary). Expired
|
|
17
|
+
* Temporary filters stay in the list — they are shown distinctly, never hidden
|
|
18
|
+
* (RFC 034 Decision 1.2).
|
|
19
|
+
*/
|
|
20
|
+
export const useFilterList = (accountId: string | undefined) => {
|
|
21
|
+
const query = useQuery({
|
|
22
|
+
...filterOperationsListFiltersOptions({
|
|
23
|
+
path: { accountId: accountId ?? "" },
|
|
24
|
+
}),
|
|
25
|
+
enabled: !!accountId,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
filters: query.data?.items ?? [],
|
|
30
|
+
isPending: query.isPending,
|
|
31
|
+
isError: query.isError,
|
|
32
|
+
error: query.error,
|
|
33
|
+
refetch: query.refetch,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create the standing (`"these and new mail like this"`) or temporary
|
|
39
|
+
* (`"until <date>"`) filter for an organize draft. The two one-time scopes
|
|
40
|
+
* never create a filter (RFC 034 recap) and are not handled here.
|
|
41
|
+
*/
|
|
42
|
+
export const useCreateFilter = (accountId: string | undefined) => {
|
|
43
|
+
const queryClient = useQueryClient();
|
|
44
|
+
const mutation = useMutation({
|
|
45
|
+
...filterOperationsCreateFilterMutation(),
|
|
46
|
+
onSuccess: () => {
|
|
47
|
+
if (!accountId) return;
|
|
48
|
+
queryClient.invalidateQueries({
|
|
49
|
+
queryKey: filterOperationsListFiltersQueryKey({
|
|
50
|
+
path: { accountId },
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const { mutate } = mutation;
|
|
56
|
+
|
|
57
|
+
const createFilter = useCallback(
|
|
58
|
+
(
|
|
59
|
+
draft: OrganizeDraft,
|
|
60
|
+
scope: Extract<OrganizeScope, "standing" | "temporary">,
|
|
61
|
+
name: string,
|
|
62
|
+
) => {
|
|
63
|
+
if (!accountId) return;
|
|
64
|
+
mutate({
|
|
65
|
+
path: { accountId },
|
|
66
|
+
body: buildCreateFilterInput(draft, scope, name),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
[accountId, mutate],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
createFilter,
|
|
74
|
+
isPending: mutation.isPending,
|
|
75
|
+
isSuccess: mutation.isSuccess,
|
|
76
|
+
isError: mutation.isError,
|
|
77
|
+
error: mutation.error,
|
|
78
|
+
reset: mutation.reset,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const useDeleteFilter = (accountId: string | undefined) => {
|
|
83
|
+
const queryClient = useQueryClient();
|
|
84
|
+
const mutation = useMutation({
|
|
85
|
+
...filterDetailOperationsDeleteFilterMutation(),
|
|
86
|
+
onSuccess: () => {
|
|
87
|
+
if (!accountId) return;
|
|
88
|
+
queryClient.invalidateQueries({
|
|
89
|
+
queryKey: filterOperationsListFiltersQueryKey({
|
|
90
|
+
path: { accountId },
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const { mutate } = mutation;
|
|
96
|
+
|
|
97
|
+
const deleteFilter = useCallback(
|
|
98
|
+
(filterId: string) => {
|
|
99
|
+
if (!accountId) return;
|
|
100
|
+
mutate({ path: { accountId, filterId } });
|
|
101
|
+
},
|
|
102
|
+
[accountId, mutate],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
deleteFilter,
|
|
107
|
+
isPending: mutation.isPending,
|
|
108
|
+
deletingFilterId: mutation.isPending
|
|
109
|
+
? mutation.variables?.path.filterId
|
|
110
|
+
: undefined,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import type {
|
|
4
|
+
RemitImapAddressResponse,
|
|
5
|
+
RemitImapThreadMessageResponse,
|
|
6
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
7
|
+
import {
|
|
8
|
+
buildAuthenticityIntel,
|
|
9
|
+
buildSenderIntel,
|
|
10
|
+
} from "./useIntelligenceData.js";
|
|
11
|
+
|
|
12
|
+
// Jan 2025 timestamp — far enough in the past to not be "today"
|
|
13
|
+
const JAN_2025_MS = new Date("2025-01-15T12:00:00Z").getTime();
|
|
14
|
+
|
|
15
|
+
function makeThread(
|
|
16
|
+
overrides: Partial<RemitImapThreadMessageResponse> = {},
|
|
17
|
+
): RemitImapThreadMessageResponse {
|
|
18
|
+
return {
|
|
19
|
+
threadMessageId: "tm-1",
|
|
20
|
+
threadId: "t-1",
|
|
21
|
+
messageId: "m-1",
|
|
22
|
+
accountConfigId: "ac-1",
|
|
23
|
+
mailboxId: "mb-1",
|
|
24
|
+
fromEmail: "alice@example.com",
|
|
25
|
+
fromName: "Alice",
|
|
26
|
+
subject: "Hello",
|
|
27
|
+
senderTrust: "unknown",
|
|
28
|
+
isRead: false,
|
|
29
|
+
hasAttachment: false,
|
|
30
|
+
hasStars: false,
|
|
31
|
+
sentDate: "2025-01-15T12:00:00Z",
|
|
32
|
+
snippet: "",
|
|
33
|
+
...overrides,
|
|
34
|
+
} as RemitImapThreadMessageResponse;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeAddress(
|
|
38
|
+
overrides: Partial<RemitImapAddressResponse> = {},
|
|
39
|
+
): RemitImapAddressResponse {
|
|
40
|
+
return {
|
|
41
|
+
addressId: "addr-1",
|
|
42
|
+
accountConfigId: "ac-1",
|
|
43
|
+
localPart: "alice",
|
|
44
|
+
domain: "example.com",
|
|
45
|
+
normalizedEmail: "alice@example.com",
|
|
46
|
+
createdAt: JAN_2025_MS,
|
|
47
|
+
updatedAt: JAN_2025_MS,
|
|
48
|
+
...overrides,
|
|
49
|
+
} as RemitImapAddressResponse;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("buildSenderIntel", () => {
|
|
53
|
+
describe("counter wiring — counters present", () => {
|
|
54
|
+
test("surfaces inboundCount from address", () => {
|
|
55
|
+
const thread = makeThread();
|
|
56
|
+
const address = makeAddress({ inboundCount: 34 });
|
|
57
|
+
const result = buildSenderIntel(thread, address);
|
|
58
|
+
assert.equal(result.inboundCount, 34);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("surfaces replyCount from address", () => {
|
|
62
|
+
const thread = makeThread();
|
|
63
|
+
const address = makeAddress({ inboundCount: 34, replyCount: 12 });
|
|
64
|
+
const result = buildSenderIntel(thread, address);
|
|
65
|
+
assert.equal(result.replyCount, 12);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("surfaces both counters together", () => {
|
|
69
|
+
const thread = makeThread();
|
|
70
|
+
const address = makeAddress({ inboundCount: 34, replyCount: 12 });
|
|
71
|
+
const result = buildSenderIntel(thread, address);
|
|
72
|
+
assert.equal(result.inboundCount, 34);
|
|
73
|
+
assert.equal(result.replyCount, 12);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("suppression — counters absent", () => {
|
|
78
|
+
test("inboundCount is undefined when address has no inboundCount", () => {
|
|
79
|
+
const thread = makeThread();
|
|
80
|
+
const address = makeAddress(); // no inboundCount
|
|
81
|
+
const result = buildSenderIntel(thread, address);
|
|
82
|
+
// Must be undefined, not 0 — so SenderCard suppresses the engagement clause
|
|
83
|
+
assert.equal(result.inboundCount, undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("replyCount is undefined when address has no replyCount", () => {
|
|
87
|
+
const thread = makeThread();
|
|
88
|
+
const address = makeAddress({ inboundCount: 5 }); // no replyCount
|
|
89
|
+
const result = buildSenderIntel(thread, address);
|
|
90
|
+
assert.equal(result.replyCount, undefined);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("both counters are undefined when address is undefined", () => {
|
|
94
|
+
const thread = makeThread();
|
|
95
|
+
const result = buildSenderIntel(thread, undefined);
|
|
96
|
+
assert.equal(result.inboundCount, undefined);
|
|
97
|
+
assert.equal(result.replyCount, undefined);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("does not coerce absent inboundCount to 0", () => {
|
|
101
|
+
// Regression guard: the old code left counters as undefined so
|
|
102
|
+
// SenderCard could suppress "0 received · you've never replied".
|
|
103
|
+
// Ensure undefined stays undefined, never becomes 0.
|
|
104
|
+
const thread = makeThread();
|
|
105
|
+
const address = makeAddress(); // inboundCount absent
|
|
106
|
+
const result = buildSenderIntel(thread, address);
|
|
107
|
+
assert.notEqual(result.inboundCount, 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("basic sender fields", () => {
|
|
112
|
+
test("uses fromName when present", () => {
|
|
113
|
+
const thread = makeThread({
|
|
114
|
+
fromName: "Alice Smith",
|
|
115
|
+
fromEmail: "alice@example.com",
|
|
116
|
+
});
|
|
117
|
+
const result = buildSenderIntel(thread, undefined);
|
|
118
|
+
assert.equal(result.name, "Alice Smith");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("falls back to fromEmail when fromName is absent", () => {
|
|
122
|
+
const thread = makeThread({
|
|
123
|
+
fromName: undefined,
|
|
124
|
+
fromEmail: "alice@example.com",
|
|
125
|
+
});
|
|
126
|
+
const result = buildSenderIntel(thread, undefined);
|
|
127
|
+
assert.equal(result.name, "alice@example.com");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("falls back to 'Unknown' when both name and email are absent", () => {
|
|
131
|
+
const thread = makeThread({ fromName: undefined, fromEmail: undefined });
|
|
132
|
+
const result = buildSenderIntel(thread, undefined);
|
|
133
|
+
assert.equal(result.name, "Unknown");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("passes senderTrust through", () => {
|
|
137
|
+
const thread = makeThread({ senderTrust: "vip" });
|
|
138
|
+
const result = buildSenderIntel(thread, undefined);
|
|
139
|
+
assert.equal(result.trust, "vip");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("firstSeenLabel is 'unknown' when no address", () => {
|
|
143
|
+
const thread = makeThread();
|
|
144
|
+
const result = buildSenderIntel(thread, undefined);
|
|
145
|
+
assert.equal(result.firstSeenLabel, "unknown");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("firstSeenLabel is formatted when address is present", () => {
|
|
149
|
+
const thread = makeThread();
|
|
150
|
+
const address = makeAddress({ createdAt: JAN_2025_MS });
|
|
151
|
+
const result = buildSenderIntel(thread, address);
|
|
152
|
+
// Should be something like "Jan 2025", not "unknown" or "today"
|
|
153
|
+
assert.notEqual(result.firstSeenLabel, "unknown");
|
|
154
|
+
assert.ok(result.firstSeenLabel.length > 0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("addressUnverified flag", () => {
|
|
159
|
+
test("false for a normal address", () => {
|
|
160
|
+
const result = buildSenderIntel(
|
|
161
|
+
makeThread({ fromEmail: "alice@example.com" }),
|
|
162
|
+
undefined,
|
|
163
|
+
);
|
|
164
|
+
assert.equal(result.addressUnverified, false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("true when the domain has no dot (placeholder junk)", () => {
|
|
168
|
+
const result = buildSenderIntel(
|
|
169
|
+
makeThread({ fromEmail: "missing_mailbox@missing_domain" }),
|
|
170
|
+
undefined,
|
|
171
|
+
);
|
|
172
|
+
assert.equal(result.addressUnverified, true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("true when the address is missing", () => {
|
|
176
|
+
const result = buildSenderIntel(
|
|
177
|
+
makeThread({ fromEmail: undefined }),
|
|
178
|
+
undefined,
|
|
179
|
+
);
|
|
180
|
+
assert.equal(result.addressUnverified, true);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("buildAuthenticityIntel", () => {
|
|
186
|
+
test("caution when there is no authenticity signal", () => {
|
|
187
|
+
const thread = makeThread({ fromEmail: "alice@example.com" });
|
|
188
|
+
const result = buildAuthenticityIntel(thread, 0);
|
|
189
|
+
assert.equal(result.verdict, "caution");
|
|
190
|
+
assert.match(result.summary, /can't verify the sender/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("aligned when a signal is present and does not mismatch", () => {
|
|
194
|
+
const thread = makeThread({
|
|
195
|
+
fromEmail: "alice@example.com",
|
|
196
|
+
authenticity: {
|
|
197
|
+
fromDomain: "example.com",
|
|
198
|
+
dkimDomain: "example.com",
|
|
199
|
+
dkimMismatch: false,
|
|
200
|
+
},
|
|
201
|
+
} as Partial<RemitImapThreadMessageResponse>);
|
|
202
|
+
const result = buildAuthenticityIntel(thread, 0);
|
|
203
|
+
assert.equal(result.verdict, "aligned");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("mismatch when the signing domain does not align", () => {
|
|
207
|
+
const thread = makeThread({
|
|
208
|
+
fromEmail: "security@your-bank.example",
|
|
209
|
+
fromName: "Your Bank",
|
|
210
|
+
authenticity: {
|
|
211
|
+
fromDomain: "your-bank.example",
|
|
212
|
+
dkimDomain: "mailer.evil.example",
|
|
213
|
+
dkimMismatch: true,
|
|
214
|
+
},
|
|
215
|
+
} as Partial<RemitImapThreadMessageResponse>);
|
|
216
|
+
const result = buildAuthenticityIntel(thread, 3);
|
|
217
|
+
assert.equal(result.verdict, "mismatch");
|
|
218
|
+
assert.equal(result.addressUnreadable, undefined);
|
|
219
|
+
assert.equal(result.similarCount, 3);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("no user-facing copy names the verification mechanism", () => {
|
|
223
|
+
const verdicts = [
|
|
224
|
+
buildAuthenticityIntel(makeThread(), 0),
|
|
225
|
+
buildAuthenticityIntel(
|
|
226
|
+
makeThread({ fromEmail: "x@no-signal.example" }),
|
|
227
|
+
0,
|
|
228
|
+
),
|
|
229
|
+
buildAuthenticityIntel(
|
|
230
|
+
makeThread({
|
|
231
|
+
fromEmail: "a@bank.example",
|
|
232
|
+
authenticity: {
|
|
233
|
+
fromDomain: "bank.example",
|
|
234
|
+
dkimDomain: "evil.example",
|
|
235
|
+
dkimMismatch: true,
|
|
236
|
+
},
|
|
237
|
+
} as Partial<RemitImapThreadMessageResponse>),
|
|
238
|
+
0,
|
|
239
|
+
),
|
|
240
|
+
];
|
|
241
|
+
for (const v of verdicts) {
|
|
242
|
+
assert.doesNotMatch(v.summary, /DKIM|SPF|DMARC/i);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("unparseable sender drives the red tier", () => {
|
|
247
|
+
test("mismatch + addressUnreadable when the domain has no dot", () => {
|
|
248
|
+
const thread = makeThread({
|
|
249
|
+
fromEmail: "missing_mailbox@missing_domain",
|
|
250
|
+
});
|
|
251
|
+
const result = buildAuthenticityIntel(thread, 0);
|
|
252
|
+
assert.equal(result.verdict, "mismatch");
|
|
253
|
+
assert.equal(result.addressUnreadable, true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("red tier even when a clean DKIM signal is present", () => {
|
|
257
|
+
const thread = makeThread({
|
|
258
|
+
fromEmail: "missing_mailbox@missing_domain",
|
|
259
|
+
authenticity: {
|
|
260
|
+
fromDomain: "missing_domain",
|
|
261
|
+
dkimMismatch: false,
|
|
262
|
+
},
|
|
263
|
+
} as Partial<RemitImapThreadMessageResponse>);
|
|
264
|
+
const result = buildAuthenticityIntel(thread, 0);
|
|
265
|
+
assert.equal(result.verdict, "mismatch");
|
|
266
|
+
assert.equal(result.addressUnreadable, true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|