@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,942 @@
|
|
|
1
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
2
|
+
import {
|
|
3
|
+
accountDetailOperationsUpdateAccountMutation,
|
|
4
|
+
accountOperationsCreateAccountMutation,
|
|
5
|
+
accountOperationsTestConnectionMutation,
|
|
6
|
+
configOperationsGetConfigQueryKey,
|
|
7
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
8
|
+
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
9
|
+
import { Button, Input, Select, securityToApi } from "@remit/ui";
|
|
10
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { Check, Loader2, X } from "lucide-react";
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
|
+
import { useForm } from "react-hook-form";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { useSignature } from "../../hooks/useSignature";
|
|
16
|
+
import {
|
|
17
|
+
getPresetById,
|
|
18
|
+
PROVIDER_PRESETS,
|
|
19
|
+
type ProviderPreset,
|
|
20
|
+
} from "../../lib/provider-presets.js";
|
|
21
|
+
import { cn } from "../../lib/utils";
|
|
22
|
+
import { SlidePanel } from "../ui/SlidePanel";
|
|
23
|
+
import {
|
|
24
|
+
appendAppPasswordHint,
|
|
25
|
+
computeSmtpAutoFill,
|
|
26
|
+
deriveSmtpHostFromImap,
|
|
27
|
+
} from "./account-form-helpers.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when the account authenticates via Microsoft OAuth — not a
|
|
31
|
+
* password-based IMAP account. Used to hide irrelevant credential /
|
|
32
|
+
* server fields and show a read-only summary instead.
|
|
33
|
+
*
|
|
34
|
+
* Exported for unit tests.
|
|
35
|
+
*/
|
|
36
|
+
export const isOAuthMicrosoftAccount = (
|
|
37
|
+
account: Pick<RemitImapAccountResponse, "authType">,
|
|
38
|
+
): boolean => account.authType === "oauthMicrosoft";
|
|
39
|
+
|
|
40
|
+
// Placeholder shown when editing - indicates password exists but isn't shown
|
|
41
|
+
const PASSWORD_PLACEHOLDER = "••••••••••";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Derive the initial state of the "use different SMTP credentials" checkbox
|
|
45
|
+
* from a persisted account. Returns true when the account stores an SMTP
|
|
46
|
+
* username distinct from the IMAP username (i.e. the user previously opted
|
|
47
|
+
* in to separate SMTP credentials).
|
|
48
|
+
*
|
|
49
|
+
* The API response intentionally omits the SMTP password, so `smtpUsername`
|
|
50
|
+
* is the only available signal.
|
|
51
|
+
*/
|
|
52
|
+
export const deriveUseDifferentSmtpCreds = (
|
|
53
|
+
account: Pick<RemitImapAccountResponse, "username"> & {
|
|
54
|
+
smtpUsername?: string;
|
|
55
|
+
},
|
|
56
|
+
): boolean => {
|
|
57
|
+
const smtpUsername = account.smtpUsername?.trim();
|
|
58
|
+
if (!smtpUsername) return false;
|
|
59
|
+
return smtpUsername !== account.username;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const accountFormSchema = z.object({
|
|
63
|
+
email: z.string().email().min(1, "Email is required"),
|
|
64
|
+
displayName: z.string().optional(),
|
|
65
|
+
username: z.string().optional(),
|
|
66
|
+
password: z.string().optional(),
|
|
67
|
+
imapHost: z.string().min(1, "IMAP host is required"),
|
|
68
|
+
imapPort: z.coerce.number().int().min(1).max(65535),
|
|
69
|
+
imapTls: z.boolean(),
|
|
70
|
+
imapStartTls: z.boolean(),
|
|
71
|
+
smtpHost: z.string().optional(),
|
|
72
|
+
smtpPort: z.coerce.number().int().min(1).max(65535).optional(),
|
|
73
|
+
smtpTls: z.boolean().optional(),
|
|
74
|
+
smtpStartTls: z.boolean().optional(),
|
|
75
|
+
smtpUsername: z.string().optional(),
|
|
76
|
+
smtpPassword: z.string().optional(),
|
|
77
|
+
useDifferentSmtpCreds: z.boolean().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
type AccountFormData = z.infer<typeof accountFormSchema>;
|
|
81
|
+
|
|
82
|
+
interface AccountFormPanelProps {
|
|
83
|
+
isOpen: boolean;
|
|
84
|
+
onClose: () => void;
|
|
85
|
+
account?: RemitImapAccountResponse;
|
|
86
|
+
focusSmtp?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const AccountFormPanel = ({
|
|
90
|
+
isOpen,
|
|
91
|
+
onClose,
|
|
92
|
+
account,
|
|
93
|
+
focusSmtp,
|
|
94
|
+
}: AccountFormPanelProps) => {
|
|
95
|
+
const queryClient = useQueryClient();
|
|
96
|
+
const isEditing = !!account;
|
|
97
|
+
|
|
98
|
+
// Track if user has modified the password field
|
|
99
|
+
const [passwordModified, setPasswordModified] = useState(false);
|
|
100
|
+
|
|
101
|
+
// Selected provider preset (empty string = "Custom / other", manual entry)
|
|
102
|
+
const [presetId, setPresetId] = useState("");
|
|
103
|
+
// When a preset is selected its server fields are read-only until the
|
|
104
|
+
// user opts into manual editing via the Advanced toggle.
|
|
105
|
+
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
106
|
+
const selectedPreset: ProviderPreset | undefined = getPresetById(presetId);
|
|
107
|
+
const serversLocked = !!selectedPreset && !advancedOpen;
|
|
108
|
+
|
|
109
|
+
const smtpSectionRef = useRef<HTMLElement | null>(null);
|
|
110
|
+
|
|
111
|
+
const form = useForm<AccountFormData>({
|
|
112
|
+
resolver: zodResolver(accountFormSchema),
|
|
113
|
+
defaultValues: {
|
|
114
|
+
email: "",
|
|
115
|
+
displayName: "",
|
|
116
|
+
username: "",
|
|
117
|
+
password: "",
|
|
118
|
+
imapHost: "",
|
|
119
|
+
imapPort: 993,
|
|
120
|
+
imapTls: true,
|
|
121
|
+
imapStartTls: false,
|
|
122
|
+
smtpHost: "",
|
|
123
|
+
smtpPort: 587,
|
|
124
|
+
smtpTls: false,
|
|
125
|
+
smtpStartTls: true,
|
|
126
|
+
useDifferentSmtpCreds: false,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Populate form when editing an existing account
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (account) {
|
|
133
|
+
const useDifferentSmtpCreds = deriveUseDifferentSmtpCreds(account);
|
|
134
|
+
form.reset({
|
|
135
|
+
email: account.email,
|
|
136
|
+
displayName: account.displayName || "",
|
|
137
|
+
username: account.username || "",
|
|
138
|
+
password: PASSWORD_PLACEHOLDER, // Show placeholder to indicate password exists
|
|
139
|
+
imapHost: account.imapHost,
|
|
140
|
+
imapPort: account.imapPort,
|
|
141
|
+
imapTls: account.imapTls,
|
|
142
|
+
imapStartTls: account.imapStartTls,
|
|
143
|
+
smtpHost: account.smtpHost || "",
|
|
144
|
+
smtpPort: account.smtpPort || 587,
|
|
145
|
+
smtpTls: account.smtpTls || false,
|
|
146
|
+
smtpStartTls: account.smtpStartTls || true,
|
|
147
|
+
smtpUsername: useDifferentSmtpCreds ? account.smtpUsername || "" : "",
|
|
148
|
+
smtpPassword: "",
|
|
149
|
+
useDifferentSmtpCreds,
|
|
150
|
+
});
|
|
151
|
+
setPasswordModified(false);
|
|
152
|
+
} else {
|
|
153
|
+
form.reset({
|
|
154
|
+
email: "",
|
|
155
|
+
displayName: "",
|
|
156
|
+
username: "",
|
|
157
|
+
password: "",
|
|
158
|
+
imapHost: "",
|
|
159
|
+
imapPort: 993,
|
|
160
|
+
imapTls: true,
|
|
161
|
+
imapStartTls: false,
|
|
162
|
+
smtpHost: "",
|
|
163
|
+
smtpPort: 587,
|
|
164
|
+
smtpTls: false,
|
|
165
|
+
smtpStartTls: true,
|
|
166
|
+
useDifferentSmtpCreds: false,
|
|
167
|
+
});
|
|
168
|
+
setPasswordModified(false);
|
|
169
|
+
}
|
|
170
|
+
}, [account, form]);
|
|
171
|
+
|
|
172
|
+
const testMutation = useMutation({
|
|
173
|
+
...accountOperationsTestConnectionMutation(),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const createMutation = useMutation({
|
|
177
|
+
...accountOperationsCreateAccountMutation(),
|
|
178
|
+
onSuccess: () => {
|
|
179
|
+
queryClient.invalidateQueries({
|
|
180
|
+
queryKey: configOperationsGetConfigQueryKey(),
|
|
181
|
+
});
|
|
182
|
+
onClose();
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const updateMutation = useMutation({
|
|
187
|
+
...accountDetailOperationsUpdateAccountMutation(),
|
|
188
|
+
onSuccess: () => {
|
|
189
|
+
queryClient.invalidateQueries({
|
|
190
|
+
queryKey: configOperationsGetConfigQueryKey(),
|
|
191
|
+
});
|
|
192
|
+
onClose();
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const handleTestImap = () => {
|
|
197
|
+
const values = form.getValues();
|
|
198
|
+
// If editing and password not modified, send accountId so server uses stored password
|
|
199
|
+
const useStoredPassword = isEditing && !passwordModified;
|
|
200
|
+
testMutation.mutate({
|
|
201
|
+
body: {
|
|
202
|
+
accountId: useStoredPassword ? account?.accountId : undefined,
|
|
203
|
+
username: values.username || values.email,
|
|
204
|
+
password: useStoredPassword ? undefined : values.password || undefined,
|
|
205
|
+
imapHost: values.imapHost,
|
|
206
|
+
imapPort: Number(values.imapPort),
|
|
207
|
+
imapTls: values.imapTls,
|
|
208
|
+
imapStartTls: values.imapStartTls,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleTestSmtp = () => {
|
|
214
|
+
const values = form.getValues();
|
|
215
|
+
// If editing and password not modified, send accountId so server uses stored password
|
|
216
|
+
const useStoredPassword = isEditing && !passwordModified;
|
|
217
|
+
testMutation.mutate({
|
|
218
|
+
body: {
|
|
219
|
+
accountId: useStoredPassword ? account?.accountId : undefined,
|
|
220
|
+
username: values.username || values.email,
|
|
221
|
+
password: useStoredPassword ? undefined : values.password || undefined,
|
|
222
|
+
imapHost: values.imapHost,
|
|
223
|
+
imapPort: Number(values.imapPort),
|
|
224
|
+
imapTls: values.imapTls,
|
|
225
|
+
imapStartTls: values.imapStartTls,
|
|
226
|
+
smtpHost: values.smtpHost || undefined,
|
|
227
|
+
smtpPort: values.smtpPort ? Number(values.smtpPort) : undefined,
|
|
228
|
+
smtpTls: values.smtpTls,
|
|
229
|
+
smtpStartTls: values.smtpStartTls,
|
|
230
|
+
smtpUsername: values.useDifferentSmtpCreds
|
|
231
|
+
? values.smtpUsername
|
|
232
|
+
: undefined,
|
|
233
|
+
smtpPassword: values.useDifferentSmtpCreds
|
|
234
|
+
? values.smtpPassword
|
|
235
|
+
: undefined,
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const handleSubmit = form.handleSubmit((values) => {
|
|
241
|
+
// Validate password for new accounts
|
|
242
|
+
if (!isEditing && !values.password) {
|
|
243
|
+
form.setError("password", { message: "Password is required" });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Safety net: when the user filled IMAP but left SMTP blank,
|
|
248
|
+
// derive sensible SMTP defaults from the IMAP host so the account
|
|
249
|
+
// can actually send mail. Only fills blanks — never overrides
|
|
250
|
+
// values the user typed. See issue #196.
|
|
251
|
+
const autoFill = computeSmtpAutoFill({
|
|
252
|
+
imapHost: values.imapHost,
|
|
253
|
+
smtpHost: values.smtpHost,
|
|
254
|
+
smtpPort: values.smtpPort,
|
|
255
|
+
smtpTls: values.smtpTls,
|
|
256
|
+
smtpStartTls: values.smtpStartTls,
|
|
257
|
+
});
|
|
258
|
+
if (autoFill) {
|
|
259
|
+
form.setValue("smtpHost", autoFill.smtpHost);
|
|
260
|
+
form.setValue("smtpPort", autoFill.smtpPort);
|
|
261
|
+
form.setValue("smtpTls", autoFill.smtpTls);
|
|
262
|
+
form.setValue("smtpStartTls", autoFill.smtpStartTls);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const effectiveSmtpHost = autoFill?.smtpHost ?? values.smtpHost;
|
|
266
|
+
const effectiveSmtpPort = autoFill?.smtpPort ?? values.smtpPort;
|
|
267
|
+
const effectiveSmtpTls = autoFill?.smtpTls ?? values.smtpTls;
|
|
268
|
+
const effectiveSmtpStartTls = autoFill?.smtpStartTls ?? values.smtpStartTls;
|
|
269
|
+
|
|
270
|
+
const baseBody = {
|
|
271
|
+
email: values.email,
|
|
272
|
+
displayName: values.displayName?.trim() || undefined,
|
|
273
|
+
username: values.username || undefined,
|
|
274
|
+
imapHost: values.imapHost,
|
|
275
|
+
imapPort: Number(values.imapPort),
|
|
276
|
+
imapTls: values.imapTls,
|
|
277
|
+
imapStartTls: values.imapStartTls,
|
|
278
|
+
smtpHost: effectiveSmtpHost || undefined,
|
|
279
|
+
smtpPort: effectiveSmtpPort ? Number(effectiveSmtpPort) : undefined,
|
|
280
|
+
smtpTls: effectiveSmtpTls,
|
|
281
|
+
smtpStartTls: effectiveSmtpStartTls,
|
|
282
|
+
smtpUsername: values.useDifferentSmtpCreds
|
|
283
|
+
? values.smtpUsername || undefined
|
|
284
|
+
: undefined,
|
|
285
|
+
smtpPassword: values.useDifferentSmtpCreds
|
|
286
|
+
? values.smtpPassword || undefined
|
|
287
|
+
: undefined,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (isEditing && account) {
|
|
291
|
+
const body = {
|
|
292
|
+
...baseBody,
|
|
293
|
+
// Only send password if it was modified
|
|
294
|
+
password: passwordModified ? values.password : undefined,
|
|
295
|
+
};
|
|
296
|
+
updateMutation.mutate({ path: { accountId: account.accountId }, body });
|
|
297
|
+
} else {
|
|
298
|
+
// Password is validated above for new accounts
|
|
299
|
+
const body = {
|
|
300
|
+
...baseBody,
|
|
301
|
+
password: values.password as string,
|
|
302
|
+
};
|
|
303
|
+
createMutation.mutate({ body });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const isSaving = createMutation.isPending || updateMutation.isPending;
|
|
308
|
+
|
|
309
|
+
const {
|
|
310
|
+
signature,
|
|
311
|
+
setSignature,
|
|
312
|
+
isSaving: isSignatureSaving,
|
|
313
|
+
} = useSignature(account?.accountId);
|
|
314
|
+
const [signatureText, setSignatureText] = useState(signature.plainText);
|
|
315
|
+
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
setSignatureText(signature.plainText);
|
|
318
|
+
}, [signature.plainText]);
|
|
319
|
+
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
if (!isOpen || !focusSmtp) return;
|
|
322
|
+
const el = smtpSectionRef.current;
|
|
323
|
+
if (!el) return;
|
|
324
|
+
const id = window.setTimeout(() => {
|
|
325
|
+
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
326
|
+
}, 250);
|
|
327
|
+
return () => window.clearTimeout(id);
|
|
328
|
+
}, [isOpen, focusSmtp]);
|
|
329
|
+
|
|
330
|
+
const handleSignatureBlur = useCallback(() => {
|
|
331
|
+
if (signatureText !== signature.plainText) {
|
|
332
|
+
setSignature(signatureText, signatureText);
|
|
333
|
+
}
|
|
334
|
+
}, [signatureText, signature.plainText, setSignature]);
|
|
335
|
+
|
|
336
|
+
const handleSecurityChange = (type: "tls" | "starttls" | "none") => {
|
|
337
|
+
const { tls, startTls } = securityToApi(type);
|
|
338
|
+
form.setValue("imapTls", tls);
|
|
339
|
+
form.setValue("imapStartTls", startTls);
|
|
340
|
+
|
|
341
|
+
const currentPort = form.getValues("imapPort");
|
|
342
|
+
const isDefaultPort = currentPort === 993 || currentPort === 143;
|
|
343
|
+
if (isDefaultPort) form.setValue("imapPort", type === "tls" ? 993 : 143);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const handleSmtpSecurityChange = (type: "tls" | "starttls" | "none") => {
|
|
347
|
+
const { tls, startTls } = securityToApi(type);
|
|
348
|
+
form.setValue("smtpTls", tls);
|
|
349
|
+
form.setValue("smtpStartTls", startTls);
|
|
350
|
+
|
|
351
|
+
const currentPort = form.getValues("smtpPort");
|
|
352
|
+
const isDefaultPort =
|
|
353
|
+
currentPort === 465 || currentPort === 587 || currentPort === 25;
|
|
354
|
+
if (isDefaultPort) {
|
|
355
|
+
const port = type === "tls" ? 465 : type === "starttls" ? 587 : 25;
|
|
356
|
+
form.setValue("smtpPort", port);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const handlePrefillFromImap = () => {
|
|
361
|
+
const imapHost = form.getValues("imapHost");
|
|
362
|
+
form.setValue("smtpHost", deriveSmtpHostFromImap(imapHost));
|
|
363
|
+
form.setValue("smtpTls", false);
|
|
364
|
+
form.setValue("smtpStartTls", true);
|
|
365
|
+
form.setValue("smtpPort", 587);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handlePresetChange = (id: string) => {
|
|
369
|
+
setPresetId(id);
|
|
370
|
+
setAdvancedOpen(false);
|
|
371
|
+
const preset = getPresetById(id);
|
|
372
|
+
if (!preset) return;
|
|
373
|
+
|
|
374
|
+
const imapSecurity = securityToApi(preset.imap.security);
|
|
375
|
+
form.setValue("imapHost", preset.imap.host);
|
|
376
|
+
form.setValue("imapPort", preset.imap.port);
|
|
377
|
+
form.setValue("imapTls", imapSecurity.tls);
|
|
378
|
+
form.setValue("imapStartTls", imapSecurity.startTls);
|
|
379
|
+
|
|
380
|
+
const smtpSecurity = securityToApi(preset.smtp.security);
|
|
381
|
+
form.setValue("smtpHost", preset.smtp.host);
|
|
382
|
+
form.setValue("smtpPort", preset.smtp.port);
|
|
383
|
+
form.setValue("smtpTls", smtpSecurity.tls);
|
|
384
|
+
form.setValue("smtpStartTls", smtpSecurity.startTls);
|
|
385
|
+
|
|
386
|
+
const email = form.getValues("email").trim();
|
|
387
|
+
const username = form.getValues("username")?.trim();
|
|
388
|
+
if (email && !username) form.setValue("username", email);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const presetHint = selectedPreset?.passwordHelp.text;
|
|
392
|
+
const imapError = appendAppPasswordHint(
|
|
393
|
+
testMutation.data?.imapError,
|
|
394
|
+
presetHint,
|
|
395
|
+
);
|
|
396
|
+
const smtpError = appendAppPasswordHint(
|
|
397
|
+
testMutation.data?.smtpError,
|
|
398
|
+
presetHint,
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const isOAuthAccount = account ? isOAuthMicrosoftAccount(account) : false;
|
|
402
|
+
|
|
403
|
+
// OAuth Microsoft accounts: show a read-only summary panel instead of
|
|
404
|
+
// the credential / server configuration form.
|
|
405
|
+
if (isOAuthAccount && isEditing) {
|
|
406
|
+
return (
|
|
407
|
+
<SlidePanel
|
|
408
|
+
isOpen={isOpen}
|
|
409
|
+
onClose={onClose}
|
|
410
|
+
title="Edit Account"
|
|
411
|
+
footer={
|
|
412
|
+
<Button type="button" variant="secondary" onClick={onClose}>
|
|
413
|
+
Close
|
|
414
|
+
</Button>
|
|
415
|
+
}
|
|
416
|
+
>
|
|
417
|
+
<div className="space-y-6">
|
|
418
|
+
<section>
|
|
419
|
+
<h3 className="text-2xs font-semibold text-fg-subtle uppercase tracking-wider mb-3">
|
|
420
|
+
Microsoft 365 Account
|
|
421
|
+
</h3>
|
|
422
|
+
<div className="space-y-3">
|
|
423
|
+
<div className="rounded-md border border-line bg-surface-sunken px-3 py-2.5 text-sm text-fg-muted">
|
|
424
|
+
<span className="block text-2xs font-medium uppercase tracking-wider text-fg-subtle mb-1">
|
|
425
|
+
Email
|
|
426
|
+
</span>
|
|
427
|
+
<span className="text-fg">{account?.email}</span>
|
|
428
|
+
</div>
|
|
429
|
+
<div className="rounded-md border border-line bg-surface-sunken px-3 py-2.5 text-sm text-fg-muted">
|
|
430
|
+
<span className="block text-2xs font-medium uppercase tracking-wider text-fg-subtle mb-1">
|
|
431
|
+
Authentication
|
|
432
|
+
</span>
|
|
433
|
+
<span className="text-fg">Microsoft OAuth (XOAUTH2)</span>
|
|
434
|
+
</div>
|
|
435
|
+
<p className="text-xs text-fg-muted">
|
|
436
|
+
Server settings are managed automatically for Microsoft 365
|
|
437
|
+
accounts. To update credentials, use the Reconnect button.
|
|
438
|
+
</p>
|
|
439
|
+
</div>
|
|
440
|
+
</section>
|
|
441
|
+
{isEditing && (
|
|
442
|
+
<section>
|
|
443
|
+
<h3 className="text-2xs font-semibold text-fg-subtle uppercase tracking-wider mb-3">
|
|
444
|
+
Signature
|
|
445
|
+
</h3>
|
|
446
|
+
<div className="space-y-3">
|
|
447
|
+
<div>
|
|
448
|
+
<label
|
|
449
|
+
htmlFor="email-sig-view"
|
|
450
|
+
className="text-sm font-medium mb-1.5 block"
|
|
451
|
+
>
|
|
452
|
+
Email Signature
|
|
453
|
+
</label>
|
|
454
|
+
<textarea
|
|
455
|
+
id="email-sig-view"
|
|
456
|
+
value={signatureText}
|
|
457
|
+
onChange={(e) => setSignatureText(e.target.value)}
|
|
458
|
+
onBlur={handleSignatureBlur}
|
|
459
|
+
rows={5}
|
|
460
|
+
className="w-full px-3 py-2 border border-line rounded-md bg-surface-sunken text-sm text-fg placeholder:text-fg-subtle focus-within:border-line-strong focus-within:ring-2 focus-within:ring-ring/30 transition-colors resize-y outline-none"
|
|
461
|
+
placeholder="Enter your email signature..."
|
|
462
|
+
/>
|
|
463
|
+
<p className="text-xs text-fg-muted mt-1">
|
|
464
|
+
{isSignatureSaving
|
|
465
|
+
? "Saving signature..."
|
|
466
|
+
: "This signature will be appended to new emails."}
|
|
467
|
+
</p>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</section>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
</SlidePanel>
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<SlidePanel
|
|
479
|
+
isOpen={isOpen}
|
|
480
|
+
onClose={onClose}
|
|
481
|
+
title={isEditing ? "Edit Account" : "Add Account"}
|
|
482
|
+
footer={
|
|
483
|
+
<>
|
|
484
|
+
<Button type="button" variant="secondary" onClick={onClose}>
|
|
485
|
+
Cancel
|
|
486
|
+
</Button>
|
|
487
|
+
<Button
|
|
488
|
+
type="submit"
|
|
489
|
+
form="account-form"
|
|
490
|
+
variant="primary"
|
|
491
|
+
disabled={isSaving}
|
|
492
|
+
>
|
|
493
|
+
{isSaving ? "Saving..." : "Save Account"}
|
|
494
|
+
</Button>
|
|
495
|
+
</>
|
|
496
|
+
}
|
|
497
|
+
>
|
|
498
|
+
<form id="account-form" onSubmit={handleSubmit} className="space-y-6">
|
|
499
|
+
{/* Account Information Section */}
|
|
500
|
+
<section>
|
|
501
|
+
<h3 className="text-2xs font-semibold text-fg-subtle uppercase tracking-wider mb-3">
|
|
502
|
+
Account Information
|
|
503
|
+
</h3>
|
|
504
|
+
<div className="space-y-3">
|
|
505
|
+
<div>
|
|
506
|
+
<label
|
|
507
|
+
htmlFor="account-provider"
|
|
508
|
+
className="text-sm font-medium mb-1.5 block"
|
|
509
|
+
>
|
|
510
|
+
Provider
|
|
511
|
+
</label>
|
|
512
|
+
<Select
|
|
513
|
+
id="account-provider"
|
|
514
|
+
value={presetId}
|
|
515
|
+
onChange={(e) => handlePresetChange(e.target.value)}
|
|
516
|
+
>
|
|
517
|
+
<option value="">Custom / other</option>
|
|
518
|
+
{PROVIDER_PRESETS.map((preset) => (
|
|
519
|
+
<option key={preset.id} value={preset.id}>
|
|
520
|
+
{preset.label}
|
|
521
|
+
</option>
|
|
522
|
+
))}
|
|
523
|
+
</Select>
|
|
524
|
+
<p className="text-xs text-fg-muted mt-1">
|
|
525
|
+
Pick your email provider to fill in server settings
|
|
526
|
+
automatically, or choose Custom to enter them by hand.
|
|
527
|
+
</p>
|
|
528
|
+
</div>
|
|
529
|
+
<div>
|
|
530
|
+
<label
|
|
531
|
+
htmlFor="account-email"
|
|
532
|
+
className="text-sm font-medium mb-1.5 block"
|
|
533
|
+
>
|
|
534
|
+
Email Address
|
|
535
|
+
</label>
|
|
536
|
+
<Input
|
|
537
|
+
id="account-email"
|
|
538
|
+
{...form.register("email")}
|
|
539
|
+
placeholder="alice@example.com"
|
|
540
|
+
/>
|
|
541
|
+
{form.formState.errors.email && (
|
|
542
|
+
<p className="text-sm text-danger mt-1">
|
|
543
|
+
{form.formState.errors.email.message}
|
|
544
|
+
</p>
|
|
545
|
+
)}
|
|
546
|
+
</div>
|
|
547
|
+
<div>
|
|
548
|
+
<label
|
|
549
|
+
htmlFor="account-displayName"
|
|
550
|
+
className="text-sm font-medium mb-1.5 block"
|
|
551
|
+
>
|
|
552
|
+
Display name (optional)
|
|
553
|
+
</label>
|
|
554
|
+
<Input
|
|
555
|
+
id="account-displayName"
|
|
556
|
+
{...form.register("displayName")}
|
|
557
|
+
placeholder="Alice"
|
|
558
|
+
/>
|
|
559
|
+
<p className="text-xs text-fg-muted mt-1">
|
|
560
|
+
What to call this account in Remit. Leave blank to use a name
|
|
561
|
+
derived from the address.
|
|
562
|
+
</p>
|
|
563
|
+
</div>
|
|
564
|
+
<div>
|
|
565
|
+
<label
|
|
566
|
+
htmlFor="account-username"
|
|
567
|
+
className="text-sm font-medium mb-1.5 block"
|
|
568
|
+
>
|
|
569
|
+
Username
|
|
570
|
+
</label>
|
|
571
|
+
<Input
|
|
572
|
+
id="account-username"
|
|
573
|
+
{...form.register("username")}
|
|
574
|
+
placeholder="Same as email if empty"
|
|
575
|
+
/>
|
|
576
|
+
</div>
|
|
577
|
+
<div>
|
|
578
|
+
<label
|
|
579
|
+
htmlFor="account-password"
|
|
580
|
+
className="text-sm font-medium mb-1.5 block"
|
|
581
|
+
>
|
|
582
|
+
Password
|
|
583
|
+
</label>
|
|
584
|
+
<Input
|
|
585
|
+
id="account-password"
|
|
586
|
+
{...form.register("password", {
|
|
587
|
+
onChange: () => setPasswordModified(true),
|
|
588
|
+
})}
|
|
589
|
+
type="password"
|
|
590
|
+
onFocus={() => {
|
|
591
|
+
// Clear placeholder when user focuses the field
|
|
592
|
+
if (isEditing && !passwordModified) {
|
|
593
|
+
form.setValue("password", "");
|
|
594
|
+
setPasswordModified(true);
|
|
595
|
+
}
|
|
596
|
+
}}
|
|
597
|
+
placeholder={isEditing ? "Leave empty to keep current" : ""}
|
|
598
|
+
/>
|
|
599
|
+
{form.formState.errors.password && (
|
|
600
|
+
<p className="text-sm text-danger mt-1">
|
|
601
|
+
{form.formState.errors.password.message}
|
|
602
|
+
</p>
|
|
603
|
+
)}
|
|
604
|
+
{selectedPreset && (
|
|
605
|
+
<p className="text-xs text-fg-muted mt-1">
|
|
606
|
+
{selectedPreset.passwordHelp.text}{" "}
|
|
607
|
+
<a
|
|
608
|
+
href={selectedPreset.passwordHelp.url}
|
|
609
|
+
target="_blank"
|
|
610
|
+
rel="noopener noreferrer"
|
|
611
|
+
className="text-accent hover:underline"
|
|
612
|
+
>
|
|
613
|
+
Get an app password
|
|
614
|
+
</a>
|
|
615
|
+
</p>
|
|
616
|
+
)}
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</section>
|
|
620
|
+
|
|
621
|
+
{/* IMAP Settings */}
|
|
622
|
+
<section>
|
|
623
|
+
<div className="flex items-center justify-between mb-3">
|
|
624
|
+
<h3 className="text-2xs font-semibold text-fg-subtle uppercase tracking-wider">
|
|
625
|
+
Incoming Mail (IMAP)
|
|
626
|
+
</h3>
|
|
627
|
+
{selectedPreset && (
|
|
628
|
+
<button
|
|
629
|
+
type="button"
|
|
630
|
+
onClick={() => setAdvancedOpen((v) => !v)}
|
|
631
|
+
className="text-xs text-fg-muted hover:text-fg transition-colors"
|
|
632
|
+
>
|
|
633
|
+
{advancedOpen ? "Use preset settings" : "Advanced"}
|
|
634
|
+
</button>
|
|
635
|
+
)}
|
|
636
|
+
</div>
|
|
637
|
+
{serversLocked && (
|
|
638
|
+
<p className="text-xs text-fg-muted mb-3">
|
|
639
|
+
Server settings are pre-filled for {selectedPreset?.label} and
|
|
640
|
+
locked. Choose Advanced to edit them by hand.
|
|
641
|
+
</p>
|
|
642
|
+
)}
|
|
643
|
+
<div className="space-y-3">
|
|
644
|
+
<div>
|
|
645
|
+
<label
|
|
646
|
+
htmlFor="imap-host"
|
|
647
|
+
className="text-sm font-medium mb-1.5 block"
|
|
648
|
+
>
|
|
649
|
+
Server
|
|
650
|
+
</label>
|
|
651
|
+
<Input
|
|
652
|
+
id="imap-host"
|
|
653
|
+
{...form.register("imapHost")}
|
|
654
|
+
readOnly={serversLocked}
|
|
655
|
+
placeholder="imap.example.com"
|
|
656
|
+
/>
|
|
657
|
+
{form.formState.errors.imapHost && (
|
|
658
|
+
<p className="text-sm text-danger mt-1">
|
|
659
|
+
{form.formState.errors.imapHost.message}
|
|
660
|
+
</p>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
<div className="grid grid-cols-2 gap-3">
|
|
664
|
+
<div>
|
|
665
|
+
<label
|
|
666
|
+
htmlFor="imap-port"
|
|
667
|
+
className="text-sm font-medium mb-1.5 block"
|
|
668
|
+
>
|
|
669
|
+
Port
|
|
670
|
+
</label>
|
|
671
|
+
<Input
|
|
672
|
+
id="imap-port"
|
|
673
|
+
{...form.register("imapPort")}
|
|
674
|
+
readOnly={serversLocked}
|
|
675
|
+
type="number"
|
|
676
|
+
/>
|
|
677
|
+
</div>
|
|
678
|
+
<div>
|
|
679
|
+
<span className="text-sm font-medium mb-1.5 block">
|
|
680
|
+
Security
|
|
681
|
+
</span>
|
|
682
|
+
<div className="space-y-1.5 mt-2">
|
|
683
|
+
<label className="flex items-center gap-2 text-sm">
|
|
684
|
+
<input
|
|
685
|
+
type="radio"
|
|
686
|
+
disabled={serversLocked}
|
|
687
|
+
checked={form.watch("imapTls")}
|
|
688
|
+
onChange={() => handleSecurityChange("tls")}
|
|
689
|
+
/>
|
|
690
|
+
TLS (Port 993)
|
|
691
|
+
</label>
|
|
692
|
+
<label className="flex items-center gap-2 text-sm">
|
|
693
|
+
<input
|
|
694
|
+
type="radio"
|
|
695
|
+
disabled={serversLocked}
|
|
696
|
+
checked={
|
|
697
|
+
!form.watch("imapTls") && form.watch("imapStartTls")
|
|
698
|
+
}
|
|
699
|
+
onChange={() => handleSecurityChange("starttls")}
|
|
700
|
+
/>
|
|
701
|
+
STARTTLS (Port 143)
|
|
702
|
+
</label>
|
|
703
|
+
<label className="flex items-center gap-2 text-sm">
|
|
704
|
+
<input
|
|
705
|
+
type="radio"
|
|
706
|
+
disabled={serversLocked}
|
|
707
|
+
checked={
|
|
708
|
+
!form.watch("imapTls") && !form.watch("imapStartTls")
|
|
709
|
+
}
|
|
710
|
+
onChange={() => handleSecurityChange("none")}
|
|
711
|
+
/>
|
|
712
|
+
None (Unencrypted)
|
|
713
|
+
</label>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
<Button
|
|
718
|
+
type="button"
|
|
719
|
+
variant="secondary"
|
|
720
|
+
onClick={handleTestImap}
|
|
721
|
+
disabled={testMutation.isPending}
|
|
722
|
+
className="w-full"
|
|
723
|
+
icon={
|
|
724
|
+
testMutation.isPending ? (
|
|
725
|
+
<Loader2 className="size-4 animate-spin" />
|
|
726
|
+
) : undefined
|
|
727
|
+
}
|
|
728
|
+
>
|
|
729
|
+
{testMutation.isPending ? "Testing..." : "Test IMAP Connection"}
|
|
730
|
+
</Button>
|
|
731
|
+
{testMutation.data && (
|
|
732
|
+
<div
|
|
733
|
+
className={cn(
|
|
734
|
+
"p-2 rounded-md text-sm",
|
|
735
|
+
testMutation.data.imapSuccess
|
|
736
|
+
? "bg-positive/10 text-positive"
|
|
737
|
+
: "bg-danger-soft text-danger",
|
|
738
|
+
)}
|
|
739
|
+
>
|
|
740
|
+
{testMutation.data.imapSuccess ? (
|
|
741
|
+
<Check className="inline size-4 mr-1" />
|
|
742
|
+
) : (
|
|
743
|
+
<X className="inline size-4 mr-1" />
|
|
744
|
+
)}
|
|
745
|
+
{testMutation.data.imapSuccess ? "IMAP Connected" : imapError}
|
|
746
|
+
</div>
|
|
747
|
+
)}
|
|
748
|
+
</div>
|
|
749
|
+
</section>
|
|
750
|
+
|
|
751
|
+
{/* SMTP Settings */}
|
|
752
|
+
<section ref={smtpSectionRef} data-testid="smtp-section">
|
|
753
|
+
<div className="flex items-center justify-between mb-3">
|
|
754
|
+
<h3 className="text-2xs font-semibold text-fg-subtle uppercase tracking-wider">
|
|
755
|
+
Outgoing Mail (SMTP)
|
|
756
|
+
</h3>
|
|
757
|
+
{!serversLocked && (
|
|
758
|
+
<button
|
|
759
|
+
type="button"
|
|
760
|
+
onClick={handlePrefillFromImap}
|
|
761
|
+
disabled={!form.watch("imapHost")}
|
|
762
|
+
className="text-xs text-fg-muted hover:text-fg transition-colors disabled:opacity-50"
|
|
763
|
+
>
|
|
764
|
+
Pre-fill from IMAP
|
|
765
|
+
</button>
|
|
766
|
+
)}
|
|
767
|
+
</div>
|
|
768
|
+
<div className="space-y-3">
|
|
769
|
+
<div>
|
|
770
|
+
<label
|
|
771
|
+
htmlFor="smtp-host"
|
|
772
|
+
className="text-sm font-medium mb-1.5 block"
|
|
773
|
+
>
|
|
774
|
+
Server
|
|
775
|
+
</label>
|
|
776
|
+
<Input
|
|
777
|
+
id="smtp-host"
|
|
778
|
+
{...form.register("smtpHost")}
|
|
779
|
+
readOnly={serversLocked}
|
|
780
|
+
placeholder="smtp.example.com"
|
|
781
|
+
/>
|
|
782
|
+
</div>
|
|
783
|
+
<div className="grid grid-cols-2 gap-3">
|
|
784
|
+
<div>
|
|
785
|
+
<label
|
|
786
|
+
htmlFor="smtp-port"
|
|
787
|
+
className="text-sm font-medium mb-1.5 block"
|
|
788
|
+
>
|
|
789
|
+
Port
|
|
790
|
+
</label>
|
|
791
|
+
<Input
|
|
792
|
+
id="smtp-port"
|
|
793
|
+
{...form.register("smtpPort")}
|
|
794
|
+
readOnly={serversLocked}
|
|
795
|
+
type="number"
|
|
796
|
+
/>
|
|
797
|
+
</div>
|
|
798
|
+
<div>
|
|
799
|
+
<span className="text-sm font-medium mb-1.5 block">
|
|
800
|
+
Security
|
|
801
|
+
</span>
|
|
802
|
+
<div className="space-y-1.5 mt-2">
|
|
803
|
+
<label className="flex items-center gap-2 text-sm">
|
|
804
|
+
<input
|
|
805
|
+
type="radio"
|
|
806
|
+
disabled={serversLocked}
|
|
807
|
+
checked={form.watch("smtpTls") === true}
|
|
808
|
+
onChange={() => handleSmtpSecurityChange("tls")}
|
|
809
|
+
/>
|
|
810
|
+
TLS (Port 465)
|
|
811
|
+
</label>
|
|
812
|
+
<label className="flex items-center gap-2 text-sm">
|
|
813
|
+
<input
|
|
814
|
+
type="radio"
|
|
815
|
+
disabled={serversLocked}
|
|
816
|
+
checked={
|
|
817
|
+
!form.watch("smtpTls") &&
|
|
818
|
+
form.watch("smtpStartTls") === true
|
|
819
|
+
}
|
|
820
|
+
onChange={() => handleSmtpSecurityChange("starttls")}
|
|
821
|
+
/>
|
|
822
|
+
STARTTLS (Port 587)
|
|
823
|
+
</label>
|
|
824
|
+
<label className="flex items-center gap-2 text-sm">
|
|
825
|
+
<input
|
|
826
|
+
type="radio"
|
|
827
|
+
disabled={serversLocked}
|
|
828
|
+
checked={
|
|
829
|
+
!form.watch("smtpTls") && !form.watch("smtpStartTls")
|
|
830
|
+
}
|
|
831
|
+
onChange={() => handleSmtpSecurityChange("none")}
|
|
832
|
+
/>
|
|
833
|
+
None (Unencrypted)
|
|
834
|
+
</label>
|
|
835
|
+
</div>
|
|
836
|
+
</div>
|
|
837
|
+
</div>
|
|
838
|
+
<label className="flex items-center gap-2 text-sm">
|
|
839
|
+
<input
|
|
840
|
+
type="checkbox"
|
|
841
|
+
{...form.register("useDifferentSmtpCreds")}
|
|
842
|
+
/>
|
|
843
|
+
Use different credentials for SMTP
|
|
844
|
+
</label>
|
|
845
|
+
{form.watch("useDifferentSmtpCreds") && (
|
|
846
|
+
<>
|
|
847
|
+
<div>
|
|
848
|
+
<label
|
|
849
|
+
htmlFor="smtp-username"
|
|
850
|
+
className="text-sm font-medium mb-1.5 block"
|
|
851
|
+
>
|
|
852
|
+
SMTP Username
|
|
853
|
+
</label>
|
|
854
|
+
<Input
|
|
855
|
+
id="smtp-username"
|
|
856
|
+
{...form.register("smtpUsername")}
|
|
857
|
+
/>
|
|
858
|
+
</div>
|
|
859
|
+
<div>
|
|
860
|
+
<label
|
|
861
|
+
htmlFor="smtp-password"
|
|
862
|
+
className="text-sm font-medium mb-1.5 block"
|
|
863
|
+
>
|
|
864
|
+
SMTP Password
|
|
865
|
+
</label>
|
|
866
|
+
<Input
|
|
867
|
+
id="smtp-password"
|
|
868
|
+
{...form.register("smtpPassword")}
|
|
869
|
+
type="password"
|
|
870
|
+
/>
|
|
871
|
+
</div>
|
|
872
|
+
</>
|
|
873
|
+
)}
|
|
874
|
+
<Button
|
|
875
|
+
type="button"
|
|
876
|
+
variant="secondary"
|
|
877
|
+
onClick={handleTestSmtp}
|
|
878
|
+
disabled={testMutation.isPending || !form.watch("smtpHost")}
|
|
879
|
+
className="w-full"
|
|
880
|
+
icon={
|
|
881
|
+
testMutation.isPending ? (
|
|
882
|
+
<Loader2 className="size-4 animate-spin" />
|
|
883
|
+
) : undefined
|
|
884
|
+
}
|
|
885
|
+
>
|
|
886
|
+
{testMutation.isPending ? "Testing..." : "Test SMTP Connection"}
|
|
887
|
+
</Button>
|
|
888
|
+
{testMutation.data?.smtpSuccess !== undefined && (
|
|
889
|
+
<div
|
|
890
|
+
className={cn(
|
|
891
|
+
"p-2 rounded-md text-sm",
|
|
892
|
+
testMutation.data.smtpSuccess
|
|
893
|
+
? "bg-positive/10 text-positive"
|
|
894
|
+
: "bg-danger-soft text-danger",
|
|
895
|
+
)}
|
|
896
|
+
>
|
|
897
|
+
{testMutation.data.smtpSuccess ? (
|
|
898
|
+
<Check className="inline size-4 mr-1" />
|
|
899
|
+
) : (
|
|
900
|
+
<X className="inline size-4 mr-1" />
|
|
901
|
+
)}
|
|
902
|
+
{testMutation.data.smtpSuccess ? "SMTP Connected" : smtpError}
|
|
903
|
+
</div>
|
|
904
|
+
)}
|
|
905
|
+
</div>
|
|
906
|
+
</section>
|
|
907
|
+
|
|
908
|
+
{isEditing && (
|
|
909
|
+
<section>
|
|
910
|
+
<h3 className="text-2xs font-semibold text-fg-subtle uppercase tracking-wider mb-3">
|
|
911
|
+
Signature
|
|
912
|
+
</h3>
|
|
913
|
+
<div className="space-y-3">
|
|
914
|
+
<div>
|
|
915
|
+
<label
|
|
916
|
+
htmlFor="email-sig-edit"
|
|
917
|
+
className="text-sm font-medium mb-1.5 block"
|
|
918
|
+
>
|
|
919
|
+
Email Signature
|
|
920
|
+
</label>
|
|
921
|
+
<textarea
|
|
922
|
+
id="email-sig-edit"
|
|
923
|
+
value={signatureText}
|
|
924
|
+
onChange={(e) => setSignatureText(e.target.value)}
|
|
925
|
+
onBlur={handleSignatureBlur}
|
|
926
|
+
rows={5}
|
|
927
|
+
className="w-full px-3 py-2 border border-line rounded-md bg-surface-sunken text-sm text-fg placeholder:text-fg-subtle focus-within:border-line-strong focus-within:ring-2 focus-within:ring-ring/30 transition-colors resize-y outline-none"
|
|
928
|
+
placeholder="Enter your email signature..."
|
|
929
|
+
/>
|
|
930
|
+
<p className="text-xs text-fg-muted mt-1">
|
|
931
|
+
{isSignatureSaving
|
|
932
|
+
? "Saving signature..."
|
|
933
|
+
: "This signature will be appended to new emails."}
|
|
934
|
+
</p>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
</section>
|
|
938
|
+
)}
|
|
939
|
+
</form>
|
|
940
|
+
</SlidePanel>
|
|
941
|
+
);
|
|
942
|
+
};
|