@open-mercato/channel-imap 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/channel_imap/widgets/injection/connect/widget.client.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { PasswordInput } from '@open-mercato/ui/primitives/password-input'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@open-mercato/ui/primitives/select'\n\ntype WidgetContext = Record<string, unknown> & {\n reload?: () => void\n}\n\ntype ConnectResponse = {\n channelId?: string\n error?: string\n fieldErrors?: Record<string, string>\n}\n\ntype TlsMode = 'tls' | 'starttls' | 'none'\n\ntype FormState = {\n displayName: string\n fromAddress: string\n imapHost: string\n imapPort: string\n imapTls: TlsMode\n imapUser: string\n imapPassword: string\n smtpHost: string\n smtpPort: string\n smtpTls: TlsMode\n smtpUser: string\n smtpPassword: string\n}\n\nconst INITIAL_FORM: FormState = {\n displayName: '',\n fromAddress: '',\n imapHost: '',\n imapPort: '993',\n imapTls: 'tls',\n imapUser: '',\n imapPassword: '',\n smtpHost: '',\n smtpPort: '465',\n smtpTls: 'tls',\n smtpUser: '',\n smtpPassword: '',\n}\n\nexport default function ConnectImapWidget({\n context,\n}: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>) {\n const t = useT()\n const widgetContext = context as WidgetContext | undefined\n const [open, setOpen] = React.useState(false)\n const [pending, setPending] = React.useState(false)\n const [form, setForm] = React.useState<FormState>(INITIAL_FORM)\n const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})\n const { runMutation, retryLastMutation } = useGuardedMutation({\n contextId: 'channel-imap-connect',\n blockedMessage: t('communication_channels.profile.connect.blocked', 'Connection blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({ providerKey: 'imap', retryLastMutation }),\n [retryLastMutation],\n )\n\n const update = React.useCallback(\n <K extends keyof FormState>(key: K, value: FormState[K]) => {\n setForm((current) => ({ ...current, [key]: value }))\n setFieldErrors((current) => {\n if (!current[key]) return current\n const next = { ...current }\n delete next[key]\n return next\n })\n },\n [],\n )\n\n const submit = React.useCallback(async () => {\n if (pending) return\n setPending(true)\n setFieldErrors({})\n try {\n const response = await runMutation({\n context: mutationContext,\n mutationPayload: { providerKey: 'imap', displayName: form.displayName },\n operation: () =>\n apiCall<ConnectResponse>('/api/communication_channels/channels/connect/credentials', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n providerKey: 'imap',\n displayName: form.displayName.trim() || form.fromAddress.trim(),\n pollIntervalSeconds: 300,\n credentials: {\n imapHost: form.imapHost.trim(),\n imapPort: Number(form.imapPort),\n imapTls: form.imapTls,\n imapUser: form.imapUser.trim(),\n imapPassword: form.imapPassword,\n smtpHost: form.smtpHost.trim(),\n smtpPort: Number(form.smtpPort),\n smtpTls: form.smtpTls,\n smtpUser: (form.smtpUser.trim() || form.imapUser.trim()),\n smtpPassword: form.smtpPassword || form.imapPassword,\n fromAddress: form.fromAddress.trim(),\n },\n }),\n }),\n })\n const body = response.result as ConnectResponse | undefined\n if (!response.ok) {\n setFieldErrors(body?.fieldErrors ?? {})\n flash(\n body?.error ??\n t('communication_channels.profile.connect.credentialsFailed', 'Could not connect mailbox.'),\n 'error',\n )\n return\n }\n flash(t('communication_channels.profile.connect.connected', 'Channel connected.'), 'success')\n setOpen(false)\n setForm(INITIAL_FORM)\n widgetContext?.reload?.()\n } finally {\n setPending(false)\n }\n }, [form, mutationContext, pending, runMutation, t, widgetContext])\n\n const onDialogKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n void submit()\n }\n },\n [submit],\n )\n\n return (\n <>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(true)}>\n {t('communication_channels.profile.connect.imap', 'Connect IMAP')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent onKeyDown={onDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.profile.connect.imapTitle', 'Connect IMAP mailbox')}\n </DialogTitle>\n </DialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <Field\n label={t('communication_channels.profile.connect.fields.displayName', 'Display name')}\n error={fieldErrors.displayName}\n >\n <Input\n value={form.displayName}\n onChange={(event) => update('displayName', event.target.value)}\n aria-invalid={Boolean(fieldErrors.displayName)}\n />\n </Field>\n <Field\n label={t('communication_channels.profile.connect.fields.fromAddress', 'From address')}\n error={fieldErrors.fromAddress}\n >\n <Input\n type=\"email\"\n value={form.fromAddress}\n onChange={(event) => update('fromAddress', event.target.value)}\n aria-invalid={Boolean(fieldErrors.fromAddress)}\n />\n </Field>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('communication_channels.profile.connect.fields.imapHost', 'IMAP host')}\n error={fieldErrors.imapHost}\n >\n <Input\n value={form.imapHost}\n onChange={(event) => update('imapHost', event.target.value)}\n aria-invalid={Boolean(fieldErrors.imapHost)}\n />\n </Field>\n <Field\n label={t('communication_channels.profile.connect.fields.imapPort', 'IMAP port')}\n error={fieldErrors.imapPort}\n >\n <Input\n inputMode=\"numeric\"\n value={form.imapPort}\n onChange={(event) => update('imapPort', event.target.value)}\n aria-invalid={Boolean(fieldErrors.imapPort)}\n />\n </Field>\n </div>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('communication_channels.profile.connect.fields.imapTls', 'IMAP security')}\n error={fieldErrors.imapTls}\n >\n <TlsSelect value={form.imapTls} onChange={(value) => update('imapTls', value)} />\n </Field>\n <Field\n label={t('communication_channels.profile.connect.fields.imapUser', 'IMAP username')}\n error={fieldErrors.imapUser}\n >\n <Input\n value={form.imapUser}\n onChange={(event) => update('imapUser', event.target.value)}\n aria-invalid={Boolean(fieldErrors.imapUser)}\n />\n </Field>\n </div>\n\n <Field\n label={t('communication_channels.profile.connect.fields.imapPassword', 'IMAP password')}\n error={fieldErrors.imapPassword}\n >\n <PasswordInput\n value={form.imapPassword}\n onChange={(event) => update('imapPassword', event.target.value)}\n aria-invalid={Boolean(fieldErrors.imapPassword)}\n />\n </Field>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('communication_channels.profile.connect.fields.smtpHost', 'SMTP host')}\n error={fieldErrors.smtpHost}\n >\n <Input\n value={form.smtpHost}\n onChange={(event) => update('smtpHost', event.target.value)}\n aria-invalid={Boolean(fieldErrors.smtpHost)}\n />\n </Field>\n <Field\n label={t('communication_channels.profile.connect.fields.smtpPort', 'SMTP port')}\n error={fieldErrors.smtpPort}\n >\n <Input\n inputMode=\"numeric\"\n value={form.smtpPort}\n onChange={(event) => update('smtpPort', event.target.value)}\n aria-invalid={Boolean(fieldErrors.smtpPort)}\n />\n </Field>\n </div>\n\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label={t('communication_channels.profile.connect.fields.smtpTls', 'SMTP security')}\n error={fieldErrors.smtpTls}\n >\n <TlsSelect value={form.smtpTls} onChange={(value) => update('smtpTls', value)} />\n </Field>\n <Field\n label={t('communication_channels.profile.connect.fields.smtpUser', 'SMTP username')}\n error={fieldErrors.smtpUser}\n >\n <Input\n value={form.smtpUser}\n onChange={(event) => update('smtpUser', event.target.value)}\n aria-invalid={Boolean(fieldErrors.smtpUser)}\n />\n </Field>\n </div>\n\n <Field\n label={t('communication_channels.profile.connect.fields.smtpPassword', 'SMTP password')}\n error={fieldErrors.smtpPassword}\n >\n <PasswordInput\n value={form.smtpPassword}\n onChange={(event) => update('smtpPassword', event.target.value)}\n aria-invalid={Boolean(fieldErrors.smtpPassword)}\n />\n </Field>\n </div>\n\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(false)} disabled={pending}>\n {t('communication_channels.profile.connect.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" onClick={() => void submit()} disabled={pending}>\n {pending\n ? t('communication_channels.profile.connect.connecting', 'Connecting...')\n : t('communication_channels.profile.connect.save', 'Connect')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\nfunction Field(props: {\n label: string\n error?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"grid gap-1.5\">\n <Label asChild>\n <span>{props.label}</span>\n </Label>\n {props.children}\n {props.error ? <span className=\"text-xs text-destructive\">{props.error}</span> : null}\n </label>\n )\n}\n\nfunction TlsSelect(props: { value: TlsMode; onChange: (value: TlsMode) => void }) {\n const t = useT()\n return (\n <Select value={props.value} onValueChange={(value) => props.onChange(value as TlsMode)}>\n <SelectTrigger>\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"tls\">\n {t('communication_channels.profile.connect.tls.tls', 'TLS')}\n </SelectItem>\n <SelectItem value=\"starttls\">\n {t('communication_channels.profile.connect.tls.starttls', 'STARTTLS')}\n </SelectItem>\n <SelectItem value=\"none\">\n {t('communication_channels.profile.connect.tls.none', 'None')}\n </SelectItem>\n </SelectContent>\n </Select>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n} from '@open-mercato/ui/primitives/dialog'\nimport {\n IMAP_CONNECT_INITIAL_VALUES,\n buildImapConnectRequestBody,\n createImapConnectFormSchema,\n type ImapConnectFormValues,\n} from '../../../lib/connect-form'\n\ntype WidgetContext = Record<string, unknown> & {\n reload?: () => void\n}\n\nconst FORM_ID = 'channel-imap-connect-form'\n\nexport default function ConnectImapWidget({\n context,\n}: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>) {\n const t = useT()\n const widgetContext = context as WidgetContext | undefined\n const [open, setOpen] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation({\n contextId: 'channel-imap-connect',\n blockedMessage: t('communication_channels.profile.connect.blocked', 'Connection blocked by validation'),\n })\n const mutationContext = React.useMemo(\n () => ({ providerKey: 'imap', retryLastMutation }),\n [retryLastMutation],\n )\n\n const schema = React.useMemo(() => createImapConnectFormSchema(t), [t])\n\n const fields = React.useMemo<CrudField[]>(() => {\n const tlsOptions = [\n { value: 'tls', label: t('communication_channels.profile.connect.tls.tls', 'TLS') },\n { value: 'starttls', label: t('communication_channels.profile.connect.tls.starttls', 'STARTTLS') },\n { value: 'none', label: t('communication_channels.profile.connect.tls.none', 'None') },\n ]\n return [\n {\n id: 'displayName',\n type: 'text',\n label: t('communication_channels.profile.connect.fields.displayName', 'Display name'),\n },\n {\n id: 'fromAddress',\n type: 'text',\n label: t('communication_channels.profile.connect.fields.fromAddress', 'From address'),\n required: true,\n },\n {\n id: 'imapHost',\n type: 'text',\n label: t('communication_channels.profile.connect.fields.imapHost', 'IMAP host'),\n required: true,\n layout: 'half',\n },\n {\n id: 'imapPort',\n type: 'number',\n label: t('communication_channels.profile.connect.fields.imapPort', 'IMAP port'),\n required: true,\n layout: 'half',\n },\n {\n id: 'imapTls',\n type: 'select',\n label: t('communication_channels.profile.connect.fields.imapTls', 'IMAP security'),\n options: tlsOptions,\n required: true,\n layout: 'half',\n },\n {\n id: 'imapUser',\n type: 'text',\n label: t('communication_channels.profile.connect.fields.imapUser', 'IMAP username'),\n required: true,\n layout: 'half',\n },\n {\n id: 'imapPassword',\n type: 'password',\n label: t('communication_channels.profile.connect.fields.imapPassword', 'IMAP password'),\n required: true,\n },\n {\n id: 'smtpHost',\n type: 'text',\n label: t('communication_channels.profile.connect.fields.smtpHost', 'SMTP host'),\n required: true,\n layout: 'half',\n },\n {\n id: 'smtpPort',\n type: 'number',\n label: t('communication_channels.profile.connect.fields.smtpPort', 'SMTP port'),\n required: true,\n layout: 'half',\n },\n {\n id: 'smtpTls',\n type: 'select',\n label: t('communication_channels.profile.connect.fields.smtpTls', 'SMTP security'),\n options: tlsOptions,\n required: true,\n layout: 'half',\n },\n {\n id: 'smtpUser',\n type: 'text',\n label: t('communication_channels.profile.connect.fields.smtpUser', 'SMTP username'),\n layout: 'half',\n },\n {\n id: 'smtpPassword',\n type: 'password',\n label: t('communication_channels.profile.connect.fields.smtpPassword', 'SMTP password'),\n },\n ]\n }, [t])\n\n const handleSubmit = React.useCallback(\n async (values: ImapConnectFormValues) => {\n const body = buildImapConnectRequestBody(values)\n await runMutation({\n context: mutationContext,\n mutationPayload: { providerKey: body.providerKey, displayName: body.displayName },\n operation: async () => {\n const call = await apiCall('/api/communication_channels/channels/connect/credentials', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!call.ok) {\n await raiseCrudError(\n call.response,\n t('communication_channels.profile.connect.credentialsFailed', 'Could not connect mailbox.'),\n )\n }\n return call\n },\n })\n flash(t('communication_channels.profile.connect.connected', 'Channel connected.'), 'success')\n setOpen(false)\n widgetContext?.reload?.()\n },\n [mutationContext, runMutation, t, widgetContext],\n )\n\n const onDialogKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.defaultPrevented || !(event.metaKey || event.ctrlKey) || event.key !== 'Enter') return\n event.preventDefault()\n const form = document.getElementById(FORM_ID)\n if (form instanceof HTMLFormElement) form.requestSubmit()\n }, [])\n\n return (\n <>\n <Button type=\"button\" variant=\"outline\" onClick={() => setOpen(true)}>\n {t('communication_channels.profile.connect.imap', 'Connect IMAP')}\n </Button>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent onKeyDown={onDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>\n {t('communication_channels.profile.connect.imapTitle', 'Connect IMAP mailbox')}\n </DialogTitle>\n </DialogHeader>\n {open ? (\n <CrudForm<ImapConnectFormValues>\n formId={FORM_ID}\n schema={schema}\n fields={fields}\n initialValues={IMAP_CONNECT_INITIAL_VALUES}\n submitLabel={t('communication_channels.profile.connect.save', 'Connect')}\n onSubmit={handleSubmit}\n embedded\n />\n ) : null}\n </DialogContent>\n </Dialog>\n </>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/channel-imap",
3
- "version": "0.9.0",
3
+ "version": "0.9.1-develop.7459.1.99e3accd30",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,9 +62,9 @@
62
62
  }
63
63
  },
