@remit/web-client 0.0.229 → 0.0.231

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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/components/calendar/CalendarComposePane.stories.tsx +25 -0
  3. package/src/components/calendar/CalendarComposePane.tsx +34 -0
  4. package/src/components/calendar/CalendarEventPane.stories.tsx +81 -0
  5. package/src/components/calendar/CalendarEventPane.tsx +67 -0
  6. package/src/components/calendar/CalendarShell.tsx +83 -0
  7. package/src/components/calendar/CalendarViewPlaceholder.tsx +37 -0
  8. package/src/components/calendar/CalendarWorkspace.stories.tsx +197 -0
  9. package/src/components/calendar/CalendarWorkspace.tsx +162 -0
  10. package/src/components/calendar/OpenCalendarEvent.tsx +39 -0
  11. package/src/components/calendar/calendar-workspace.render.test.ts +193 -0
  12. package/src/components/config-import/ConfigImportWizard.flow.test.ts +338 -0
  13. package/src/components/config-import/ConfigImportWizard.tsx +720 -0
  14. package/src/components/config-import/config-import.fixtures.ts +229 -0
  15. package/src/components/config-import/config-import.stories.tsx +449 -0
  16. package/src/components/config-import/steps.tsx +772 -0
  17. package/src/components/mail/MailSidebarAdapter.tsx +17 -0
  18. package/src/components/onboarding/OnboardingWizard.tsx +64 -390
  19. package/src/components/onboarding/credential-steps.tsx +441 -0
  20. package/src/components/settings/ConfigExportCard.tsx +114 -0
  21. package/src/components/settings/ConfigTransferPanel.tsx +72 -0
  22. package/src/hooks/useCalendarData.ts +72 -0
  23. package/src/lib/calendar-density.ts +34 -0
  24. package/src/lib/calendar-fixtures.ts +207 -0
  25. package/src/lib/calendar-format.ts +29 -0
  26. package/src/lib/calendar-route.test.ts +210 -0
  27. package/src/lib/calendar-route.ts +183 -0
  28. package/src/lib/config-import.test.ts +306 -0
  29. package/src/lib/config-import.ts +492 -0
  30. package/src/lib/json.ts +11 -0
  31. package/src/lib/search-params.test.ts +77 -0
  32. package/src/lib/search-params.ts +59 -0
  33. package/src/routeTree.gen.ts +220 -0
  34. package/src/router.tsx +5 -0
  35. package/src/routes/calendar/$view.$date/$calendarObjectId/$recurrenceId.tsx +26 -0
  36. package/src/routes/calendar/$view.$date/$calendarObjectId/index.tsx +18 -0
  37. package/src/routes/calendar/$view.$date/$calendarObjectId.tsx +15 -0
  38. package/src/routes/calendar/$view.$date/index.tsx +25 -0
  39. package/src/routes/calendar/$view.$date/new.tsx +20 -0
  40. package/src/routes/calendar/$view.$date.tsx +100 -0
  41. package/src/routes/calendar/-calendar-tree.test.ts +267 -0
  42. package/src/routes/calendar/index.tsx +27 -0
  43. package/src/routes/calendar.tsx +98 -0
  44. package/src/routes/import.tsx +51 -0
  45. package/src/routes/onboarding.tsx +9 -2
  46. package/src/routes/settings/accounts.tsx +30 -8
  47. package/src/routes/settings/advanced.tsx +5 -1
  48. package/src/routing/calendar-fragment.test.ts +66 -0
  49. package/src/routing/calendar.ts +139 -0
  50. package/src/routing/fragment.ts +19 -0
  51. package/src/routing/index.ts +10 -0
  52. package/src/routing/nav-selection.ts +2 -0
  53. package/src/routing/sections.ts +3 -1
@@ -219,6 +219,22 @@ export function MailSidebarAdapter({
219
219
  </NavLink>
220
220
  );
221
221
  }
