@nocobase/portal-template-default 1.0.15 → 1.0.17

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 (153) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-iG-ADes-.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DXoSw1tM.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-C_ee4Eht.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-5Uxza9Ms.js → auto-login-provider-DO5Il6Dp.js} +2 -2
  8. package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
  9. package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
  10. package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
  11. package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
  12. package/dist/assets/calendar-C7xnRgOT.js +1 -0
  13. package/dist/assets/{cas-sign-in-button-CyqG1QME.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-D-ptYjX_.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-C08mkkiX.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-wj34-b1B.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-DWrhrHL_.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
  19. package/dist/assets/demo-BPlae2bp.js +1 -0
  20. package/dist/assets/demo-Bj_Y0RrW.js +1 -0
  21. package/dist/assets/demo-CKPAQOib.js +1 -0
  22. package/dist/assets/demo-CXRTPvIx.js +1 -0
  23. package/dist/assets/demo-DJuVDu1n.js +1 -0
  24. package/dist/assets/demo-DUmWU7ik.js +1 -0
  25. package/dist/assets/demo-NhIzFRE1.js +1 -0
  26. package/dist/assets/{dingtalk-sign-in-button-DSWsSruv.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-DxHNfq1C.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-D8kXn6ME.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-QY8v95qK.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-D-yqAK-r.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-P4-i8jPU.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-ByQZ-DbP.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-BFVEyPEp.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Btk35Nx-.js → index-CPzehPGe.js} +3 -3
  36. package/dist/assets/index-Cc6awvdg.js +1064 -0
  37. package/dist/assets/index-ChA0z9ke.js +38 -0
  38. package/dist/assets/{index-um5hdKdZ.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-CwNxd66J.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DQqRy3a_.js → labelHelper-Dk5jkiD-.js} +1 -1
  42. package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
  43. package/dist/assets/{oidc-sign-in-button-CQdRcR_p.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-CnQ4GYM7.js → page-context-DAiW_dId.js} +3 -3
  45. package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
  46. package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
  47. package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
  48. package/dist/assets/{sectorHelper-DN_Vv6OL.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-DTX9GV2I.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-jI1psK0d.js → static.browser-kzzpH0Xt.js} +8 -8
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-DFttLAfo.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-CRGjzXju.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
  55. package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
  56. package/dist/assets/{use-wecom-sign-in-C9j8wfCD.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/wecom-sign-in-button-Dn_2zDab.js +1 -0
  58. package/dist/index.html +2 -2
  59. package/dist/index.raw.html +2 -2
  60. package/package.json +9 -1
  61. package/src/App.tsx +5 -2
  62. package/src/app/extension.ts +2 -0
  63. package/src/app/extensions.tsx +18 -6
  64. package/src/app/route-access-guard.tsx +22 -0
  65. package/src/app/route-runtime.ts +173 -0
  66. package/src/app/routes.tsx +2 -2
  67. package/src/components/app-shell/header.tsx +56 -1
  68. package/src/components/app-shell/sidebar.tsx +14 -2
  69. package/src/components/auth/sign-up-form.tsx +287 -59
  70. package/src/components/auth/types.ts +23 -1
  71. package/src/extensions/nocobase-acl/README.md +28 -14
  72. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  73. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  74. package/src/extensions/nocobase-mail/README.md +74 -0
  75. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  76. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  77. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  78. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  79. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  80. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  81. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  82. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  83. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  84. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  85. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  86. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  87. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  89. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  90. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  91. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  92. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  93. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  94. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  95. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  96. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  97. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  98. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  99. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  100. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  101. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  102. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  103. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  104. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  108. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  109. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  110. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  111. package/src/extensions/nocobase-mail/index.ts +1 -0
  112. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  113. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  114. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  115. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  116. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  117. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  118. package/src/extensions/nocobase-users-example/index.ts +1 -0
  119. package/src/lib/nocobase/acl/menu.ts +17 -3
  120. package/src/lib/nocobase/acl/types.ts +4 -0
  121. package/src/locales/en-US.ts +2 -1
  122. package/src/locales/zh-CN.ts +2 -1
  123. package/src/providers/runtime-config.ts +18 -0
  124. package/src/routes.tsx +10 -0
  125. package/src/vite-env.d.ts +3 -0
  126. package/vite.config.ts +8 -0
  127. package/dist/assets/auth-method-demo-DaOCuvsq.js +0 -1
  128. package/dist/assets/auto-login-provider-DyaPGK2D.js +0 -1
  129. package/dist/assets/auto-redirect-provider-CfwRvYOI.js +0 -1
  130. package/dist/assets/auto-redirect-provider-Cr3jmYts.js +0 -1
  131. package/dist/assets/auto-redirect-provider-QkJQRBbG.js +0 -1
  132. package/dist/assets/calendar-DjDRfD5t.js +0 -1
  133. package/dist/assets/card-DuQBOS6A.js +0 -1
  134. package/dist/assets/createSeriesDataSimply-D0A6SAVQ.js +0 -1
  135. package/dist/assets/demo-BYWHeKUe.js +0 -1
  136. package/dist/assets/demo-CPWoNtx8.js +0 -1
  137. package/dist/assets/demo-CyvJk5zV.js +0 -1
  138. package/dist/assets/demo-D4OiKplc.js +0 -1
  139. package/dist/assets/demo-DBueJg0m.js +0 -1
  140. package/dist/assets/demo-DkIhyiRK.js +0 -1
  141. package/dist/assets/demo-tpTjO0sC.js +0 -1
  142. package/dist/assets/index-BgCbGDMc.js +0 -43
  143. package/dist/assets/index-MB-jfQld.js +0 -686
  144. package/dist/assets/index-NH3aqqQz.css +0 -1
  145. package/dist/assets/index-gFtCXH3o.js +0 -58
  146. package/dist/assets/ldap-sign-in-form--6Gt3yGq.js +0 -1
  147. package/dist/assets/prompt-card-B2RS4nt2.js +0 -1
  148. package/dist/assets/prompt-output-DZHRM2ft.js +0 -1
  149. package/dist/assets/saml-sign-in-button-BHL5a0JV.js +0 -1
  150. package/dist/assets/shortcut-C5x5LQWU.js +0 -47
  151. package/dist/assets/switch-CRL8PmRI.js +0 -1
  152. package/dist/assets/use-dingtalk-sign-in-8WYzJ2jf.js +0 -1
  153. package/dist/assets/wecom-sign-in-button-mIS1zGSU.js +0 -1
@@ -0,0 +1,74 @@
1
+ # NocoBase Mail
2
+
3
+ Reusable mail inbox, compose, detail, and label components backed by the NocoBase mail API.
4
+
5
+ After installation, import the components from `@/extensions/nocobase-mail`.
6
+
7
+ - `MailInbox` full inbox with toolbar, table, detail panel, and compose.
8
+ - `MailCompose` / `MailComposeForm` standalone compose dialog or embedded form,
9
+ with immediate and scheduled sending plus opt-in bulk mode, account-aware sender aliases,
10
+ server templates/signatures, recipient suggestions, reply/forward references, automatic draft
11
+ saving and recovery, and real attachment uploads.
12
+ - `MailComposeAttachments` uploads files through `mail:messageAttachmentUpload`
13
+ with retry, removal, and the backend's 25 MB limit.
14
+ - `MailFilters` folder, read-state, label, and todo-message filters.
15
+ - `MailUnreadProvider` / `MailUnreadIndicator` / `MailUnreadIcon` reusable unread-count
16
+ display with interval, window-focus, and visibility refresh.
17
+ - `MailDetail` threaded message detail with per-message Reply, conditional Reply all,
18
+ and Forward actions.
19
+ - `MailSettingsDrawer` personal mailbox settings with Mailbox, Labels, Templates,
20
+ and Signature management tabs.
21
+ - `MailMassTracking` bulk delivery tracking with recipient status, cancel, and resend.
22
+ - `useMailMessages` hook for paginated message listing with search and filters.
23
+ - `useMailCompose` hook to open compose from anywhere.
24
+ - `MailLabelBadge` / `MailLabelsEditor` label display, assignment, and full CRUD.
25
+ - `MailNoteEditor` per-message private notes.
26
+ - `MailAttachmentList` attachment preview and download.
27
+ - Message detail resolves authenticated `cid:` inline images and keeps those images out of the
28
+ regular attachment list.
29
+
30
+ The mail API client (`mailApi`) uses the Starter's built-in `API_URL`, `getNocoBaseHeaders`, and `NOCOBASE_TOKEN_KEY` from `@/providers/constants`. A compatible NocoBase backend with the mail plugin enabled is required.
31
+
32
+ ## Send modes
33
+
34
+ The standard compose form enables scheduled sending by default. Bulk sending is
35
+ kept on the dedicated `/dev/mail/bulk` showcase page, where the bulk form and
36
+ delivery jobs are visible together. Reusable compose forms can still opt into bulk mode:
37
+
38
+ - **Schedule send** calls `mail:messageSend` with an ISO `scheduleSendAt` value.
39
+ - **Bulk send** calls `mailMassMessages:send`, creates one separate message per
40
+ To recipient, and defaults to a 2-second interval between messages.
41
+
42
+ ```tsx
43
+ <MailComposeForm
44
+ allowScheduleSend
45
+ allowBulkSend
46
+ defaultBulkIntervalMs={2000}
47
+ />
48
+ ```
49
+
50
+ Bulk and scheduled sending are separate modes, matching the NocoBase email
51
+ manager backend. Bulk sending requires at least two recipients and does not
52
+ support a scheduled start time.
53
+
54
+ Scheduled messages can be canceled back into drafts and edited or rescheduled.
55
+ Opening a local draft enters compose mode with its uploaded attachments; local
56
+ drafts can be updated or permanently deleted. Provider-backed drafts remain
57
+ read-only and must be changed in the original mailbox so provider state and
58
+ attachments are preserved. Trash actions are provider-backed: messages
59
+ can be moved to trash, put back, or permanently deleted after confirmation.
60
+
61
+ Templates are stored in `mailTemplates`; the backend model stores template
62
+ content but no subject. Signatures are stored per mail account in
63
+ `mailAccounts.config.signatures`, so changing the selected sender also changes
64
+ the available/default signature. Use `recipientOptions` on the compose
65
+ components to add application-specific record recipients alongside NocoBase
66
+ users.
67
+
68
+ Gmail aliases can be viewed and refreshed from Mail settings. Synced aliases are
69
+ immediately available in the compose sender selector. Outlook does not expose
70
+ sender aliases through this integration.
71
+
72
+ Mailbox account creation, OAuth authorization, and provider settings are not
73
+ part of this component package; they remain owned by the separate mail settings
74
+ center.
@@ -0,0 +1,79 @@
1
+ export * from "./types";
2
+ export { mailApi } from "./mail-api";
3
+ export { MailInbox } from "./mail-inbox";
4
+ export type { MailInboxProps } from "./mail-inbox";
5
+ export { MailToolbar } from "./mail-toolbar";
6
+ export { MailTable } from "./mail-table";
7
+ export { MailDetail } from "./mail-detail";
8
+ export { MailCompose, MailComposeForm } from "./mail-compose";
9
+ export type {
10
+ ComposeInitialValues,
11
+ ComposeReference,
12
+ ComposeMode,
13
+ ComposeVariant,
14
+ MailComposeFormProps,
15
+ MailComposeProps,
16
+ } from "./mail-compose";
17
+ export { MailSendActions } from "./mail-send-actions";
18
+ export type { MailSendActionsProps } from "./mail-send-actions";
19
+ export { MailRecipientInput } from "./mail-recipient-input";
20
+ export type { MailRecipientInputProps } from "./mail-recipient-input";
21
+ export { MailComposeAttachments } from "./mail-compose-attachments";
22
+ export type { MailComposeAttachmentsProps } from "./mail-compose-attachments";
23
+ export { getMailSenderCandidates, resolveMailSender } from "./mail-senders";
24
+ export type { MailSenderCandidate } from "./mail-senders";
25
+ export { MailRichEditor } from "./mail-rich-editor";
26
+ export type { MailRichEditorProps } from "./mail-rich-editor";
27
+ export { useMailSignatures } from "./use-mail-signatures";
28
+ export type { MailSignatureValues } from "./use-mail-signatures";
29
+ export { MailSignatureManager } from "./mail-signature-manager";
30
+ export type { MailSignatureManagerProps } from "./mail-signature-manager";
31
+ export { applySignature } from "./mail-signatures";
32
+ export { useMailTemplates } from "./use-mail-templates";
33
+ export type { MailTemplateValues } from "./use-mail-templates";
34
+ export { MailTemplateManager } from "./mail-template-manager";
35
+ export type { MailTemplateManagerProps } from "./mail-template-manager";
36
+ export { useMailMessages } from "./use-mail-messages";
37
+ export type {
38
+ UseMailMessagesOptions,
39
+ UseMailMessagesResult,
40
+ } from "./use-mail-messages";
41
+ export {
42
+ useMailCompose,
43
+ buildComposeInitial,
44
+ canReplyAll,
45
+ } from "./use-mail-compose";
46
+ export type { UseMailComposeOptions } from "./use-mail-compose";
47
+ export { MailLabelBadge } from "./mail-label-badge";
48
+ export { MailLabelsEditor } from "./mail-labels-editor";
49
+ export { MailLabelManager } from "./mail-label-manager";
50
+ export type { MailLabelManagerProps } from "./mail-label-manager";
51
+ export { MailNoteEditor } from "./mail-note-editor";
52
+ export { MailAttachmentList } from "./mail-attachment-list";
53
+ export { createDebouncedDraftSaver } from "./mail-draft-autosave";
54
+ export type { DebouncedDraftSaver } from "./mail-draft-autosave";
55
+ export { serializeReplyQuote, splitReplyQuote } from "./mail-reply-quote";
56
+ export type { MailReplyContent } from "./mail-reply-quote";
57
+ export {
58
+ collectInlineContentIds,
59
+ filterInlineAttachments,
60
+ normalizeContentId,
61
+ replaceInlineImageSources,
62
+ } from "./mail-inline-images";
63
+ export { MailEmpty } from "./mail-empty";
64
+ export {
65
+ MailUnreadIcon,
66
+ MailUnreadIndicator,
67
+ MailUnreadProvider,
68
+ useMailUnread,
69
+ } from "./mail-unread";
70
+ export type {
71
+ MailUnreadIndicatorProps,
72
+ MailUnreadProviderProps,
73
+ } from "./mail-unread";
74
+ export { MailSettingsDrawer } from "./mail-settings-drawer";
75
+ export type { MailSettingsDrawerProps } from "./mail-settings-drawer";
76
+ export { MailMassTracking } from "./mail-mass-tracking";
77
+ export { useMailMassMessages } from "./use-mail-mass-messages";
78
+ export { MailFilters } from "./mail-filters";
79
+ export type { MailFilterValue, MailFiltersProps } from "./mail-filters";
@@ -0,0 +1,423 @@
1
+ import { nocobaseClient } from "@/lib/nocobase/client";
2
+ import type {
3
+ MailAccount,
4
+ MailIdentity,
5
+ MailLabel,
6
+ MailListParams,
7
+ MailListResponse,
8
+ MailMassSendSettings,
9
+ MailMassListResponse,
10
+ MailMassMessage,
11
+ MailMessage,
12
+ MailNote,
13
+ MailSendPayload,
14
+ MailTemplate,
15
+ MailUploadedAttachment,
16
+ MailUserRecord,
17
+ } from "./types";
18
+
19
+ function parseListResponse(payload: unknown): MailListResponse {
20
+ if (!payload || typeof payload !== "object") return { rows: [], count: 0 };
21
+ const obj = payload as {
22
+ data?: unknown;
23
+ meta?: { count?: number };
24
+ rows?: unknown;
25
+ count?: number;
26
+ };
27
+ if (Array.isArray(obj.data)) {
28
+ return { rows: obj.data, count: obj.meta?.count ?? obj.data.length };
29
+ }
30
+ if (Array.isArray(obj.rows)) {
31
+ return { rows: obj.rows, count: obj.count ?? obj.rows.length };
32
+ }
33
+ if (Array.isArray(payload)) {
34
+ return { rows: payload, count: payload.length };
35
+ }
36
+ return { rows: [], count: 0 };
37
+ }
38
+
39
+ export const mailApi = {
40
+ listMessages(params: MailListParams = {}): Promise<MailListResponse> {
41
+ const filter: Record<string, unknown> = {};
42
+ if (params.boxType) filter.boxType = { $in: [params.boxType] };
43
+ if (params.isRead !== undefined) filter.isRead = params.isRead;
44
+ if (params.labelId) filter["labels.id"] = { $in: [params.labelId] };
45
+ if (params.userId !== undefined) filter.userId = params.userId;
46
+ if (params.search) {
47
+ filter.$or = [
48
+ { subject: { $includes: params.search } },
49
+ { from: { $includes: params.search } },
50
+ { to: { $includes: params.search } },
51
+ ];
52
+ }
53
+ if (params.filter) Object.assign(filter, params.filter);
54
+
55
+ const action = params.scope === "personal" ? "listPerson" : "list";
56
+
57
+ return nocobaseClient
58
+ .action<unknown>("mailMessages", action, {
59
+ query: {
60
+ page: params.page ?? 1,
61
+ pageSize: params.pageSize ?? 20,
62
+ subjectMerge: true,
63
+ appends: "user",
64
+ ...(Object.keys(filter).length
65
+ ? { filter: JSON.stringify(filter) }
66
+ : {}),
67
+ sort: params.sort ?? "-relatedMessageLatestDate",
68
+ },
69
+ unwrap: "none",
70
+ })
71
+ .then(parseListResponse);
72
+ },
73
+
74
+ getMessage(id: number | string): Promise<MailMessage> {
75
+ return nocobaseClient.action<MailMessage>("mailMessages", "get", {
76
+ query: { filterByTk: id, appends: "children,labels,note" },
77
+ });
78
+ },
79
+
80
+ trashMessages(
81
+ ids: (number | string)[],
82
+ moveToTrash = true
83
+ ): Promise<unknown> {
84
+ return nocobaseClient.action("mailMessages", "trash", {
85
+ body: { filterByTk: ids, moveToTrash },
86
+ });
87
+ },
88
+
89
+ destroyMessages(ids: (number | string)[]): Promise<unknown> {
90
+ return nocobaseClient.action("mailMessages", "destroy", {
91
+ query: { filterByTk: ids },
92
+ });
93
+ },
94
+
95
+ cancelScheduled(id: number | string): Promise<unknown> {
96
+ return nocobaseClient.action("mailMessages", "cancelTimelySend", {
97
+ query: { id },
98
+ });
99
+ },
100
+
101
+ setRead(mailIds: string[], isRead: boolean): Promise<unknown> {
102
+ return nocobaseClient.action("mail", "messageSetReaded", {
103
+ body: { mailIds, isRead },
104
+ });
105
+ },
106
+
107
+ send(payload: MailSendPayload): Promise<unknown> {
108
+ return nocobaseClient.action("mail", "messageSend", { body: payload });
109
+ },
110
+
111
+ massSend(
112
+ payload: MailSendPayload,
113
+ settings: MailMassSendSettings = {}
114
+ ): Promise<unknown> {
115
+ return nocobaseClient.action("mailMassMessages", "send", {
116
+ body: {
117
+ data: payload,
118
+ mailSendSetting: settings,
119
+ },
120
+ });
121
+ },
122
+
123
+ async listMassMessages(
124
+ parentId: number | null = null
125
+ ): Promise<MailMassListResponse> {
126
+ const pageSize = 100;
127
+ const rows: MailMassMessage[] = [];
128
+ let page = 1;
129
+ let count = 0;
130
+
131
+ do {
132
+ const payload = await nocobaseClient.action<unknown>(
133
+ "mailMassMessages",
134
+ "list",
135
+ {
136
+ query: {
137
+ page,
138
+ pageSize,
139
+ filter: JSON.stringify({ parentId }),
140
+ sort: "-createdAt",
141
+ },
142
+ unwrap: "none",
143
+ }
144
+ );
145
+ const parsed = parseListResponse(payload);
146
+ const nextRows = parsed.rows as unknown as MailMassMessage[];
147
+ rows.push(...nextRows);
148
+ count = parsed.count;
149
+ if (!nextRows.length) break;
150
+ page += 1;
151
+ } while (rows.length < count);
152
+
153
+ return { rows, count };
154
+ },
155
+
156
+ cancelMassMessage(id: number | string): Promise<unknown> {
157
+ return nocobaseClient.action("mailMassMessages", "cancel", {
158
+ body: { id },
159
+ });
160
+ },
161
+
162
+ resendMassMessage(id: number | string): Promise<unknown> {
163
+ return nocobaseClient.action("mailMassMessages", "resend", {
164
+ body: { id },
165
+ });
166
+ },
167
+
168
+ uploadAttachment(file: File): Promise<MailUploadedAttachment> {
169
+ const formData = new FormData();
170
+ formData.append("file", file);
171
+ return nocobaseClient
172
+ .action<MailUploadedAttachment>("mail", "messageAttachmentUpload", {
173
+ body: formData,
174
+ })
175
+ .then((attachment) => ({
176
+ ...attachment,
177
+ // Google reads originalname/mimetype; Microsoft reads filename/mimeType.
178
+ // Keep both pairs aligned so recipients see the real file name and type.
179
+ filename: attachment.originalname,
180
+ mimeType: attachment.mimetype,
181
+ }));
182
+ },
183
+
184
+ saveDraft(payload: MailSendPayload): Promise<{ id: number }> {
185
+ return nocobaseClient.action<{ id: number }>("mail", "messageSavingDraft", {
186
+ body: payload,
187
+ });
188
+ },
189
+
190
+ sync(emails: string[]): Promise<unknown> {
191
+ return nocobaseClient.action("mail", "messagesSync", { body: { emails } });
192
+ },
193
+
194
+ unreadCount(): Promise<number> {
195
+ return nocobaseClient
196
+ .action<unknown>("mail", "messageUnreadCount")
197
+ .then((value) => {
198
+ if (typeof value === "number") return value;
199
+ if (value && typeof value === "object" && "count" in value) {
200
+ const count = Number((value as { count?: unknown }).count);
201
+ return Number.isFinite(count) ? count : 0;
202
+ }
203
+ return 0;
204
+ });
205
+ },
206
+
207
+ getAccounts(): Promise<MailAccount[]> {
208
+ return nocobaseClient.action<MailAccount[]>("mail", "getMailAccounts");
209
+ },
210
+
211
+ findDraft(values: {
212
+ accountEmail: string;
213
+ identityEmail: string;
214
+ to: string[];
215
+ }): Promise<MailMessage | undefined> {
216
+ return nocobaseClient
217
+ .action<MailMessage | undefined>("mailMessages", "get", {
218
+ query: {
219
+ filter: JSON.stringify({
220
+ isDraft: true,
221
+ from: values.identityEmail,
222
+ email: values.accountEmail,
223
+ to: values.to,
224
+ }),
225
+ appends: "labels,note",
226
+ },
227
+ })
228
+ .then((draft) => draft || undefined);
229
+ },
230
+
231
+ getOAuthUrl(
232
+ type: "google" | "microsoft",
233
+ options: { email?: string; reauthorize?: boolean } = {}
234
+ ): Promise<string> {
235
+ return nocobaseClient
236
+ .action<{ url?: string }>("mail", "oauth2url", {
237
+ query: { type, ...options },
238
+ })
239
+ .then((response) => response.url || "");
240
+ },
241
+
242
+ deleteAccount(email: string): Promise<unknown> {
243
+ return nocobaseClient.action("mail", "deleteMailAccount", {
244
+ body: { email },
245
+ });
246
+ },
247
+
248
+ resyncAccount(email: string): Promise<unknown> {
249
+ return nocobaseClient.action("mail", "messagesResync", {
250
+ body: { email },
251
+ });
252
+ },
253
+
254
+ syncAliases(email: string): Promise<MailIdentity[]> {
255
+ return nocobaseClient
256
+ .action<MailIdentity[]>("mail", "accountIdentitiesSync", {
257
+ body: { email },
258
+ })
259
+ .then((identities) => (Array.isArray(identities) ? identities : []));
260
+ },
261
+
262
+ getUsers(): Promise<MailUserRecord[]> {
263
+ return nocobaseClient
264
+ .action<MailUserRecord[]>("users", "list", {
265
+ query: { pageSize: 100 },
266
+ })
267
+ .then((res) => (Array.isArray(res) ? res : []));
268
+ },
269
+
270
+ getLabels(userId?: number | string): Promise<MailLabel[]> {
271
+ const filter =
272
+ userId !== undefined ? { createdBy: { id: userId } } : undefined;
273
+ return nocobaseClient
274
+ .action<MailLabel[]>("mailMessageLabels", "list", {
275
+ query: {
276
+ paginate: false,
277
+ ...(filter ? { filter: JSON.stringify(filter) } : {}),
278
+ },
279
+ })
280
+ .then((res) => (Array.isArray(res) ? res : []));
281
+ },
282
+
283
+ createLabel(values: {
284
+ label: string;
285
+ color: string;
286
+ description?: string;
287
+ }): Promise<MailLabel> {
288
+ return nocobaseClient.action<MailLabel>("mailMessageLabels", "create", {
289
+ body: values,
290
+ });
291
+ },
292
+
293
+ updateLabel(
294
+ id: number | string,
295
+ values: { label: string; color: string; description?: string }
296
+ ): Promise<MailLabel> {
297
+ return nocobaseClient.action<MailLabel>("mailMessageLabels", "update", {
298
+ query: { filterByTk: id },
299
+ body: values,
300
+ });
301
+ },
302
+
303
+ deleteLabel(id: number | string): Promise<unknown> {
304
+ return nocobaseClient.action("mailMessageLabels", "destroy", {
305
+ query: { filterByTk: id },
306
+ });
307
+ },
308
+
309
+ getTemplates(): Promise<MailTemplate[]> {
310
+ return nocobaseClient
311
+ .action<MailTemplate[]>("mailTemplates", "list", {
312
+ query: { paginate: false, sort: "createdAt" },
313
+ })
314
+ .then((res) => (Array.isArray(res) ? res : []));
315
+ },
316
+
317
+ createTemplate(values: {
318
+ name: string;
319
+ content: string;
320
+ }): Promise<MailTemplate> {
321
+ return nocobaseClient.action<MailTemplate>("mailTemplates", "create", {
322
+ body: values,
323
+ });
324
+ },
325
+
326
+ updateTemplate(
327
+ id: number | string,
328
+ values: { name: string; content: string }
329
+ ): Promise<MailTemplate> {
330
+ return nocobaseClient.action<MailTemplate>("mailTemplates", "update", {
331
+ query: { filterByTk: id },
332
+ body: values,
333
+ });
334
+ },
335
+
336
+ deleteTemplate(id: number | string): Promise<unknown> {
337
+ return nocobaseClient.action("mailTemplates", "destroy", {
338
+ query: { filterByTk: id },
339
+ });
340
+ },
341
+
342
+ updateAccount(
343
+ id: number | string,
344
+ values: Partial<MailAccount>
345
+ ): Promise<MailAccount> {
346
+ return nocobaseClient.action<MailAccount>("mailAccounts", "update", {
347
+ query: { filterByTk: id },
348
+ body: values,
349
+ });
350
+ },
351
+
352
+ setMessageLabels(
353
+ messageId: number | string,
354
+ labelIds: number[]
355
+ ): Promise<unknown> {
356
+ return nocobaseClient.action("mailMessages", "update", {
357
+ query: { filterByTk: messageId },
358
+ body: { labels: labelIds },
359
+ });
360
+ },
361
+
362
+ setTodo(messageId: number | string, isTodo: boolean): Promise<unknown> {
363
+ return nocobaseClient.action("mailMessages", "update", {
364
+ query: { filterByTk: messageId },
365
+ body: { isTodo },
366
+ });
367
+ },
368
+
369
+ setBoxType(messageId: number | string, boxType: string): Promise<unknown> {
370
+ return nocobaseClient.action("mailMessages", "update", {
371
+ query: { filterByTk: messageId },
372
+ body: { boxType },
373
+ });
374
+ },
375
+
376
+ createNote(messageId: number | string, note: string): Promise<MailNote> {
377
+ return nocobaseClient.action<MailNote>("mailMessageNotes", "create", {
378
+ body: { mailMessageId: messageId, note },
379
+ });
380
+ },
381
+
382
+ updateNote(
383
+ noteId: number | string,
384
+ messageId: number | string,
385
+ note: string
386
+ ): Promise<MailNote> {
387
+ return nocobaseClient.action<MailNote>("mailMessageNotes", "update", {
388
+ query: { filterByTk: noteId },
389
+ body: { mailMessageId: messageId, note },
390
+ });
391
+ },
392
+
393
+ attachmentUrl(messageId: number | string, attachmentId: string): string {
394
+ return nocobaseClient
395
+ .buildUrl("mail:messageAttachmentGet", {
396
+ id: messageId,
397
+ attachmentId,
398
+ })
399
+ .toString();
400
+ },
401
+
402
+ inlineImageUrl(messageId: number | string, contentId: string): string {
403
+ return nocobaseClient
404
+ .buildUrl("mail:messageContentPreview", { messageId, contentId })
405
+ .toString();
406
+ },
407
+
408
+ async fetchInlineImage(
409
+ messageId: number | string,
410
+ contentId: string
411
+ ): Promise<Blob> {
412
+ const response = await fetch(this.inlineImageUrl(messageId, contentId), {
413
+ headers: nocobaseClient.getHeaders({
414
+ withAclMeta: false,
415
+ headers: { Accept: "image/*" },
416
+ }),
417
+ });
418
+ if (!response.ok) {
419
+ throw new Error(`Inline image request failed (${response.status})`);
420
+ }
421
+ return response.blob();
422
+ },
423
+ };
@@ -0,0 +1,67 @@
1
+ import { Download, FileText, Image as ImageIcon, Paperclip } from "lucide-react";
2
+ import type { MailAttachment } from "./types";
3
+ import { mailApi } from "./mail-api";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function formatSize(size?: number) {
7
+ if (!size) return "";
8
+ if (size < 1024) return `${size} B`;
9
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
10
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
11
+ }
12
+
13
+ function isImage(mimeType: string) {
14
+ return mimeType.startsWith("image/");
15
+ }
16
+
17
+ export function MailAttachmentList({
18
+ messageId,
19
+ attachments,
20
+ className,
21
+ }: {
22
+ messageId: number | string;
23
+ attachments: MailAttachment[];
24
+ className?: string;
25
+ }) {
26
+ if (!attachments?.length) return null;
27
+
28
+ return (
29
+ <div className={cn("flex flex-col gap-2", className)}>
30
+ <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
31
+ <Paperclip className="size-3.5" />
32
+ {attachments.length} attachment{attachments.length > 1 ? "s" : ""}
33
+ </div>
34
+ <div className="flex flex-wrap gap-2">
35
+ {attachments.map((att) => (
36
+ <a
37
+ key={att.attachmentId}
38
+ href={mailApi.attachmentUrl(messageId, att.attachmentId)}
39
+ target="_blank"
40
+ rel="noreferrer"
41
+ className={cn(
42
+ "group flex items-center gap-2.5 rounded-lg border border-border/80 bg-card px-3 py-2",
43
+ "shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-all hover:border-primary/40 hover:shadow-[0_2px_8px_rgba(0,0,0,0.06)]"
44
+ )}
45
+ >
46
+ {isImage(att.mimeType) ? (
47
+ <ImageIcon className="size-4 shrink-0 text-sky-500" />
48
+ ) : (
49
+ <FileText className="size-4 shrink-0 text-muted-foreground" />
50
+ )}
51
+ <div className="min-w-0">
52
+ <div className="max-w-[180px] truncate text-xs font-medium text-foreground">
53
+ {att.filename}
54
+ </div>
55
+ {att.size ? (
56
+ <div className="text-[10px] text-muted-foreground">
57
+ {formatSize(att.size)}
58
+ </div>
59
+ ) : null}
60
+ </div>
61
+ <Download className="size-3.5 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
62
+ </a>
63
+ ))}
64
+ </div>
65
+ </div>
66
+ );
67
+ }