64
64
  "dependencies": {
65
- "@open-mercato/core": "0.9.0",
66
- "@open-mercato/queue": "0.9.0",
67
- "@open-mercato/ui": "0.9.0",
65
+ "@open-mercato/core": "0.9.1-develop.7459.1.99e3accd30",
66
+ "@open-mercato/queue": "0.9.1-develop.7459.1.99e3accd30",
67
+ "@open-mercato/ui": "0.9.1-develop.7459.1.99e3accd30",
68
68
  "@types/mailparser": "^3.4.5",
69
69
  "@types/nodemailer": "^8.0.1",
70
70
  "imapflow": "^1.7.6",
@@ -73,12 +73,12 @@
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@mikro-orm/postgresql": "^7.0.14",
76
- "@open-mercato/shared": "0.9.0",
76
+ "@open-mercato/shared": "0.9.1-develop.7459.1.99e3accd30",
77
77
  "react": "^19.0.0",
78
78
  "react-dom": "^19.0.0"
79
79
  },
80
80
  "devDependencies": {
81
- "@open-mercato/shared": "0.9.0",
81
+ "@open-mercato/shared": "0.9.1-develop.7459.1.99e3accd30",
82
82
  "@types/jest": "^30.0.0",
83
83
  "@types/react": "^19.2.18",
84
84
  "@types/react-dom": "^19.2.5",
@@ -97,5 +97,6 @@
97
97
  "type": "git",
98
98
  "url": "https://github.com/open-mercato/open-mercato",
99
99
  "directory": "packages/channel-imap"
100
- }
100
+ },
101
+ "stableVersion": "0.9.0"
101
102
  }
