@nocobase/app-plugin-mail 1.0.0-beta.12 → 1.0.0-beta.14

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 (112) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +33 -0
  3. package/dist/client/components/ui/field.d.ts +7 -0
  4. package/dist/client/components/ui/field.d.ts.map +1 -0
  5. package/dist/client/components/ui/field.js +20 -0
  6. package/dist/client/components/ui/field.js.map +1 -0
  7. package/dist/client/hooks/use-mail-workspace-data.d.ts +3 -1
  8. package/dist/client/hooks/use-mail-workspace-data.d.ts.map +1 -1
  9. package/dist/client/hooks/use-mail-workspace-data.js +14 -2
  10. package/dist/client/hooks/use-mail-workspace-data.js.map +1 -1
  11. package/dist/client/locales/en-US.d.ts +3 -0
  12. package/dist/client/locales/en-US.d.ts.map +1 -1
  13. package/dist/client/locales/en-US.js +3 -0
  14. package/dist/client/locales/en-US.js.map +1 -1
  15. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  16. package/dist/client/locales/zh-CN.js +3 -0
  17. package/dist/client/locales/zh-CN.js.map +1 -1
  18. package/dist/client/mail-client.d.ts +5 -1
  19. package/dist/client/mail-client.d.ts.map +1 -1
  20. package/dist/client/mail-client.js.map +1 -1
  21. package/dist/client/pages/mail-workspace-page.d.ts.map +1 -1
  22. package/dist/client/pages/mail-workspace-page.js +39 -10
  23. package/dist/client/pages/mail-workspace-page.js.map +1 -1
  24. package/dist/database/migrations/.manifest.json +10 -2
  25. package/dist/database/migrations/202609030001_create_mail_tables.d.ts.map +1 -1
  26. package/dist/database/migrations/202609030001_create_mail_tables.js +8 -4
  27. package/dist/database/migrations/202609030001_create_mail_tables.js.map +1 -1
  28. package/dist/database/migrations/202610090001_mail_create_message_participants.d.ts +4 -0
  29. package/dist/database/migrations/202610090001_mail_create_message_participants.d.ts.map +1 -0
  30. package/dist/database/migrations/202610090001_mail_create_message_participants.js +167 -0
  31. package/dist/database/migrations/202610090001_mail_create_message_participants.js.map +1 -0
  32. package/dist/database/migrations/202610250001_mail_imap_message_locations.d.ts +4 -0
  33. package/dist/database/migrations/202610250001_mail_imap_message_locations.d.ts.map +1 -0
  34. package/dist/database/migrations/202610250001_mail_imap_message_locations.js +297 -0
  35. package/dist/database/migrations/202610250001_mail_imap_message_locations.js.map +1 -0
  36. package/dist/server/adapters/imap-smtp/adapter.d.ts +60 -3
  37. package/dist/server/adapters/imap-smtp/adapter.d.ts.map +1 -1
  38. package/dist/server/adapters/imap-smtp/adapter.js +774 -83
  39. package/dist/server/adapters/imap-smtp/adapter.js.map +1 -1
  40. package/dist/server/adapters/imap-smtp/constants.d.ts +6 -0
  41. package/dist/server/adapters/imap-smtp/constants.d.ts.map +1 -1
  42. package/dist/server/adapters/imap-smtp/constants.js +6 -0
  43. package/dist/server/adapters/imap-smtp/constants.js.map +1 -1
  44. package/dist/server/adapters/imap-smtp/content.d.ts +11 -0
  45. package/dist/server/adapters/imap-smtp/content.d.ts.map +1 -1
  46. package/dist/server/adapters/imap-smtp/content.js +82 -3
  47. package/dist/server/adapters/imap-smtp/content.js.map +1 -1
  48. package/dist/server/adapters/imap-smtp/definition.d.ts +7 -0
  49. package/dist/server/adapters/imap-smtp/definition.d.ts.map +1 -1
  50. package/dist/server/adapters/imap-smtp/definition.js +19 -11
  51. package/dist/server/adapters/imap-smtp/definition.js.map +1 -1
  52. package/dist/server/adapters/imap-smtp/locators.d.ts +7 -0
  53. package/dist/server/adapters/imap-smtp/locators.d.ts.map +1 -1
  54. package/dist/server/adapters/imap-smtp/locators.js +53 -3
  55. package/dist/server/adapters/imap-smtp/locators.js.map +1 -1
  56. package/dist/server/adapters/imap-smtp/types.d.ts +87 -0
  57. package/dist/server/adapters/imap-smtp/types.d.ts.map +1 -1
  58. package/dist/server/operations/message-mutations.d.ts.map +1 -1
  59. package/dist/server/operations/message-mutations.js +57 -5
  60. package/dist/server/operations/message-mutations.js.map +1 -1
  61. package/dist/server/operations/sync-mailbox.d.ts.map +1 -1
  62. package/dist/server/operations/sync-mailbox.js +26 -10
  63. package/dist/server/operations/sync-mailbox.js.map +1 -1
  64. package/dist/server/providers/mail-builtins.d.ts.map +1 -1
  65. package/dist/server/providers/mail-builtins.js +6 -2
  66. package/dist/server/providers/mail-builtins.js.map +1 -1
  67. package/dist/server/routes/api.d.ts.map +1 -1
  68. package/dist/server/routes/api.js +3 -1
  69. package/dist/server/routes/api.js.map +1 -1
  70. package/dist/server/routes/schemas.d.ts +2 -0
  71. package/dist/server/routes/schemas.d.ts.map +1 -1
  72. package/dist/server/routes/schemas.js +13 -0
  73. package/dist/server/routes/schemas.js.map +1 -1
  74. package/dist/server/store/account-removals.d.ts.map +1 -1
  75. package/dist/server/store/account-removals.js +6 -0
  76. package/dist/server/store/account-removals.js.map +1 -1
  77. package/dist/server/store/attachments.d.ts.map +1 -1
  78. package/dist/server/store/attachments.js +3 -0
  79. package/dist/server/store/attachments.js.map +1 -1
  80. package/dist/server/store/draft-states.d.ts.map +1 -1
  81. package/dist/server/store/draft-states.js +2 -0
  82. package/dist/server/store/draft-states.js.map +1 -1
  83. package/dist/server/store/message-participants.d.ts +25 -0
  84. package/dist/server/store/message-participants.d.ts.map +1 -0
  85. package/dist/server/store/message-participants.js +109 -0
  86. package/dist/server/store/message-participants.js.map +1 -0
  87. package/dist/server/store/message-writes.d.ts +24 -1
  88. package/dist/server/store/message-writes.d.ts.map +1 -1
  89. package/dist/server/store/message-writes.js +360 -1
  90. package/dist/server/store/message-writes.js.map +1 -1
  91. package/dist/server/store/messages.d.ts.map +1 -1
  92. package/dist/server/store/messages.js +26 -3
  93. package/dist/server/store/messages.js.map +1 -1
  94. package/dist/server/store/rows.d.ts +20 -0
  95. package/dist/server/store/rows.d.ts.map +1 -1
  96. package/dist/server/store/sync.d.ts +7 -1
  97. package/dist/server/store/sync.d.ts.map +1 -1
  98. package/dist/server/store/sync.js +333 -7
  99. package/dist/server/store/sync.js.map +1 -1
  100. package/dist/shared/mail.d.ts +2 -0
  101. package/dist/shared/mail.d.ts.map +1 -1
  102. package/dist/shared/mail.js.map +1 -1
  103. package/dist/shared/participant.d.ts +14 -0
  104. package/dist/shared/participant.d.ts.map +1 -0
  105. package/dist/shared/participant.js +45 -0
  106. package/dist/shared/participant.js.map +1 -0
  107. package/package.json +12 -11
  108. package/skills/nocobase-app-plugin-mail/SKILL.md +9 -7
  109. package/skills/nocobase-app-plugin-mail/references/client-integration.md +19 -1
  110. package/skills/nocobase-app-plugin-mail/references/http-api.md +32 -24
  111. package/skills/nocobase-app-plugin-mail/references/public-api.md +2 -0
  112. package/skills/nocobase-app-plugin-mail/references/synchronization-and-diagnostics.md +14 -0