222
+ if (navId === "calendar") {
223
+ // No list segment to name: the calendar's index route sends the reader
224
+ // to the week they are in, so a bookmark of the nav entry keeps working
225
+ // tomorrow.
226
+ return (
227
+ <NavLink
228
+ to="/calendar"
229
+ onClick={() => onClick?.()}
230
+ className={className}
231
+ aria-label={ariaLabel}
232
+ title={title}
233
+ >
234
+ {children}
235
+ </NavLink>
236
+ );
237
+ }
222
238
  if (navId === "outbox") {
223
239
  return (
224
240
  <NavLink
@@ -332,6 +348,7 @@ export function MailSidebarAdapter({
332
348
  selectedNavId={selectedNavId}
333
349
  onSelectNav={handleSelectNav}
334
350
  linkComponent={linkComponent}
351
+ calendarNav="shown"
335
352
  variant={variant}
336
353
  savedSearches={savedSearches}
337
354
  saveableQuery={saveableQuery}
@@ -19,14 +19,12 @@
19
19
 
20
20
  import {
21
21
  accountOperationsCreateAccountMutation,
22
- accountOperationsTestConnectionMutation,
23
22
  configOperationsGetConfigOptions,
24
23
  configOperationsGetConfigQueryKey,
25
24
  microsoftOAuthOperationsMicrosoftOAuthStartMutation,
26
25
  syncOperationsGetSyncStatusOptions,
27
26
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
28
27
  import {
29
- AppPasswordHint,
30
28
  Banner,
31
29
  Button,
32
30
  CheckRow,
@@ -34,14 +32,13 @@ import {
34
32
  FieldLabel,
35
33
  Input,
36
34
  Kbd,
37
- PasswordInput,
38
35
  Select,
39
36
  ServerFields,
40
37
  securityToApi,
41
38
  WizardShell,
42
39
  } from "@remit/ui";
43
40
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
44
- import { AtSign, Inbox, Loader2, Mail, Server } from "lucide-react";
41
+ import { AtSign, FileJson, Inbox, Loader2, Mail, Server } from "lucide-react";
45
42
  import { useCallback, useEffect, useRef, useState } from "react";
46
43
  import {
47
44
  REDIRECT_STALL_MESSAGE,
@@ -53,7 +50,6 @@ import {
53
50
  discoverSettings,
54
51
  getAppPasswordUrl,
55
52
  getDiscoveryStatusMessage,
56
- type SecurityMode,
57
53
  } from "../../lib/autodiscovery.js";
58
54
  import {
59
55
  getPresetById,
@@ -61,6 +57,11 @@ import {
61
57
  presetIdForEmail,
62
58
  } from "../../lib/provider-presets.js";
63
59
  import { computeSmtpAutoFill } from "../settings/account-form-helpers.js";
60
+ import {
61
+ ConnectionTestStep,
62
+ CredentialsStep,
63
+ type ServerConfig,
64
+ } from "./credential-steps.js";
64
65
 
65
66
  /* ------------------------------------------------------------------ */
66
67
  /* Wizard step definitions */
@@ -101,12 +102,6 @@ const STEP_INDEX: Record<WizardStep, number> = {
101
102
  /* State shapes */
102
103
  /* ------------------------------------------------------------------ */
103
104
 
104
- interface ServerConfig {
105
- host: string;
106
- port: number;
107
- security: SecurityMode;
108
- }
109
-
110
105
  interface WizardState {
111
106
  email: string;
112
107
  displayName: string;
@@ -128,31 +123,22 @@ const DEFAULT_SMTP: ServerConfig = {
128
123
  security: "starttls",
129
124
  };
130
125
 
131
- type TestPhase =
132
- | "idle"
133
- | "running"
134
- | "success"
135
- | "auth-failure"
136
- | "network-failure";
137
-
138
- interface TestResult {
139
- imapState: "pending" | "running" | "ok" | "failed";
140
- smtpState: "pending" | "running" | "ok" | "failed";
141
- imapDetail?: string;
142
- smtpDetail?: string;
143
- rawError?: string;
144
- phase: TestPhase;
145
- }
146
-
147
- /* ------------------------------------------------------------------ */
148
- /* Helpers */
149
- /* ------------------------------------------------------------------ */
150
-
151
126
  /* ------------------------------------------------------------------ */
152
127
  /* Individual step components */
153
128
  /* ------------------------------------------------------------------ */
154
129
 
155
- function StepWelcome({ onStart }: { onStart: () => void }) {
130
+ /**
131
+ * First run. Importing a config file sits beside adding the first account, not
132
+ * under it: someone arriving from another Reader has already made every choice
133
+ * the wizard is about to ask for, and the file is the shorter road.
134
+ */
135
+ export function StepWelcome({
136
+ onStart,
137
+ onImportConfig,
138
+ }: {
139
+ onStart: () => void;
140
+ onImportConfig?: () => void;
141
+ }) {
156
142
  // Keyboard: Enter advances
157
143
  useEffect(() => {
158
144
  const handler = (e: KeyboardEvent) => {
@@ -168,12 +154,26 @@ function StepWelcome({ onStart }: { onStart: () => void }) {
168
154
  activeStep={0}
169
155
  hideSteps
170
156
  title="Welcome to Remit"
171
- subtitle="One inbox for all your mail, with context about every message."
157
+ subtitle={
158
+ onImportConfig
159
+ ? "Set up your first account, or bring the settings from another Reader."
160
+ : "One inbox for all your mail, with context about every message."
161
+ }
172
162
  footer={
173
163
  <>
174
- <span className="text-2xs text-fg-subtle">
175
- <Kbd>Enter</Kbd> to continue
176
- </span>
164
+ {onImportConfig ? (
165
+ <Button
166
+ variant="secondary"
167
+ icon={<FileJson className="size-4" />}
168
+ onClick={onImportConfig}
169
+ >
170
+ Import a config file
171
+ </Button>
172
+ ) : (
173
+ <span className="text-2xs text-fg-subtle">
174
+ <Kbd>Enter</Kbd> to continue
175
+ </span>
176
+ )}
177
177
  <Button variant="primary" onClick={onStart}>
178
178
  Add your first account
179
179
  </Button>
@@ -189,6 +189,13 @@ function StepWelcome({ onStart }: { onStart: () => void }) {
189
189
  writing to you — sender history, authenticity checks, and similar
190
190
  messages, right next to every email.
191
191
  </p>
192
+ {onImportConfig && (
193
+ <p className="mt-3 max-w-sm text-sm leading-relaxed text-fg-muted">
194
+ A config file carries your accounts, folder roles, rules, flagged
195
+ senders and appearance from another Reader. It never carries
196
+ passwords, so you sign in again here.
197
+ </p>
198
+ )}
192
199
  </div>
193
200
  </WizardShell>
194
201
  );
@@ -778,357 +785,6 @@ function StepServers({
778
785
  );
779
786
  }
780
787
 
781
- function StepCredentials({
782
- email,
783
- username,
784
- password,
785
- onContinue,
786
- onBack,
787
- onChange,
788
- }: {
789
- email: string;
790
- username: string;
791
- password: string;
792
- onContinue: () => void;
793
- onBack: () => void;
794
- onChange: (username: string, password: string) => void;
795
- }) {
796
- const [localUsername, setLocalUsername] = useState(username || email);
797
- const [localPassword, setLocalPassword] = useState(password);
798
- const [error, setError] = useState<string | null>(null);
799
-
800
- const domain = email.split("@")[1] ?? "";
801
- const appPasswordUrl = getAppPasswordUrl(email);
802
- const isValid = localUsername.trim() !== "" && localPassword.trim() !== "";
803
-
804
- useEffect(() => {
805
- onChange(localUsername, localPassword);
806
- }, [localUsername, localPassword, onChange]);
807
-
808
- const handleContinue = useCallback(() => {
809
- if (!isValid) {
810
- setError("Enter your username and password.");
811
- return;
812
- }
813
- setError(null);
814
- onContinue();
815
- }, [isValid, onContinue]);
816
-
817
- // Keyboard: Enter advances, Esc goes back
818
- useEffect(() => {
819
- const handler = (e: KeyboardEvent) => {
820
- if (e.key === "Enter") handleContinue();
821
- if (e.key === "Escape") onBack();
822
- };
823
- window.addEventListener("keydown", handler);
824
- return () => window.removeEventListener("keydown", handler);
825
- }, [handleContinue, onBack]);
826
-
827
- return (
828
- <WizardShell
829
- steps={STEP_LABELS}
830
- activeStep={STEP_INDEX.credentials}
831
- title={`Sign in to ${domain || "your mail server"}`}
832
- subtitle="Many providers require an app password instead of your normal one."
833
- footer={
834
- <>
835
- <Button variant="ghost" onClick={onBack}>
836
- Back
837
- </Button>
838
- <Button variant="primary" onClick={handleContinue}>
839
- Test connection
840
- </Button>
841
- </>
842
- }
843
- >
844
- <div className="space-y-3">
845
- {error && (
846
- <div className="rounded-md bg-danger-soft px-3 py-2 text-sm text-danger">
847
- {error}
848
- </div>
849
- )}
850
- <div>
851
- <FieldLabel htmlFor="credentials-username">Username</FieldLabel>
852
- <Input
853
- id="credentials-username"
854
- value={localUsername}
855
- onChange={(e) => {
856
- setLocalUsername(e.target.value);
857
- setError(null);
858
- }}
859
- autoComplete="username"
860
- />
861
- </div>
862
- <div>
863
- <FieldLabel htmlFor="credentials-password">
864
- Password or app password
865
- </FieldLabel>
866
- <PasswordInput
867
- id="credentials-password"
868
- value={localPassword}
869
- onChange={(e) => {
870
- setLocalPassword(e.target.value);
871
- setError(null);
872
- }}
873
- autoComplete="current-password"
874
- />
875
- </div>
876
- <AppPasswordHint url={appPasswordUrl} />
877
- </div>
878
- </WizardShell>
879
- );
880
- }
881
-
882
- function StepTest({
883
- email,
884
- imapConfig,
885
- smtpConfig,
886
- username,
887
- password,
888
- onSuccess,
889
- onBackToCredentials,
890
- onBackToServers,
891
- }: {
892
- email: string;
893
- imapConfig: ServerConfig;
894
- smtpConfig: ServerConfig;
895
- username: string;
896
- password: string;
897
- onSuccess: () => void;
898
- onBackToCredentials: () => void;
899
- onBackToServers: () => void;
900
- }) {
901
- const [testResult, setTestResult] = useState<TestResult>({
902
- imapState: "running",
903
- smtpState: "pending",
904
- phase: "running",
905
- });
906
- const [attempt, setAttempt] = useState(0);
907
- const smtpConfigured = smtpConfig.host.trim() !== "";
908
- const imapTls = securityToApi(imapConfig.security);
909
- const smtpTls = securityToApi(smtpConfig.security);
910
-
911
- const testMutation = useMutation({
912
- ...accountOperationsTestConnectionMutation(),
913
- });
914
-
915
- // biome-ignore lint/correctness/useExhaustiveDependencies: runs on mount and each Retry via `attempt`; the connection inputs are read intentionally and must not re-trigger the test on every render
916
- useEffect(() => {
917
- let cancelled = false;
918
- const timers: number[] = [];
919
-
920
- setTestResult({
921
- imapState: "running",
922
- smtpState: "pending",
923
- phase: "running",
924
- });
925
-
926
- testMutation.mutate(
927
- {
928
- body: {
929
- username,
930
- password,
931
- imapHost: imapConfig.host,
932
- imapPort: imapConfig.port,
933
- imapTls: imapTls.tls,
934
- imapStartTls: imapTls.startTls,
935
- smtpHost: smtpConfig.host,
936
- smtpPort: smtpConfig.port,
937
- smtpTls: smtpTls.tls,
938
- smtpStartTls: smtpTls.startTls,
939
- },
940
- },
941
- {
942
- onSuccess: (data) => {
943
- if (cancelled) return;
944
- // Show IMAP result first, then SMTP. SMTP is optional: when no
945
- // host was given the account is receive-only, so IMAP alone
946
- // decides success.
947
- const imapOk = data.imapSuccess;
948
- const smtpOk = smtpConfigured ? data.smtpSuccess : true;
949
-
950
- setTestResult({
951
- imapState: imapOk ? "ok" : "failed",
952
- smtpState: smtpConfigured ? "running" : "ok",
953
- imapDetail: imapOk
954
- ? `Connected — ${imapConfig.host}:${imapConfig.port}`
955
- : data.imapError,
956
- phase: "running",
957
- });
958
-
959
- timers.push(
960
- window.setTimeout(() => {
961
- if (cancelled) return;
962
- // An auth error can come from either protocol; route on the
963
- // one that actually failed. IMAP failures take precedence in
964
- // the raw-error display since IMAP is tested first.
965
- const isAuthError = (msg: string | undefined): boolean => {
966
- const m = msg?.toLowerCase() ?? "";
967
- return (
968
- m.includes("auth") ||
969
- m.includes("login") ||
970
- m.includes("535") ||
971
- m.includes("credential")
972
- );
973
- };
974
- const isAuthFailure =
975
- (!imapOk && isAuthError(data.imapError)) ||
976
- (imapOk &&
977
- smtpConfigured &&
978
- !data.smtpSuccess &&
979
- isAuthError(data.smtpError));
980
-
981
- setTestResult({
982
- imapState: imapOk ? "ok" : "failed",
983
- smtpState: !smtpConfigured
984
- ? "ok"
985
- : data.smtpSuccess
986
- ? "ok"
987
- : "failed",
988
- imapDetail: imapOk
989
- ? `Connected — ${imapConfig.host}:${imapConfig.port}`
990
- : data.imapError,
991
- smtpDetail: !smtpConfigured
992
- ? "Not set up — receive-only"
993
- : data.smtpSuccess
994
- ? `Connected — ${smtpConfig.host}:${smtpConfig.port}`
995
- : data.smtpError,
996
- rawError: !imapOk
997
- ? data.imapError
998
- : smtpConfigured && !data.smtpSuccess
999
- ? data.smtpError
1000
- : undefined,
1001
- phase:
1002
- imapOk && smtpOk
1003
- ? "success"
1004
- : isAuthFailure
1005
- ? "auth-failure"
1006
- : "network-failure",
1007
- });
1008
-
1009
- if (imapOk && smtpOk) {
1010
- timers.push(
1011
- window.setTimeout(() => {
1012
- if (!cancelled) onSuccess();
1013
- }, 800),
1014
- );
1015
- }
1016
- }, 400),
1017
- );
1018
- },
1019
- onError: (err) => {
1020
- if (cancelled) return;
1021
- const msg = err instanceof Error ? err.message : "Connection failed";
1022
- setTestResult({
1023
- imapState: "failed",
1024
- smtpState: "failed",
1025
- imapDetail: msg,
1026
- smtpDetail: msg,
1027
- rawError: msg,
1028
- phase: "network-failure",
1029
- });
1030
- },
1031
- },
1032
- );
1033
-
1034
- return () => {
1035
- cancelled = true;
1036
- for (const t of timers) window.clearTimeout(t);
1037
- };
1038
- }, [attempt]);
1039
-
1040
- const { phase } = testResult;
1041
-
1042
- // Keyboard: Esc goes back based on failure type
1043
- useEffect(() => {
1044
- const handler = (e: KeyboardEvent) => {
1045
- if (e.key === "Escape") {
1046
- if (phase === "auth-failure") onBackToCredentials();
1047
- else if (phase === "network-failure") onBackToServers();
1048
- }
1049
- };
1050
- window.addEventListener("keydown", handler);
1051
- return () => window.removeEventListener("keydown", handler);
1052
- }, [phase, onBackToCredentials, onBackToServers]);
1053
-
1054
- const footer =
1055
- phase === "auth-failure" ? (
1056
- <>
1057
- <Button variant="ghost" onClick={onBackToCredentials}>
1058
- Back to credentials
1059
- </Button>
1060
- <Button variant="primary" onClick={() => setAttempt((a) => a + 1)}>
1061
- Retry
1062
- </Button>
1063
- </>
1064
- ) : phase === "network-failure" ? (
1065
- <>
1066
- <Button variant="ghost" onClick={onBackToServers}>
1067
- Back to servers
1068
- </Button>
1069
- <Button variant="primary" onClick={() => setAttempt((a) => a + 1)}>
1070
- Retry
1071
- </Button>
1072
- </>
1073
- ) : phase === "success" ? (
1074
- <>
1075
- <span className="text-2xs text-fg-subtle">Connection verified</span>
1076
- <Button variant="primary" onClick={onSuccess}>
1077
- Start syncing
1078
- </Button>
1079
- </>
1080
- ) : (
1081
- <>
1082
- <span className="text-2xs text-fg-subtle">Testing…</span>
1083
- <span />
1084
- </>
1085
- );
1086
-
1087
- return (
1088
- <WizardShell
1089
- steps={STEP_LABELS}
1090
- activeStep={STEP_INDEX.test}
1091
- title="Testing the connection"
1092
- subtitle={`Checking ${email}`}
1093
- footer={footer}
1094
- >
1095
- <div className="divide-y divide-line">
1096
- <CheckRow
1097
- label={`IMAP — ${imapConfig.host}:${imapConfig.port}`}
1098
- detail={testResult.imapDetail}
1099
- state={testResult.imapState}
1100
- />
1101
- <CheckRow
1102
- label={
1103
- smtpConfigured
1104
- ? `SMTP — ${smtpConfig.host}:${smtpConfig.port}`
1105
- : "SMTP — sending"
1106
- }
1107
- detail={
1108
- smtpConfigured
1109
- ? testResult.smtpDetail
1110
- : "Not set up — you can add sending later in Settings"
1111
- }
1112
- state={testResult.smtpState}
1113
- />
1114
- {(phase === "auth-failure" || phase === "network-failure") &&
1115
- testResult.rawError && (
1116
- <div className="py-3">
1117
- {phase === "auth-failure" && (
1118
- <p className="mb-2 text-xs text-fg-muted">
1119
- Check your password — many providers require an app password.
1120
- </p>
1121
- )}
1122
- <code className="block rounded bg-surface-sunken px-2.5 py-2 text-2xs text-fg-muted">
1123
- {testResult.rawError}
1124
- </code>
1125
- </div>
1126
- )}
1127
- </div>
1128
- </WizardShell>
1129
- );
1130
- }
1131
-
1132
788
  /**
1133
789
  * How long the sync step holds "Go to inbox" shut while it waits for the first
1134
790
  * messages. Long enough for a normal IMAP handshake and first batch, short
@@ -1494,12 +1150,19 @@ export interface OnboardingWizardProps {
1494
1150
  onComplete: (accountId: string) => void;
1495
1151
  /** Called when the user cancels (Esc from connector picker) */
1496
1152
  onCancel?: () => void;
1153
+ /**
1154
+ * Offered on the welcome step only, beside adding the first account. Absent
1155
+ * where the wizard is embedded to add an account to an instance that already
1156
+ * has one — importing a configuration is a first-run move, not an add.
1157
+ */
1158
+ onImportConfig?: () => void;
1497
1159
  }
1498
1160
 
1499
1161
  export function OnboardingWizard({
1500
1162
  skipWelcome = false,
1501
1163
  onComplete,
1502
1164
  onCancel,
1165
+ onImportConfig,
1503
1166
  }: OnboardingWizardProps) {
1504
1167
  const [step, setStep] = useState<WizardStep>(
1505
1168
  skipWelcome ? "connector" : "welcome",
@@ -1599,7 +1262,12 @@ export function OnboardingWizard({
1599
1262
 
1600
1263
  switch (step) {
1601
1264
  case "welcome":
1602
- return <StepWelcome onStart={handleWelcomeStart} />;
1265
+ return (
1266
+ <StepWelcome
1267
+ onStart={handleWelcomeStart}
1268
+ onImportConfig={onImportConfig}
1269
+ />
1270
+ );
1603
1271
 
1604
1272
  case "connector":
1605
1273
  return (
@@ -1644,7 +1312,11 @@ export function OnboardingWizard({
1644
1312
 
1645
1313
  case "credentials":
1646
1314
  return (
1647
- <StepCredentials
1315
+ <CredentialsStep
1316
+ steps={STEP_LABELS}
1317
+ activeStep={STEP_INDEX.credentials}
1318
+ title={`Sign in to ${state.email.split("@")[1] || "your mail server"}`}
1319
+ subtitle="Many providers require an app password instead of your normal one."
1648
1320
  email={state.email}
1649
1321
  username={state.username}
1650
1322
  password={state.password}
@@ -1656,7 +1328,9 @@ export function OnboardingWizard({
1656
1328
 
1657
1329
  case "test":
1658
1330
  return (
1659
- <StepTest
1331
+ <ConnectionTestStep
1332
+ steps={STEP_LABELS}
1333
+ activeStep={STEP_INDEX.test}
1660
1334
  email={state.email}
1661
1335
  imapConfig={state.imapConfig}
1662
1336
  smtpConfig={state.smtpConfig}