@@ -77,4 +77,21 @@ test.describe('TC-CHANNEL-EMAIL-001: IMAP provider registration', () => {
77
77
  expect(body?.fieldErrors ?? body?.error).toBeTruthy()
78
78
  }
79
79
  })
80
+
81
+ test('POST connect/credentials with an empty display name returns per-field details (#5593)', async ({ request }) => {
82
+ const token = await getAuthToken(request)
83
+ const response = await apiRequest(
84
+ request,
85
+ 'POST',
86
+ '/api/communication_channels/channels/connect/credentials',
87
+ {
88
+ token,
89
+ data: { providerKey: 'imap', displayName: '', credentials: {} },
90
+ },
91
+ )
92
+ expect(response.status()).toBe(422)
93
+ const body = await readJsonSafe<{ error?: string; details?: Array<{ path?: unknown[] }> }>(response)
94
+ expect(body?.error).toBe('Invalid request body')
95
+ expect(body?.details?.map((issue) => issue.path?.[0])).toEqual(['displayName'])
96
+ })
80
97
  })
@@ -0,0 +1,84 @@
1
+ import {
2
+ IMAP_CONNECT_INITIAL_VALUES,
3
+ buildImapConnectRequestBody,
4
+ createImapConnectFormSchema,
5
+ } from '../connect-form'
6
+
7
+ const translate = (key: string, fallback: string) => `${key}|${fallback}`
8
+
9
+ function fieldErrorsOf(input: Record<string, unknown>): Record<string, string> {
10
+ const result = createImapConnectFormSchema(translate).safeParse(input)
11
+ if (result.success) return {}
12
+ const errors: Record<string, string> = {}
13
+ for (const issue of result.error.issues) {
14
+ const field = String(issue.path[0])
15
+ if (!errors[field]) errors[field] = issue.message
16
+ }
17
+ return errors
18
+ }
19
+
20
+ const VALID = {
21
+ displayName: '',
22
+ fromAddress: 'me@example.com',
23
+ imapHost: 'imap.example.com',
24
+ imapPort: 993,
25
+ imapTls: 'tls',
26
+ imapUser: 'me@example.com',
27
+ imapPassword: 'secret',
28
+ smtpHost: 'smtp.example.com',
29
+ smtpPort: 465,
30
+ smtpTls: 'tls',
31
+ smtpUser: '',
32
+ smtpPassword: '',
33
+ }
34
+
35
+ describe('IMAP connect form schema', () => {
36
+ it('reports a translated per-field error for every required field of an empty form (#5593)', () => {
37
+ expect(fieldErrorsOf(IMAP_CONNECT_INITIAL_VALUES)).toEqual({
38
+ fromAddress: 'communication_channels.profile.connect.errors.required|This field is required.',
39
+ imapHost: 'communication_channels.profile.connect.errors.required|This field is required.',
40
+ imapUser: 'communication_channels.profile.connect.errors.required|This field is required.',
41
+ imapPassword: 'communication_channels.profile.connect.errors.required|This field is required.',
42
+ smtpHost: 'communication_channels.profile.connect.errors.required|This field is required.',
43
+ })
44
+ })
45
+
46
+ it('flags an invalid from address and out-of-range ports on their own fields', () => {
47
+ expect(fieldErrorsOf({ ...VALID, fromAddress: 'not-an-email', imapPort: 0, smtpPort: 70000 })).toEqual({
48
+ fromAddress: 'communication_channels.profile.connect.errors.invalidEmail|Enter a valid email address.',
49
+ imapPort: 'communication_channels.profile.connect.errors.invalidPort|Enter a port between 1 and 65535.',
50
+ smtpPort: 'communication_channels.profile.connect.errors.invalidPort|Enter a port between 1 and 65535.',
51
+ })
52
+ })
53
+
54
+ it('accepts a form with only the required fields filled', () => {
55
+ expect(fieldErrorsOf(VALID)).toEqual({})
56
+ })
57
+ })
58
+
59
+ describe('buildImapConnectRequestBody', () => {
60
+ it('falls back to the from address and IMAP login when optional fields are blank', () => {
61
+ const values = createImapConnectFormSchema(translate).parse({ ...VALID, imapPort: '993' })
62
+ const body = buildImapConnectRequestBody(values)
63
+
64
+ expect(body.displayName).toBe('me@example.com')
65
+ expect(body.credentials).toMatchObject({
66
+ imapPort: 993,
67
+ smtpUser: 'me@example.com',
68
+ smtpPassword: 'secret',
69
+ })
70
+ })
71
+
72
+ it('keeps explicit display name and SMTP credentials', () => {
73
+ const values = createImapConnectFormSchema(translate).parse({
74
+ ...VALID,
75
+ displayName: 'Work mail',
76
+ smtpUser: 'relay',
77
+ smtpPassword: 'relay-secret',
78
+ })
79
+ const body = buildImapConnectRequestBody(values)
80
+
81
+ expect(body.displayName).toBe('Work mail')
82
+ expect(body.credentials).toMatchObject({ smtpUser: 'relay', smtpPassword: 'relay-secret' })
83
+ })
84
+ })
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod'
2
+
3
+ type Translate = (key: string, fallback: string) => string
4
+
5
+ const TLS_MODES = ['tls', 'starttls', 'none'] as const
6
+
7
+ export type ImapConnectTlsMode = (typeof TLS_MODES)[number]
8
+
9
+ export function createImapConnectFormSchema(t: Translate) {
10
+ const required = t('communication_channels.profile.connect.errors.required', 'This field is required.')
11
+ const invalidPort = t(
12
+ 'communication_channels.profile.connect.errors.invalidPort',
13
+ 'Enter a port between 1 and 65535.',
14
+ )
15
+ const requiredText = z.string().trim().min(1, required)
16
+ const optionalText = z.string().trim().optional()
17
+ const port = z.coerce.number(invalidPort).int(invalidPort).min(1, invalidPort).max(65535, invalidPort)
18
+
19
+ return z.object({
20
+ displayName: optionalText,
21
+ fromAddress: z
22
+ .string()
23
+ .trim()
24
+ .min(1, required)
25
+ .email(t('communication_channels.profile.connect.errors.invalidEmail', 'Enter a valid email address.')),
26
+ imapHost: requiredText,
27
+ imapPort: port,
28
+ imapTls: z.enum(TLS_MODES),
29
+ imapUser: requiredText,
30
+ imapPassword: z.string().min(1, required),
31
+ smtpHost: requiredText,
32
+ smtpPort: port,
33
+ smtpTls: z.enum(TLS_MODES),
34
+ smtpUser: optionalText,
35
+ smtpPassword: z.string().optional(),
36
+ })
37
+ }
38
+
39
+ export type ImapConnectFormValues = z.infer<ReturnType<typeof createImapConnectFormSchema>>
40
+
41
+ export const IMAP_CONNECT_INITIAL_VALUES: Partial<ImapConnectFormValues> = {
42
+ displayName: '',
43
+ fromAddress: '',
44
+ imapHost: '',
45
+ imapPort: 993,
46
+ imapTls: 'tls',
47
+ imapUser: '',
48
+ imapPassword: '',
49
+ smtpHost: '',
50
+ smtpPort: 465,
51
+ smtpTls: 'tls',
52
+ smtpUser: '',
53
+ smtpPassword: '',
54
+ }
55
+
56
+ export function buildImapConnectRequestBody(values: ImapConnectFormValues) {
57
+ return {
58
+ providerKey: 'imap',
59
+ displayName: values.displayName || values.fromAddress,
60
+ pollIntervalSeconds: 300,
61
+ credentials: {
62
+ imapHost: values.imapHost,
63
+ imapPort: values.imapPort,
64
+ imapTls: values.imapTls,
65
+ imapUser: values.imapUser,
66
+ imapPassword: values.imapPassword,
67
+ smtpHost: values.smtpHost,
68
+ smtpPort: values.smtpPort,
69
+ smtpTls: values.smtpTls,
70
+ smtpUser: values.smtpUser || values.imapUser,
71
+ smtpPassword: values.smtpPassword || values.imapPassword,
72
+ fromAddress: values.fromAddress,
73
+ },
74
+ }
75
+ }