@@ -0,0 +1,14 @@
1
+ /** The participant index accepts ASCII dot-atom mailboxes and DNS domains. */
2
+ export interface NormalizedMailParticipantAddress {
3
+ readonly address: string;
4
+ readonly domain: string;
5
+ }
6
+ export interface MailParticipantFilter {
7
+ readonly field: 'address' | 'domain';
8
+ readonly value: string;
9
+ }
10
+ /** Trim and lowercase without alias folding, truncation, or display-name parsing. */
11
+ export declare function normalizeMailParticipantAddress(value: unknown): NormalizedMailParticipantAddress | undefined;
12
+ /** An omitted filter is distinct from an explicitly empty or malformed one. */
13
+ export declare function parseMailParticipant(value: string): MailParticipantFilter | undefined;
14
+ //# sourceMappingURL=participant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participant.d.ts","sourceRoot":"","sources":["../../shared/participant.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAkBD,qFAAqF;AACrF,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,OAAO,GACb,gCAAgC,GAAG,SAAS,CAiB9C;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GACZ,qBAAqB,GAAG,SAAS,CAUnC"}
@@ -0,0 +1,45 @@
1
+ function normalizeDomain(value) {
2
+ const domain = value.toLowerCase();
3
+ if (domain.length > 253 || !domain.includes('.'))
4
+ return undefined;
5
+ const labels = domain.split('.');
6
+ if (labels.some((label) => label.length === 0 ||
7
+ label.length > 63 ||
8
+ !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label)))
9
+ return undefined;
10
+ return domain;
11
+ }
12
+ /** Trim and lowercase without alias folding, truncation, or display-name parsing. */
13
+ export function normalizeMailParticipantAddress(value) {
14
+ if (typeof value !== 'string')
15
+ return undefined;
16
+ if (/[^\u0021-\u007e]/.test(value.trim()))
17
+ return undefined;
18
+ const address = value.trim().toLowerCase();
19
+ if (address.length > 320)
20
+ return undefined;
21
+ const at = address.indexOf('@');
22
+ if (at <= 0 || at !== address.lastIndexOf('@'))
23
+ return undefined;
24
+ const local = address.slice(0, at);
25
+ if (local.length > 64 ||
26
+ !/^[a-z0-9!#$%&'*+\-/=?^_`{|}~]+(?:\.[a-z0-9!#$%&'*+\-/=?^_`{|}~]+)*$/.test(local))
27
+ return undefined;
28
+ const domain = normalizeDomain(address.slice(at + 1));
29
+ return domain ? { address, domain } : undefined;
30
+ }
31
+ /** An omitted filter is distinct from an explicitly empty or malformed one. */
32
+ export function parseMailParticipant(value) {
33
+ if (value.length > 320)
34
+ return undefined;
35
+ if (/[^\u0021-\u007e]/.test(value.trim()))
36
+ return undefined;
37
+ const normalized = value.trim().toLowerCase();
38
+ if (normalized.startsWith('@')) {
39
+ const domain = normalizeDomain(normalized.slice(1));
40
+ return domain ? { field: 'domain', value: domain } : undefined;
41
+ }
42
+ const mailbox = normalizeMailParticipantAddress(normalized);
43
+ return mailbox ? { field: 'address', value: mailbox.address } : undefined;
44
+ }
45
+ //# sourceMappingURL=participant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"participant.js","sourceRoot":"","sources":["../../shared/participant.ts"],"names":[],"mappings":"AAWA,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACnC,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACnE,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,IACE,MAAM,CAAC,IAAI,CACT,CAAC,KAAK,EAAE,EAAE,CACR,KAAK,CAAC,MAAM,KAAK,CAAC;QAClB,KAAK,CAAC,MAAM,GAAG,EAAE;QACjB,CAAC,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC,CACnD;QAED,OAAO,SAAS,CAAC;IACnB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,+BAA+B,CAC7C,KAAc;IAEd,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAC5D,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACnC,IACE,KAAK,CAAC,MAAM,GAAG,EAAE;QACjB,CAAC,qEAAqE,CAAC,IAAI,CACzE,KAAK,CACN;QAED,OAAO,SAAS,CAAC;IACnB,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;IACtD,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAClD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,oBAAoB,CAClC,KAAa;IAEb,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAAE,OAAO,SAAS,CAAC;IAC5D,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACjE,CAAC;IACD,MAAM,OAAO,GAAG,+BAA+B,CAAC,UAAU,CAAC,CAAC;IAC5D,OAAO,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5E,CAAC"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@nocobase/app-plugin-mail",
3
3
  "displayName": "Mail App Plugin",
4
4
  "description": "Provides mail sending and resumable mailbox synchronization architecture.",
5
- "version": "1.0.0-beta.12",
5
+ "version": "1.0.0-beta.14",
6
6
  "type": "module",
7
7
  "prettier": "@nocobase/dev-config/prettier",
8
8
  "engines": {
@@ -62,17 +62,17 @@
62
62
  "peerDependencies": {
63
63
  "@base-ui/react": "^1.6.0",
64
64
  "class-variance-authority": "^0.7.1",
65
+ "cn": "^0.4.0",
66
+ "date-fns": "^4.4.0",
65
67
  "lucide-react": "^0.487.0",
66
68
  "react": "^19.0.0",
67
- "react-dom": "^19.0.0",
68
- "date-fns": "^4.4.0",
69
69
  "react-day-picker": "^10.0.1",
70
- "cn": "^0.4.0",
70
+ "react-dom": "^19.0.0",
71
71
  "@nocobase/app-client": "^3.0.0-beta.1",
72
- "@nocobase/app-plugin-authentication": "^2.0.0-beta.7",
72
+ "@nocobase/app-plugin-authentication": "^2.0.0-beta.9",
73
73
  "@nocobase/app-plugin-authorization": "^1.0.0-beta.27",
74
- "@nocobase/app-server": "^2.0.0-beta.5",
75
- "@nocobase/db": "^1.0.0-beta.19",
74
+ "@nocobase/app-server": "^2.0.0-beta.7",
75
+ "@nocobase/db": "^1.0.0-beta.20",
76
76
  "@nocobase/i18n": "^1.0.0-beta.7",
77
77
  "@nocobase/jobs": "^0.1.0-beta.3",
78
78
  "@nocobase/service-provider": "^0.0.2-beta.2"
@@ -86,8 +86,10 @@
86
86
  "zod": "^4.6.5"
87
87
  },
88
88
  "devDependencies": {
89
+ "@playwright/test": "^1.62.1",
89
90
  "@testing-library/jest-dom": "^7.0.0",
90
91
  "@testing-library/react": "^16.3.2",
92
+ "@testing-library/user-event": "^14.6.3",
91
93
  "@types/mailparser": "^3.4.6",
92
94
  "@types/node": "^24.0.0",
93
95
  "@types/nodemailer": "^8.0.1",
@@ -104,10 +106,9 @@
104
106
  "tw-animate-css": "^1.2.5",
105
107
  "typescript": "^6.0.3",
106
108
  "vitest": "^4.1.10",
107
- "@playwright/test": "^1.62.1",
108
- "@nocobase/dev-config": "0.1.0-beta.22",
109
- "@nocobase/app-testing": "0.1.0-beta.3",
110
- "@nocobase/config": "0.1.0-beta.3"
109
+ "@nocobase/app-testing": "0.1.0-beta.4",
110
+ "@nocobase/config": "0.1.0-beta.3",
111
+ "@nocobase/dev-config": "0.1.0-beta.22"
111
112
  },
112
113
  "repository": {
113
114
  "type": "git",
@@ -4,7 +4,7 @@ description: Use when integrating the NocoBase 3 Mail plugin into an application
4
4
  argument-hint: "[action: inspect|configure|integrate|send|diagnose] [target]"
5
5
  allowed-tools: Read, Grep, Glob, Bash
6
6
  owner: mail-plugin-team
7
- version: 1.1.2
7
+ version: 1.2.0
8
8
  last-reviewed: 2026-09-26
9
9
  risk-level: medium
10
10
  ---
@@ -62,17 +62,19 @@ When the user says “you decide”, use `provider=auto`, `scope=local`, and `va
62
62
 
63
63
  # Reference Loading Map
64
64
 
65
- | Task | Read |
66
- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
67
- | Register Mail, configure provider quotas, connect an account, configure OAuth/push, or replace credential storage | [Configuration and accounts](references/configuration-and-accounts.md) |
68
- | Embed a production workspace, connect business records, customize reading/composition, or add management and log pages | [Client integration](references/client-integration.md) |
69
- | Send from business code, manage drafts or attachments, schedule or bulk-send, or handle uncertain delivery | [Sending and drafts](references/sending-and-drafts.md) |
70
- | Set an initial sync boundary, handle Gmail quota errors, recover synchronization, or diagnose stalled tasks and logs | [Synchronization and diagnostics](references/synchronization-and-diagnostics.md) |
65
+ | Task | Read |
66
+ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
67
+ | Register Mail, configure provider quotas, connect an account, configure OAuth/push, or replace credential storage | [Configuration and accounts](references/configuration-and-accounts.md) |
68
+ | Embed a production workspace, connect business records, customize reading/composition, or add management and log pages | [Client integration](references/client-integration.md) |
69
+ | Send from business code, manage drafts or attachments, schedule or bulk-send, or handle uncertain delivery | [Sending and drafts](references/sending-and-drafts.md) |
70
+ | Upgrade the participant index, set an initial sync boundary, handle Gmail quota errors, recover synchronization, or diagnose stalled tasks and logs | [Synchronization and diagnostics](references/synchronization-and-diagnostics.md) |
71
71
 
72
72
  # Safety Gate
73
73
 
74
74
  Do not expose credential values in logs, reports, screenshots, or test output. The default credential vault stores plain JSON in the database; promise encryption only when an application-supplied vault is configured.
75
75
 
76
+ Participant-index upgrades require a backed-up maintenance window with application writes and Mail synchronization stopped; complete and verify the historical backfill before restarting or exposing filtered results. Bounded batches do not make this an online backfill. Read the [upgrade checklist](references/synchronization-and-diagnostics.md#participant-index-upgrade) before deployment.
77
+
76
78
  Treat `accepted` as provider acceptance, not recipient delivery; `unknown` may mean the provider already sent the message. Do not retry an uncertain submission until the sending reference and provider state have been checked.
77
79
 
78
80
  For destructive or external actions, keep the operation reversible where possible. If a configuration write is wrong, restore the read-back value from the prior snapshot and report the rollback. If a mailbox mutation is wrong, stop further mutations and use the provider's supported move, restore, or deletion-recovery operation; do not edit Mail tables directly. See Rollback Guidance below.
@@ -47,6 +47,24 @@ export function CustomerMail() {
47
47
 
48
48
  `{{record.customer.name}}` resolves when applying a template. Unknown variables stay visible. Applying a template replaces subject and body. This does not associate messages with a customer or filter correspondence automatically; implement explicitly requested record relationships in the application's domain layer.
49
49
 
50
+ ## CRM correspondence and participant filtering
51
+
52
+ Use the application-owned client to load exact correspondence, independently of template variables:
53
+
54
+ ```ts
55
+ const mail = app.services.resolve(mailClientToken);
56
+ const customerMail = await mail.listMessages({
57
+ participant: 'alice@example.com',
58
+ });
59
+ const companyMail = await mail.listMessages({ participant: '@example.com' });
60
+ ```
61
+
62
+ In React, obtain `mail` with `useMailClient()`. `participant` is optional in `MailMessagesQuery` and `MailManagedMessagesQuery` and is forwarded unchanged to the API. The server trims surrounding whitespace and lowercases the entire mailbox/domain before an exact From/To/Cc match. It accepts ASCII dot-atom mailboxes and dotted DNS domains, not display-name forms, quoted local parts or Unicode addresses. Domain input must start with `@`; `@example.com` excludes subdomains, similar domains and suffix tricks. There is no alias or `+tag` folding. Bcc, Reply-To, display names, subject, preview and body do not participate. `q` keeps its existing fuzzy behavior, including display names and Bcc, and intersects with participant and other filters. Results include only locally synchronized mail within the caller's existing account permissions; do not retry a denied personal request against management APIs.
63
+
64
+ The workspace exposes a separate Participant field with full-address/`@domain` help and accessible validation errors. It uses the existing 300 ms keyword debounce interval, resets page tokens, list and reader selection immediately on every participant edit or clear, and ignores stale responses. Invalid input issues no list request. Clearing omits the parameter instead of sending an empty string. A custom CRM page must likewise clear tokens/selections, guard outstanding responses and block invalid filters; an explicit empty, invalid or oversized API value returns `400 INVALID_INPUT` with a `participant` field violation. `MailWorkspacePageProps` does not provide an externally controlled participant prop; own CRM filtering state in a custom page rather than assuming template variables set the filter.
65
+
66
+ List totals and paging apply the filter before pagination; opening a conversation still loads the complete thread, and the conversation badge keeps its existing full-thread count. Separate IMAP folder copies are not deduplicated by this filter. Verify historical results only after the [participant index upgrade](synchronization-and-diagnostics.md#participant-index-upgrade) has completed.
67
+
50
68
  ## Build a custom mail page
51
69
 
52
70
  Use `useMailClient()` for typed data access and own page state in the application. `listMessages()` accepts `MAIL_VIRTUAL_FOLDER_IDS` for built-in cross-account folders; `getMessage()` or `listConversationMessages()` loads detail; mutation methods such as `updateMessage()`, `moveMessage()` and `sendMessage()` perform the corresponding server operation. A custom page can combine these calls with `MailMessageList`, `MailConversationView`, `MailboxSidebar` and `MailComposer`, or render its own UI. The application owns filtering, loading/error state, cursor handling, selected-message state and refreshing after the user-scoped realtime invalidation event.
@@ -191,7 +209,7 @@ Completion callbacks may return `void` or `Promise<void>`. Legacy callbacks that
191
209
 
192
210
  All-user detail and attachment APIs live under `/api/mail/management/accounts/:accountId/messages/:messageId`, with `/attachments/:attachmentId` for downloads. They require management permission. Opening management detail is read-only and does not mark mail read; draft rows use draft status and skip read-state actions. Moving to folders belongs to the personal workspace. Keep personal and all-user client paths explicit instead of retrying permission failures against a broader API.
193
211
 
194
- The Mail center pages by cursor, 50 messages per page: `listMessages()` and `listConversationMessages()` take `pageSize` and `pageToken`, and return `{ items, total, nextCursor }`, where `nextCursor` is the opaque token to send back as the next `pageToken` and is absent on the last page. Management and log tables page by number with page-size choices 20, 50 and 100: `listManagedMessages()`, `listSyncRunsPage()` and `listSubmissionsPage()` take `{ page, pageSize }` (page from 1, `pageSize` at most 100) and return `{ items, total }`. Reset pagination after filter or page-size changes. Search is `q`.
212
+ The Mail center pages by cursor, 50 messages per page: `listMessages()` and `listConversationMessages()` take `pageSize` and `pageToken`, and return `{ items, total, nextCursor }`, where `nextCursor` is the opaque token to send back as the next `pageToken` and is absent on the last page. Management and log tables page by number with page-size choices 20, 50 and 100: `listManagedMessages()`, `listSyncRunsPage()` and `listSubmissionsPage()` take `{ page, pageSize }` (page from 1, `pageSize` at most 100) and return `{ items, total }`. Reset pagination and selection after filter or page-size changes. Keyword search is `q`; exact From/To/Cc filtering is the independent optional `participant`.
195
213
 
196
214
  Grouped bulk history counts complete batches, not recipients: call `listSubmissionsPage({ bulkOnly: true, groupByBatch: true, page, pageSize })`. Keep expandable recipient results inside their batch. Retry/cancel eligibility is defined in the sending reference.
197
215
 
@@ -77,24 +77,32 @@ Mail publishes the user-scoped realtime topic `mail:messages` with `{ kind: 'mai
77
77
 
78
78
  ## Messages, conversations, attachments, and sending
79
79
 
80
- | Method | Path | Request / result |
81
- | -------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
82
- | `GET` | `/messages` | Filters by `accountId`, `folderId`, `labelId`, `conversationId`, `q`, `unread`, and `starred`; pages with `pageSize` and `pageToken`. |
83
- | `GET` | `/messages/countUnread` | Returns the current user's unread count as a number. |
84
- | `POST` | `/messages/send` | Accepts `MailComposeInput`; returns `MailSubmissionView`. |
85
- | `POST` | `/messages/sendBulk` | Accepts `MailBulkComposeInput`; returns `MailSubmissionView[]`. |
86
- | `POST` | `/messages/saveDraft` | Accepts draft compose fields; creates the draft or replaces the one named by `draftMessageId` or `draftKey`; returns `MailMessage`. |
87
- | `GET` | `/accounts/:accountId/conversations/:conversationId/messages` | Conversation messages, paged with `pageSize` and `pageToken`. |
88
- | `GET` | `/accounts/:accountId/messages/:messageId` | Returns a `MailMessage`. |
89
- | `PATCH` | `/accounts/:accountId/messages/:messageId` | Optional read/starred/note/todo changes; returns the updated `MailMessage`. |
90
- | `DELETE` | `/accounts/:accountId/messages/:messageId` | Optional `permanently=true` query parameter; returns `204`. |
91
- | `GET` | `/accounts/:accountId/messages/:messageId/attachments/:attachmentId` | Streams a personal mailbox attachment. |
92
- | `POST` | `/accounts/:accountId/messages/:messageId/modifyLabels` | `addLabelIds` and/or `removeLabelIds`; returns the updated `MailMessage`. |
93
- | `POST` | `/accounts/:accountId/messages/:messageId/move` | Requires `providerFolderId`; returns the updated `MailMessage`. |
94
- | `POST` | `/accounts/:accountId/messages/:messageId/retryContent` | Retries deferred message content retrieval; returns the updated `MailMessage`. |
95
- | `POST` | `/accounts/:accountId/messages/:messageId/resolveDraftConflict` | Requires `action` `useRemote` or `keepLocal`; returns the resolved `MailMessage`. |
96
- | `POST` | `/attachments` | `multipart/form-data` with field `file`, at most 27 MiB; returns `201` and `MailOutboundAttachmentView`. |
97
- | `GET` | `/attachments/:attachmentId` | Streams an uploaded outbound attachment. |
80
+ | Method | Path | Request / result |
81
+ | -------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `GET` | `/messages` | Filters by `accountId`, `folderId`, `labelId`, `conversationId`, `q`, `participant`, `unread`, and `starred`; pages with `pageSize` and `pageToken`. |
83
+ | `GET` | `/messages/countUnread` | Returns the current user's unread count as a number. |
84
+ | `POST` | `/messages/send` | Accepts `MailComposeInput`; returns `MailSubmissionView`. |
85
+ | `POST` | `/messages/sendBulk` | Accepts `MailBulkComposeInput`; returns `MailSubmissionView[]`. |
86
+ | `POST` | `/messages/saveDraft` | Accepts draft compose fields; creates the draft or replaces the one named by `draftMessageId` or `draftKey`; returns `MailMessage`. |
87
+ | `GET` | `/accounts/:accountId/conversations/:conversationId/messages` | Conversation messages, paged with `pageSize` and `pageToken`. |
88
+ | `GET` | `/accounts/:accountId/messages/:messageId` | Returns a `MailMessage`. |
89
+ | `PATCH` | `/accounts/:accountId/messages/:messageId` | Optional read/starred/note/todo changes; returns the updated `MailMessage`. |
90
+ | `DELETE` | `/accounts/:accountId/messages/:messageId` | Optional `permanently=true` query parameter; returns `204`. |
91
+ | `GET` | `/accounts/:accountId/messages/:messageId/attachments/:attachmentId` | Streams a personal mailbox attachment. |
92
+ | `POST` | `/accounts/:accountId/messages/:messageId/modifyLabels` | `addLabelIds` and/or `removeLabelIds`; returns the updated `MailMessage`. |
93
+ | `POST` | `/accounts/:accountId/messages/:messageId/move` | Requires `providerFolderId`; returns the updated `MailMessage`. |
94
+ | `POST` | `/accounts/:accountId/messages/:messageId/retryContent` | Retries deferred message content retrieval; returns the updated `MailMessage`. |
95
+ | `POST` | `/accounts/:accountId/messages/:messageId/resolveDraftConflict` | Requires `action` `useRemote` or `keepLocal`; returns the resolved `MailMessage`. |
96
+ | `POST` | `/attachments` | `multipart/form-data` with field `file`, at most 27 MiB; returns `201` and `MailOutboundAttachmentView`. |
97
+ | `GET` | `/attachments/:attachmentId` | Streams an uploaded outbound attachment. |
98
+
99
+ ### Participant query semantics
100
+
101
+ `GET /messages` and `GET /management/messages` accept optional `participant` as a full mailbox (`alice@example.com`) or an exact domain (`@example.com`). The server trims leading/trailing whitespace and lowercases the whole mailbox/domain. Supported input is an ASCII dot-atom local part (at most 64 characters) with a dotted DNS domain (at most 253 characters, labels at most 63); the query value is at most 320 characters. Display-name forms, quoted local parts, Unicode addresses and malformed/empty values are rejected with `400 INVALID_ARGUMENT`, reason `INVALID_INPUT`, and a `participant` field violation. Permission is checked first. Omit the parameter to disable it.
102
+
103
+ Only actual From, To and Cc addresses match. Subject, preview, body, display names, Bcc and Reply-To are excluded; aliases and `+tag` addresses are not folded. `@example.com` does not match `sub.example.com`, `notexample.com` or `example.com.evil`. Existing `q` fuzzy search is unchanged (including display names and Bcc) and intersects with participant and all other filters. Ownership, management permission, sort order and IMAP-copy record granularity are unchanged. The predicate applies before totals and pagination and never duplicates a matching message because several participants match. Searches cover locally synchronized mail only.
104
+
105
+ Conversation-message detail and full-thread counts retain their existing semantics: a filtered list row can open a complete thread containing other participants. The internal participant index has no public CRUD endpoint. Existing installations must finish the [maintenance-window backfill](synchronization-and-diagnostics.md#participant-index-upgrade) before relying on historical filtered results.
98
106
 
99
107
  ## Synchronization and delivery history
100
108
 
@@ -111,12 +119,12 @@ Mail publishes the user-scoped realtime topic `mail:messages` with `{ kind: 'mai
111
119
 
112
120
  ## All-user management
113
121
 
114
- | Method | Path | Request / result |
115
- | ------ | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
116
- | `GET` | `/management/messages` | Filters by `accountId`, `folderId`, `q`, `unread`, and `starred`; paged with `page` and `pageSize`. |
117
- | `POST` | `/management/messages/batchApply` | Accepts `MailManagementMessageActionInput`; returns per-item results. |
118
- | `GET` | `/management/accounts/:accountId/messages/:messageId` | Returns a read-only `MailMessage`; does not mark it read. |
119
- | `GET` | `/management/accounts/:accountId/messages/:messageId/attachments/:attachmentId` | Streams an all-user management attachment. |
122
+ | Method | Path | Request / result |
123
+ | ------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
124
+ | `GET` | `/management/messages` | Filters by `accountId`, `folderId`, `q`, `participant`, `unread`, and `starred`; paged with `page` and `pageSize`. |
125
+ | `POST` | `/management/messages/batchApply` | Accepts `MailManagementMessageActionInput`; returns per-item results. |
126
+ | `GET` | `/management/accounts/:accountId/messages/:messageId` | Returns a read-only `MailMessage`; does not mark it read. |
127
+ | `GET` | `/management/accounts/:accountId/messages/:messageId/attachments/:attachmentId` | Streams an all-user management attachment. |
120
128
 
121
129
  ## OAuth and Provider webhooks
122
130
 
@@ -84,6 +84,8 @@ Output status, folder-type and Provider error category/reason types have `Known.
84
84
  - **Synchronization:** `startSync`, `getSyncRun`, `listSyncRunsPage`, `retrySyncRun`, `cancelSyncRun`.
85
85
  - **Delivery history:** `listSubmissionsPage`, `retrySubmission`, `cancelSubmission`.
86
86
 
87
+ `MailMessagesQuery` and `MailManagedMessagesQuery` include an optional `participant: string` for an exact normalized From/To/Cc mailbox or `@domain` match, independent of `q`. `MailClient` forwards it unchanged; omit it to clear the filter. List filtering does not trim full conversation details or change full-thread counts. See [CRM correspondence](client-integration.md#crm-correspondence-and-participant-filtering) for examples, validation and normalization boundaries.
88
+
87
89
  The TypeScript signatures are the request/result source of truth. For direct HTTP clients, see [HTTP API](http-api.md).
88
90
 
89
91
  ## Reusable components
@@ -8,6 +8,20 @@ Automatic synchronization is configured globally through `mail.automaticSyncInte
8
8
 
9
9
  `mail.syncBatchSize` or `MAIL_SYNC_BATCH_SIZE` defaults to 100, with an integer range of 1–200. This bounds each provider list page, not the total imported history. The account screen defaults a new initial sync to one calendar month ago. Direct account-creation APIs require `initialSyncReceivedAfter`; direct initial-sync requests must carry `receivedAfter` unless the account already has a saved start date. Both are RFC 3339 date-times such as `2026-01-01T00:00:00Z`. Legacy accounts without one use a one-month boundary for automatic or push-triggered first syncs. An active pre-upgrade initial run missing a valid boundary restarts with the same one-month fallback; Gmail rejects any initial list call without a valid date. The sync request no longer accepts `maxMessages`; an import is bounded only by its start date.
10
10
 
11
+ ## Participant index upgrade
12
+
13
+ The participant feature adds an internal `mailMessageParticipants` collection and indexes, with one derived row per normalized From/To/Cc role/address. Historical data is backfilled from stored sender/recipient JSON, without provider requests or changes to original message JSON, timestamps, bodies or folders. Unsupported or invalid historical addresses/JSON are skipped with non-sensitive diagnostic counts; valid addresses from the same message remain eligible. Do not edit the index directly or treat partial backfill results as complete.
14
+
15
+ Before production deployment, measure representative and large mailboxes: message and participant row counts, migration time, index storage, write overhead, locks and transaction-log usage. Large To/Cc sets increase participant rows and insertion cost. Keyset reads and bounded insert batches constrain memory and binding parameters, not total transaction duration or lock time; the migration is not an online backfill. If measurements do not fit a maintenance window, stop and plan a separately designed resumable task with completion and concurrency guarantees.
16
+
17
+ 1. With explicit production authorization, take and verify a restorable database backup and schedule the measured maintenance window.
18
+ 2. Stop application writes and all Mail/background sync workers, including other processes sharing the database; do not run the old version concurrently with backfill.
19
+ 3. Deploy the upgraded artifacts and run the application's migration command (`pnpm nocobase db apply` in a source application, or `node dist/cli/index.js db apply` in a deployment). This applies the schema and historical backfill together.
20
+ 4. Verify successful migration completion, inspect non-sensitive skip counts, and check known historical From/To/Cc full-address and `@domain` queries, exclusions, totals and pagination. Verify original messages are intact before enabling traffic.
21
+ 5. Only then restart the upgraded application and sync workers and verify newly synchronized messages appear in participant queries.
22
+
23
+ On failure, leave writes and synchronization stopped, inspect the migration error and transaction outcome, and use the application's migration recovery procedures or the verified backup before retrying. Never expose a partly populated index or repair data by editing Mail tables. A migration rollback removes only the derived participant structure; original sender/recipient JSON remains available for a later reapply/backfill. Rehearse apply/rollback/reapply and failure recovery on a database copy before the production window.
24
+
11
25
  ## Gmail quota, HTTP batch, and retry behavior
12
26
 
13
27
  For Gmail, one list page is fetched with `messages.list`, followed by one `messages.get` per message. Mail groups up to 10 message gets into each HTTP `multipart/mixed` batch request. Batching reduces HTTP connections only: Google charges every embedded call separately, so `messages.get` still costs 20 quota units and `messages.list` costs 5. A 100-message page therefore costs about 2,005 units before incidental profile, label, history, or fallback metadata calls; it does not become a single quota unit. See [Gmail quota configuration](configuration-and-accounts.md#gmail-quota-configuration) for defaults and project overrides.