dsh-email 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/types.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface AddressEntry {
5
5
  }
6
6
  /** One listed message: everything cheap to fetch without the body. */
7
7
  export interface ListedMessage {
8
- uid: number;
8
+ uid: number | string;
9
9
  /** ISO 8601 string, or '' when unknown. */
10
10
  date: string;
11
11
  from: AddressEntry[];
@@ -36,6 +36,9 @@ export interface ReadMessageBody {
36
36
  truncated: boolean;
37
37
  }
38
38
  export interface EmailListResult {
39
+ countExact?: boolean;
40
+ hasMore?: boolean;
41
+ order?: 'relevance';
39
42
  account: string;
40
43
  count: number;
41
44
  folder: string;
@@ -45,22 +48,48 @@ export interface EmailListResult {
45
48
  }
46
49
  export interface EmailReadResult extends ReadMessageBody {
47
50
  account: string;
48
- uid: number;
51
+ uid: number | string;
49
52
  folder: string;
53
+ uidValidity?: number;
54
+ }
55
+ export interface EmailSearchOptions {
56
+ /** Local header window; independent of result offset and bodySearchLimit. */
57
+ headerScanLimit?: number;
58
+ headerScanOffset?: number;
59
+ }
60
+ export interface EmailHeaderScan {
61
+ mailboxTotal: number;
62
+ scanned: number;
63
+ matched: number;
64
+ scanOffset: number;
65
+ scanLimit: number;
66
+ complete: boolean;
67
+ nextScanOffset?: number;
50
68
  }
51
69
  export interface EmailSearchResult {
70
+ countExact?: boolean;
71
+ hasMore?: boolean;
72
+ order?: 'relevance';
52
73
  account: string;
53
74
  query: string;
54
- /** Server path: the server's match count. Fallback scan: this page's row count (see countKind). */
75
+ /** Server candidates, local header-window matches, or body-fallback page rows; inspect searchSource and coverage. */
55
76
  count: number;
56
77
  folder: string;
57
78
  /** How many newest matches the caller skipped (0 on the first page). */
58
79
  offset: number;
59
80
  messages: ListedMessage[];
60
- /** Set to 'scanned' when the local body scan produced this page, so count is not a total. */
81
+ /** Local scans cannot claim a full mailbox/body match total. */
61
82
  countKind?: 'scanned';
62
- /** How many newest messages the fallback scan looked at (set with countKind='scanned'). */
83
+ /** Header rows actually scanned, or the configured body window for legacy renderers. */
63
84
  scannedLimit?: number;
85
+ searchSource?: 'server' | 'local-headers' | 'local-body' | 'api';
86
+ serverMatchCount?: number;
87
+ serverSearchFailed?: boolean;
88
+ headerScan?: EmailHeaderScan;
89
+ bodyScan?: {
90
+ scanned: number;
91
+ scanLimit: number;
92
+ };
64
93
  }
65
94
  export interface EmailSendResult {
66
95
  account: string;
@@ -81,7 +110,7 @@ export interface EmailFoldersResult {
81
110
  }
82
111
  export interface EmailAttachmentResult {
83
112
  account: string;
84
- uid: number;
113
+ uid: number | string;
85
114
  filename: string;
86
115
  contentType: string;
87
116
  size: number;
@@ -101,7 +130,7 @@ export interface EmailListArgs extends AccountArg {
101
130
  until?: string;
102
131
  }
103
132
  export interface EmailReadArgs extends AccountArg {
104
- uid: number;
133
+ uid: number | string;
105
134
  folder?: string;
106
135
  }
107
136
  export interface EmailSearchArgs extends AccountArg {
@@ -111,6 +140,8 @@ export interface EmailSearchArgs extends AccountArg {
111
140
  offset?: number;
112
141
  since?: string;
113
142
  until?: string;
143
+ headerScanLimit?: number;
144
+ headerScanOffset?: number;
114
145
  }
115
146
  export interface EmailSendArgs extends AccountArg {
116
147
  to: string;
@@ -124,7 +155,7 @@ export interface EmailFoldersArgs extends AccountArg {
124
155
  subscribedOnly?: boolean;
125
156
  }
126
157
  export interface EmailAttachmentArgs extends AccountArg {
127
- uid: number;
158
+ uid: number | string;
128
159
  /** 0-based index into the attachments of email_read. Default 0. */
129
160
  index?: number;
130
161
  folder?: string;
@@ -136,7 +167,7 @@ export interface EmailWatchArgs extends AccountArg {
136
167
  }
137
168
  export type EmailMarkAction = 'read' | 'unread' | 'star' | 'unstar' | 'move';
138
169
  export interface EmailMarkArgs extends AccountArg {
139
- uid: number;
170
+ uid: number | string;
140
171
  action: EmailMarkAction;
141
172
  /** Target folder path; only used (and required) when action is 'move'. */
142
173
  toFolder?: string;
@@ -144,7 +175,7 @@ export interface EmailMarkArgs extends AccountArg {
144
175
  }
145
176
  export interface EmailMarkResult {
146
177
  account: string;
147
- uid: number;
178
+ uid: number | string;
148
179
  folder: string;
149
180
  action: EmailMarkAction;
150
181
  seen: boolean;
@@ -156,7 +187,7 @@ export interface EmailMarkResult {
156
187
  }
157
188
  export type EmailReplyMode = 'reply' | 'reply-all' | 'forward';
158
189
  export interface EmailReplyArgs extends AccountArg {
159
- uid: number;
190
+ uid: number | string;
160
191
  /** The new text to write; the original is quoted below it automatically. */
161
192
  text: string;
162
193
  mode?: EmailReplyMode;
@@ -169,7 +200,7 @@ export interface EmailReplyResult {
169
200
  account: string;
170
201
  mode: EmailReplyMode;
171
202
  /** uid of the original message that was answered/forwarded. */
172
- originalUid: number;
203
+ originalUid: number | string;
173
204
  messageId: string;
174
205
  accepted: string[];
175
206
  rejected: string[];
@@ -178,6 +209,7 @@ export interface EmailReplyResult {
178
209
  subject: string;
179
210
  }
180
211
  export interface EmailWatchResult {
212
+ totalUnreadExact?: boolean;
181
213
  account: string;
182
214
  folder: string;
183
215
  /** True on the first call for this account+folder: it sets the baseline. */
package/lib/web.d.ts CHANGED
@@ -175,7 +175,8 @@ export declare class EmailSettingsBackend {
175
175
  private readonly ctx;
176
176
  private readonly scope;
177
177
  private readonly rowConfig;
178
- constructor(ctx: any, scope: any, rowConfig: EmailConfig);
178
+ private readonly apiFetch?;
179
+ constructor(ctx: any, scope: any, rowConfig: EmailConfig, apiFetch?: typeof fetch | undefined);
179
180
  /** Wired by apply(): the email_watch core; 'web' keeps its own cursor scope. */
180
181
  watchImpl?: (account: string, folder: string, limit: number, scope: string) => Promise<EmailWatchResult>;
181
182
  private get namespace();
@@ -262,11 +263,18 @@ export declare class EmailSettingsBackend {
262
263
  * what was actually tried — including on failure.
263
264
  */
264
265
  test(value: EmailSettingsValue, accountName?: string): Promise<{
266
+ ok: boolean;
267
+ account: string;
268
+ apiHost: "https://api.agentmail.to" | "https://api.agentmail.eu" | undefined;
269
+ message: string;
270
+ } | {
265
271
  account: string;
266
272
  imapHost: string;
267
273
  imapPort: number;
268
274
  ok: boolean;
269
275
  ms: number;
276
+ apiHost?: undefined;
277
+ message?: undefined;
270
278
  }>;
271
279
  /**
272
280
  * Resolve one named account of the *stored* settings — the same accounts the
package/lib/web.js CHANGED
@@ -6,6 +6,7 @@ import { SETTINGS_NAMESPACE, toEmailConfig, validateSettingsValue } from './sett
6
6
  import { isOAuth2Account, OUTLOOK_OAUTH2_CLIENT_ID, parseAccountsYaml, parseServerPresets, presetNamesIn, PROVIDER_PRESETS, resolveEmailSettings, serializeAccountsYaml, } from './config.js';
7
7
  import { clearTokenFor, getFreshAccessToken, oauth2StateOf, pollDeviceFlow, startDeviceFlow, } from './oauth2.js';
8
8
  import { EmailPool, messageOf, redactCredentials } from './mail-client.js';
9
+ import { AgentMailClient } from './agentmail.js';
9
10
  /** Same-origin route the browser settings section talks to. */
10
11
  export const SETTINGS_ROUTE = '/_dsh/dsh-email/settings';
11
12
  /** Same-origin route serving the whale-girl courier image to the widget. */
@@ -98,7 +99,7 @@ function buildAccountCards(raw, defaultAccount, presets, tokens = () => ({ state
98
99
  provider: providerName,
99
100
  ...(preset?.label !== undefined && preset.label !== '' ? { providerLabel: preset.label } : {}),
100
101
  user,
101
- hasPassword: typeof account.password === 'string' && account.password !== '',
102
+ hasPassword: typeof account.password === 'string' && account.password !== '' || typeof account.apiKey === 'string' && account.apiKey !== '',
102
103
  authKind,
103
104
  ...(pinned === 'oauth2' || pinned === 'password' ? { authKindDeclared: pinned } : {}),
104
105
  ...(clientId !== '' ? { clientId } : {}),
@@ -308,6 +309,8 @@ function washEndpoint(account, key) {
308
309
  function persistedProvider(provider, customNames) {
309
310
  if (typeof provider !== 'string' || provider === '')
310
311
  return undefined;
312
+ if (provider === 'agentmail' || provider === 'agentmail-eu')
313
+ return provider;
311
314
  if (Object.prototype.hasOwnProperty.call(PROVIDER_PRESETS, provider))
312
315
  return provider;
313
316
  return customNames.has(provider) ? provider : undefined;
@@ -520,6 +523,15 @@ function serializeAccountsDraft(source, cards, defaultAccount, customNames) {
520
523
  // never written — a stored copy is washed out below instead.
521
524
  const previousProvider = account.get('provider');
522
525
  const nextProvider = persistedProvider(card.provider, customNames);
526
+ const apiProvider = (value) => value === 'agentmail' || value === 'agentmail-eu';
527
+ if (apiProvider(previousProvider) !== apiProvider(nextProvider)) {
528
+ account.delete('password');
529
+ account.delete('apiKey');
530
+ account.delete('authPassword');
531
+ account.delete('authKind');
532
+ }
533
+ if (card.password !== undefined && apiProvider(nextProvider))
534
+ account.delete('apiKey');
523
535
  writeField(account, 'provider', nextProvider);
524
536
  writeField(account, 'user', card.user);
525
537
  // senderName / authUser are plain three-state fields (undefined = 保持原样,
@@ -749,10 +761,12 @@ export class EmailSettingsBackend {
749
761
  ctx;
750
762
  scope;
751
763
  rowConfig;
752
- constructor(ctx, scope, rowConfig) {
764
+ apiFetch;
765
+ constructor(ctx, scope, rowConfig, apiFetch) {
753
766
  this.ctx = ctx;
754
767
  this.scope = scope;
755
768
  this.rowConfig = rowConfig;
769
+ this.apiFetch = apiFetch;
756
770
  }
757
771
  /** Wired by apply(): the email_watch core; 'web' keeps its own cursor scope. */
758
772
  watchImpl;
@@ -886,6 +900,10 @@ export class EmailSettingsBackend {
886
900
  if (cfg === undefined) {
887
901
  throw new Error(`未知账号 "${name}",可用:${available.join('、')}`);
888
902
  }
903
+ if (cfg.transport === 'agentmail') {
904
+ await new AgentMailClient(settings, this.apiFetch).test(name);
905
+ return { ok: true, account: name, apiHost: cfg.apiBase, message: 'AgentMail API 连接成功 / AgentMail API connection successful' };
906
+ }
889
907
  const target = { account: name, imapHost: cfg.imap.host, imapPort: cfg.imap.port };
890
908
  const targetDescription = '账号 "' + name + '" · IMAP ' + cfg.imap.host + ':' + cfg.imap.port;
891
909
  // An OAuth2 account has no password to check: without a token there is
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.17.2",
4
- "description": "在 DSH 中收发、搜索和整理邮件,支持多个邮箱账号。",
3
+ "version": "0.18.0",
4
+ "description": "在 DSH 中浏览、收发、搜索和整理邮件,支持 IMAP/SMTP 与 AgentMail 多账号。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -34,7 +34,8 @@
34
34
  "dsh",
35
35
  "email",
36
36
  "imap",
37
- "smtp"
37
+ "smtp",
38
+ "agentmail"
38
39
  ],
39
40
  "author": "stardustlc",
40
41
  "repository": {