zele 0.3.15 → 0.3.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.
- package/README.md +113 -33
- package/dist/api-utils.d.ts +7 -0
- package/dist/api-utils.js +12 -0
- package/dist/api-utils.js.map +1 -1
- package/dist/auth.d.ts +71 -9
- package/dist/auth.js +187 -11
- package/dist/auth.js.map +1 -1
- package/dist/calendar-time.js +6 -0
- package/dist/calendar-time.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/attachment.js +2 -0
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.js +104 -6
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.js +1 -1
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.js +9 -3
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/filter.d.ts +2 -0
- package/dist/commands/filter.js +64 -0
- package/dist/commands/filter.js.map +1 -0
- package/dist/commands/label.js +19 -9
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail-actions.js +12 -2
- package/dist/commands/mail-actions.js.map +1 -1
- package/dist/commands/mail.js +194 -84
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/profile.js +25 -18
- package/dist/commands/profile.js.map +1 -1
- package/dist/db.js +24 -0
- package/dist/db.js.map +1 -1
- package/dist/generated/internal/class.js +2 -2
- package/dist/generated/internal/class.js.map +1 -1
- package/dist/generated/internal/prismaNamespace.d.ts +2 -0
- package/dist/generated/internal/prismaNamespace.js +2 -0
- package/dist/generated/internal/prismaNamespace.js.map +1 -1
- package/dist/generated/internal/prismaNamespaceBrowser.d.ts +2 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +2 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/generated/models/Account.d.ts +97 -1
- package/dist/gmail-client.d.ts +42 -0
- package/dist/gmail-client.js +175 -9
- package/dist/gmail-client.js.map +1 -1
- package/dist/imap-smtp-client.d.ts +235 -0
- package/dist/imap-smtp-client.js +1225 -0
- package/dist/imap-smtp-client.js.map +1 -0
- package/dist/mail-tui.js +3 -2
- package/dist/mail-tui.js.map +1 -1
- package/dist/output.d.ts +2 -0
- package/dist/output.js +4 -0
- package/dist/output.js.map +1 -1
- package/package.json +9 -5
- package/schema.prisma +7 -5
- package/skills/zele/SKILL.md +141 -0
- package/src/api-utils.ts +13 -0
- package/src/auth.ts +283 -15
- package/src/calendar-time.test.ts +35 -0
- package/src/calendar-time.ts +5 -0
- package/src/cli.ts +6 -1
- package/src/commands/attachment.ts +1 -0
- package/src/commands/auth-cmd.ts +112 -6
- package/src/commands/calendar.ts +1 -1
- package/src/commands/draft.ts +7 -3
- package/src/commands/filter.ts +74 -0
- package/src/commands/label.ts +22 -11
- package/src/commands/mail-actions.ts +16 -3
- package/src/commands/mail.ts +207 -89
- package/src/commands/profile.ts +27 -17
- package/src/db.ts +28 -0
- package/src/generated/internal/class.ts +2 -2
- package/src/generated/internal/prismaNamespace.ts +2 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +2 -0
- package/src/generated/models/Account.ts +97 -1
- package/src/gmail-client.test.ts +155 -2
- package/src/gmail-client.ts +221 -16
- package/src/imap-smtp-client.ts +1381 -0
- package/src/mail-tui.tsx +3 -3
- package/src/output.ts +8 -1
- package/src/schema.sql +2 -0
package/dist/gmail-client.d.ts
CHANGED
|
@@ -2,6 +2,16 @@ import { type gmail_v1 } from '@googleapis/gmail';
|
|
|
2
2
|
import type { OAuth2Client } from 'google-auth-library';
|
|
3
3
|
import { AuthError, ApiError, NotFoundError, EmptyThreadError, MissingDataError } from './api-utils.js';
|
|
4
4
|
import type { AccountId } from './auth.js';
|
|
5
|
+
export type AuthVerdict = 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror' | 'bestguesspass';
|
|
6
|
+
export interface AuthResult {
|
|
7
|
+
spf: AuthVerdict;
|
|
8
|
+
dkim: AuthVerdict;
|
|
9
|
+
dmarc: AuthVerdict;
|
|
10
|
+
/** true when all three are 'pass' — the email is fully authenticated */
|
|
11
|
+
authentic: boolean;
|
|
12
|
+
/** raw Authentication-Results header value */
|
|
13
|
+
raw: string;
|
|
14
|
+
}
|
|
5
15
|
export interface Sender {
|
|
6
16
|
name?: string;
|
|
7
17
|
email: string;
|
|
@@ -29,6 +39,8 @@ export interface ParsedMessage {
|
|
|
29
39
|
mimeType: string;
|
|
30
40
|
textBody: string | null;
|
|
31
41
|
attachments: AttachmentMeta[];
|
|
42
|
+
/** SPF/DKIM/DMARC authentication results from Gmail. null for sent/draft messages. */
|
|
43
|
+
auth: AuthResult | null;
|
|
32
44
|
}
|
|
33
45
|
export interface AttachmentMeta {
|
|
34
46
|
attachmentId: string;
|
|
@@ -54,10 +66,16 @@ export interface ThreadListItem {
|
|
|
54
66
|
snippet: string;
|
|
55
67
|
subject: string;
|
|
56
68
|
from: Sender;
|
|
69
|
+
to: Sender[];
|
|
70
|
+
cc: Sender[];
|
|
57
71
|
date: string;
|
|
58
72
|
labelIds: string[];
|
|
59
73
|
unread: boolean;
|
|
74
|
+
starred: boolean;
|
|
60
75
|
messageCount: number;
|
|
76
|
+
inReplyTo: string | null;
|
|
77
|
+
hasAttachments: boolean;
|
|
78
|
+
listUnsubscribe: string | null;
|
|
61
79
|
}
|
|
62
80
|
export interface ThreadListResult {
|
|
63
81
|
threads: ThreadListItem[];
|
|
@@ -244,6 +262,12 @@ export declare class GmailClient {
|
|
|
244
262
|
archive({ threadIds }: {
|
|
245
263
|
threadIds: string[];
|
|
246
264
|
}): Promise<void | AuthError | ApiError>;
|
|
265
|
+
markAsSpam({ threadIds }: {
|
|
266
|
+
threadIds: string[];
|
|
267
|
+
}): Promise<void | AuthError | ApiError>;
|
|
268
|
+
unmarkSpam({ threadIds }: {
|
|
269
|
+
threadIds: string[];
|
|
270
|
+
}): Promise<void | AuthError | ApiError>;
|
|
247
271
|
/** Invalidate thread cache after a thread mutation. */
|
|
248
272
|
private invalidateAfterThreadMutation;
|
|
249
273
|
/** Moves all spam threads to trash. Does not permanently delete. */
|
|
@@ -282,6 +306,18 @@ export declare class GmailClient {
|
|
|
282
306
|
deleteLabel({ labelId }: {
|
|
283
307
|
labelId: string;
|
|
284
308
|
}): Promise<void>;
|
|
309
|
+
listFilters(): Promise<{
|
|
310
|
+
parsed: gmail_v1.Schema$Filter[];
|
|
311
|
+
} | AuthError | ApiError>;
|
|
312
|
+
createFilter(opts: {
|
|
313
|
+
from?: string;
|
|
314
|
+
query?: string;
|
|
315
|
+
addLabelIds?: string[];
|
|
316
|
+
removeLabelIds?: string[];
|
|
317
|
+
}): Promise<gmail_v1.Schema$Filter | AuthError | ApiError>;
|
|
318
|
+
deleteFilter(filterId: string): Promise<void | AuthError | ApiError>;
|
|
319
|
+
/** Resolve a label name to its ID, auto-creating if missing. Public wrapper for filter commands. */
|
|
320
|
+
resolveLabel(nameOrId: string): Promise<string | AuthError | ApiError>;
|
|
285
321
|
getLabelCounts(): Promise<{
|
|
286
322
|
parsed: Array<{
|
|
287
323
|
label: string;
|
|
@@ -327,6 +363,10 @@ export declare class GmailClient {
|
|
|
327
363
|
private extractBody;
|
|
328
364
|
private findBodyPart;
|
|
329
365
|
private extractAttachmentMeta;
|
|
366
|
+
/** Quick check: does the message payload tree contain any non-inline attachments?
|
|
367
|
+
* Checks the root part itself (some messages have attachment metadata there)
|
|
368
|
+
* then recurses into child parts. */
|
|
369
|
+
private hasNonInlineAttachments;
|
|
330
370
|
getEmailAliases(): Promise<Array<{
|
|
331
371
|
email: string;
|
|
332
372
|
name?: string;
|
|
@@ -366,3 +406,5 @@ export declare class GmailClient {
|
|
|
366
406
|
private getHeaderAll;
|
|
367
407
|
private getHeaderValues;
|
|
368
408
|
}
|
|
409
|
+
/** Parse a Gmail Authentication-Results header into structured verdicts. */
|
|
410
|
+
export declare function parseAuthResults(header: string): AuthResult;
|
package/dist/gmail-client.js
CHANGED
|
@@ -528,7 +528,9 @@ export class GmailClient {
|
|
|
528
528
|
return messageIds;
|
|
529
529
|
if (messageIds.length === 0)
|
|
530
530
|
return;
|
|
531
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['UNREAD'] });
|
|
531
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['UNREAD'] });
|
|
532
|
+
if (mod instanceof Error)
|
|
533
|
+
return mod;
|
|
532
534
|
await this.invalidateAfterThreadMutation(threadIds);
|
|
533
535
|
}
|
|
534
536
|
async markAsUnread({ threadIds }) {
|
|
@@ -537,7 +539,9 @@ export class GmailClient {
|
|
|
537
539
|
return messageIds;
|
|
538
540
|
if (messageIds.length === 0)
|
|
539
541
|
return;
|
|
540
|
-
await this.batchModifyMessages(messageIds, { addLabelIds: ['UNREAD'] });
|
|
542
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['UNREAD'] });
|
|
543
|
+
if (mod instanceof Error)
|
|
544
|
+
return mod;
|
|
541
545
|
await this.invalidateAfterThreadMutation(threadIds);
|
|
542
546
|
}
|
|
543
547
|
async star({ threadIds }) {
|
|
@@ -546,7 +550,9 @@ export class GmailClient {
|
|
|
546
550
|
return messageIds;
|
|
547
551
|
if (messageIds.length === 0)
|
|
548
552
|
return;
|
|
549
|
-
await this.batchModifyMessages(messageIds, { addLabelIds: ['STARRED'] });
|
|
553
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['STARRED'] });
|
|
554
|
+
if (mod instanceof Error)
|
|
555
|
+
return mod;
|
|
550
556
|
await this.invalidateAfterThreadMutation(threadIds);
|
|
551
557
|
}
|
|
552
558
|
async unstar({ threadIds }) {
|
|
@@ -555,7 +561,9 @@ export class GmailClient {
|
|
|
555
561
|
return messageIds;
|
|
556
562
|
if (messageIds.length === 0)
|
|
557
563
|
return;
|
|
558
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['STARRED'] });
|
|
564
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['STARRED'] });
|
|
565
|
+
if (mod instanceof Error)
|
|
566
|
+
return mod;
|
|
559
567
|
await this.invalidateAfterThreadMutation(threadIds);
|
|
560
568
|
}
|
|
561
569
|
async modifyLabels({ threadIds, addLabelIds = [], removeLabelIds = [], }) {
|
|
@@ -574,10 +582,12 @@ export class GmailClient {
|
|
|
574
582
|
return messageIds;
|
|
575
583
|
if (messageIds.length === 0)
|
|
576
584
|
return;
|
|
577
|
-
await this.batchModifyMessages(messageIds, {
|
|
585
|
+
const mod = await this.batchModifyMessages(messageIds, {
|
|
578
586
|
addLabelIds: resolvedAdd.filter((r) => typeof r === 'string'),
|
|
579
587
|
removeLabelIds: resolvedRemove,
|
|
580
588
|
});
|
|
589
|
+
if (mod instanceof Error)
|
|
590
|
+
return mod;
|
|
581
591
|
await this.invalidateAfterThreadMutation(threadIds);
|
|
582
592
|
}
|
|
583
593
|
async trash({ threadId }) {
|
|
@@ -600,7 +610,31 @@ export class GmailClient {
|
|
|
600
610
|
return messageIds;
|
|
601
611
|
if (messageIds.length === 0)
|
|
602
612
|
return;
|
|
603
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['INBOX'] });
|
|
613
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['INBOX'] });
|
|
614
|
+
if (mod instanceof Error)
|
|
615
|
+
return mod;
|
|
616
|
+
await this.invalidateAfterThreadMutation(threadIds);
|
|
617
|
+
}
|
|
618
|
+
async markAsSpam({ threadIds }) {
|
|
619
|
+
const messageIds = await this.getMessageIdsForThreads(threadIds);
|
|
620
|
+
if (messageIds instanceof Error)
|
|
621
|
+
return messageIds;
|
|
622
|
+
if (messageIds.length === 0)
|
|
623
|
+
return;
|
|
624
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] });
|
|
625
|
+
if (mod instanceof Error)
|
|
626
|
+
return mod;
|
|
627
|
+
await this.invalidateAfterThreadMutation(threadIds);
|
|
628
|
+
}
|
|
629
|
+
async unmarkSpam({ threadIds }) {
|
|
630
|
+
const messageIds = await this.getMessageIdsForThreads(threadIds, (labelIds) => labelIds.includes('SPAM'));
|
|
631
|
+
if (messageIds instanceof Error)
|
|
632
|
+
return messageIds;
|
|
633
|
+
if (messageIds.length === 0)
|
|
634
|
+
return;
|
|
635
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['SPAM'], addLabelIds: ['INBOX'] });
|
|
636
|
+
if (mod instanceof Error)
|
|
637
|
+
return mod;
|
|
604
638
|
await this.invalidateAfterThreadMutation(threadIds);
|
|
605
639
|
}
|
|
606
640
|
/** Invalidate thread cache after a thread mutation. */
|
|
@@ -625,10 +659,12 @@ export class GmailClient {
|
|
|
625
659
|
const messageIds = await this.getMessageIdsForThreads(threadIds);
|
|
626
660
|
if (messageIds instanceof Error)
|
|
627
661
|
return messageIds;
|
|
628
|
-
await this.batchModifyMessages(messageIds, {
|
|
662
|
+
const mod = await this.batchModifyMessages(messageIds, {
|
|
629
663
|
addLabelIds: ['TRASH'],
|
|
630
664
|
removeLabelIds: ['SPAM', 'INBOX'],
|
|
631
665
|
});
|
|
666
|
+
if (mod instanceof Error)
|
|
667
|
+
return mod;
|
|
632
668
|
totalDeleted += threadIds.length;
|
|
633
669
|
pageToken = res.nextPageToken ?? undefined;
|
|
634
670
|
if (!pageToken)
|
|
@@ -699,6 +735,46 @@ export class GmailClient {
|
|
|
699
735
|
await this.invalidateLabels();
|
|
700
736
|
}
|
|
701
737
|
// =========================================================================
|
|
738
|
+
// Filters
|
|
739
|
+
// =========================================================================
|
|
740
|
+
async listFilters() {
|
|
741
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () => withRetry(() => this.gmail.users.settings.filters.list({ userId: 'me' })));
|
|
742
|
+
if (res instanceof Error)
|
|
743
|
+
return res;
|
|
744
|
+
return { parsed: res.data.filter ?? [] };
|
|
745
|
+
}
|
|
746
|
+
async createFilter(opts) {
|
|
747
|
+
const criteria = {};
|
|
748
|
+
if (opts.from)
|
|
749
|
+
criteria.from = opts.from;
|
|
750
|
+
if (opts.query)
|
|
751
|
+
criteria.query = opts.query;
|
|
752
|
+
const action = {};
|
|
753
|
+
if (opts.addLabelIds?.length)
|
|
754
|
+
action.addLabelIds = opts.addLabelIds;
|
|
755
|
+
if (opts.removeLabelIds?.length)
|
|
756
|
+
action.removeLabelIds = opts.removeLabelIds;
|
|
757
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () => withRetry(() => this.gmail.users.settings.filters.create({
|
|
758
|
+
userId: 'me',
|
|
759
|
+
requestBody: { criteria, action },
|
|
760
|
+
})));
|
|
761
|
+
if (res instanceof Error)
|
|
762
|
+
return res;
|
|
763
|
+
return res.data;
|
|
764
|
+
}
|
|
765
|
+
async deleteFilter(filterId) {
|
|
766
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () => withRetry(() => this.gmail.users.settings.filters.delete({
|
|
767
|
+
userId: 'me',
|
|
768
|
+
id: filterId,
|
|
769
|
+
})));
|
|
770
|
+
if (res instanceof Error)
|
|
771
|
+
return res;
|
|
772
|
+
}
|
|
773
|
+
/** Resolve a label name to its ID, auto-creating if missing. Public wrapper for filter commands. */
|
|
774
|
+
async resolveLabel(nameOrId) {
|
|
775
|
+
return this.resolveLabelId(nameOrId);
|
|
776
|
+
}
|
|
777
|
+
// =========================================================================
|
|
702
778
|
// Label counts (unread counts per folder/label)
|
|
703
779
|
// =========================================================================
|
|
704
780
|
async getLabelCounts() {
|
|
@@ -831,6 +907,18 @@ export class GmailClient {
|
|
|
831
907
|
.map((h) => h.value ?? '')
|
|
832
908
|
.filter((v) => v.length > 0);
|
|
833
909
|
const { body, mimeType, textBody } = this.extractBody(message.payload ?? {});
|
|
910
|
+
// Authentication-Results: multiple MTAs can add this header. Prefer the one
|
|
911
|
+
// stamped by Gmail's trusted authserv-id (mx.google.com) to avoid trusting
|
|
912
|
+
// headers injected by upstream/untrusted relays.
|
|
913
|
+
const authHeaders = headers
|
|
914
|
+
.filter((h) => h.name?.toLowerCase() === 'authentication-results')
|
|
915
|
+
.map((h) => h.value ?? '')
|
|
916
|
+
.filter((v) => v.length > 0);
|
|
917
|
+
const trustedAuthHeader = authHeaders.find((v) => v.trim().toLowerCase().startsWith('mx.google.com'))
|
|
918
|
+
?? authHeaders[0]
|
|
919
|
+
?? null;
|
|
920
|
+
const isSentOrDraft = labelIds.includes('SENT') || labelIds.includes('DRAFT');
|
|
921
|
+
const auth = trustedAuthHeader && !isSentOrDraft ? parseAuthResults(trustedAuthHeader) : null;
|
|
834
922
|
return {
|
|
835
923
|
id: message.id ?? '',
|
|
836
924
|
threadId: message.threadId ?? '',
|
|
@@ -856,6 +944,7 @@ export class GmailClient {
|
|
|
856
944
|
mimeType,
|
|
857
945
|
textBody,
|
|
858
946
|
attachments: this.extractAttachmentMeta(message.payload?.parts ?? []),
|
|
947
|
+
auth,
|
|
859
948
|
};
|
|
860
949
|
}
|
|
861
950
|
/** Parse raw gmail_v1.Schema$Thread (format: metadata) into ThreadListItem.
|
|
@@ -890,16 +979,39 @@ export class GmailClient {
|
|
|
890
979
|
}
|
|
891
980
|
}
|
|
892
981
|
}
|
|
982
|
+
// Parse recipients from latest message
|
|
983
|
+
const toHeader = getHeader('to') ?? '';
|
|
984
|
+
const ccHeaders = headers
|
|
985
|
+
.filter((h) => h.name?.toLowerCase() === 'cc')
|
|
986
|
+
.map((h) => h.value ?? '')
|
|
987
|
+
.filter((v) => v.length > 0);
|
|
988
|
+
// Check if any non-draft message in the thread is a reply (has In-Reply-To header)
|
|
989
|
+
const inReplyTo = nonDraftMessages
|
|
990
|
+
.map((m) => m.payload?.headers?.find((h) => h.name?.toLowerCase() === 'in-reply-to')?.value ??
|
|
991
|
+
null)
|
|
992
|
+
.find((v) => v !== null) ?? null;
|
|
993
|
+
// Check if any message has attachments (non-inline)
|
|
994
|
+
const hasAttachments = messages.some((m) => this.hasNonInlineAttachments(m.payload));
|
|
995
|
+
// List-Unsubscribe from latest message
|
|
996
|
+
const listUnsubscribe = getHeader('list-unsubscribe') ?? null;
|
|
893
997
|
return {
|
|
894
998
|
id: raw.id ?? '',
|
|
895
999
|
historyId: raw.historyId ?? null,
|
|
896
1000
|
snippet: sanitizeSnippet(latest?.snippet ?? ''),
|
|
897
1001
|
subject: (getHeader('subject') ?? '(no subject)').replace(/"/g, '').trim(),
|
|
898
1002
|
from: displayFrom,
|
|
1003
|
+
to: toHeader ? parseAddressList(toHeader) : [],
|
|
1004
|
+
cc: ccHeaders.length > 0
|
|
1005
|
+
? ccHeaders.filter((h) => h.trim().length > 0).flatMap((h) => parseAddressList(h))
|
|
1006
|
+
: [],
|
|
899
1007
|
date: getHeader('date') ?? '',
|
|
900
1008
|
labelIds: allLabels,
|
|
901
1009
|
unread: allLabels.includes('UNREAD'),
|
|
1010
|
+
starred: allLabels.includes('STARRED'),
|
|
902
1011
|
messageCount: nonDraftMessages.length,
|
|
1012
|
+
inReplyTo,
|
|
1013
|
+
hasAttachments,
|
|
1014
|
+
listUnsubscribe,
|
|
903
1015
|
};
|
|
904
1016
|
}
|
|
905
1017
|
/** Parse raw gmail_v1.Schema$Label[] from labels.list into our label objects. */
|
|
@@ -1001,6 +1113,25 @@ export class GmailClient {
|
|
|
1001
1113
|
}
|
|
1002
1114
|
return results;
|
|
1003
1115
|
}
|
|
1116
|
+
/** Quick check: does the message payload tree contain any non-inline attachments?
|
|
1117
|
+
* Checks the root part itself (some messages have attachment metadata there)
|
|
1118
|
+
* then recurses into child parts. */
|
|
1119
|
+
hasNonInlineAttachments(part) {
|
|
1120
|
+
if (!part)
|
|
1121
|
+
return false;
|
|
1122
|
+
if (part.filename && part.filename.length > 0 && part.body?.attachmentId) {
|
|
1123
|
+
const disposition = part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')?.value ?? '';
|
|
1124
|
+
const hasContentId = part.headers?.some((h) => h.name?.toLowerCase() === 'content-id');
|
|
1125
|
+
const isInline = disposition.toLowerCase().includes('inline');
|
|
1126
|
+
if (!isInline || !hasContentId)
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
for (const child of part.parts ?? []) {
|
|
1130
|
+
if (this.hasNonInlineAttachments(child))
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1004
1135
|
async getEmailAliases() {
|
|
1005
1136
|
const profile = await this.getProfile();
|
|
1006
1137
|
if (profile instanceof Error)
|
|
@@ -1345,13 +1476,15 @@ export class GmailClient {
|
|
|
1345
1476
|
const chunkSize = 1000;
|
|
1346
1477
|
for (let i = 0; i < messageIds.length; i += chunkSize) {
|
|
1347
1478
|
const chunk = messageIds.slice(i, i + chunkSize);
|
|
1348
|
-
await withRetry(() => this.gmail.users.messages.batchModify({
|
|
1479
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () => withRetry(() => this.gmail.users.messages.batchModify({
|
|
1349
1480
|
userId: 'me',
|
|
1350
1481
|
requestBody: {
|
|
1351
1482
|
ids: chunk,
|
|
1352
1483
|
...body,
|
|
1353
1484
|
},
|
|
1354
|
-
}));
|
|
1485
|
+
})));
|
|
1486
|
+
if (res instanceof Error)
|
|
1487
|
+
return res;
|
|
1355
1488
|
}
|
|
1356
1489
|
}
|
|
1357
1490
|
// =========================================================================
|
|
@@ -1375,6 +1508,39 @@ export class GmailClient {
|
|
|
1375
1508
|
}
|
|
1376
1509
|
}
|
|
1377
1510
|
// ---------------------------------------------------------------------------
|
|
1511
|
+
// Email authentication: parse the Authentication-Results header
|
|
1512
|
+
// ---------------------------------------------------------------------------
|
|
1513
|
+
// Gmail adds an Authentication-Results header to every received message with
|
|
1514
|
+
// SPF, DKIM, and DMARC verdicts. Format is semi-structured:
|
|
1515
|
+
// Authentication-Results: mx.google.com;
|
|
1516
|
+
// dkim=pass header.i=@example.com header.s=sel1;
|
|
1517
|
+
// spf=pass (...) smtp.mailfrom=user@example.com;
|
|
1518
|
+
// dmarc=pass (p=REJECT) header.from=example.com
|
|
1519
|
+
// We extract the verdict keyword after each protocol name.
|
|
1520
|
+
// ---------------------------------------------------------------------------
|
|
1521
|
+
const AUTH_VERDICTS = new Set(['pass', 'fail', 'softfail', 'neutral', 'none', 'temperror', 'permerror', 'bestguesspass']);
|
|
1522
|
+
/** Parse a Gmail Authentication-Results header into structured verdicts. */
|
|
1523
|
+
export function parseAuthResults(header) {
|
|
1524
|
+
const lower = header.toLowerCase();
|
|
1525
|
+
const extract = (protocol) => {
|
|
1526
|
+
// Match "protocol=verdict" — verdict is a word that stops at whitespace, semicolons, or parens
|
|
1527
|
+
const re = new RegExp(`\\b${protocol}\\s*=\\s*([a-z]+)`);
|
|
1528
|
+
const match = re.exec(lower);
|
|
1529
|
+
const verdict = match?.[1] ?? 'none';
|
|
1530
|
+
return AUTH_VERDICTS.has(verdict) ? verdict : 'none';
|
|
1531
|
+
};
|
|
1532
|
+
const spf = extract('spf');
|
|
1533
|
+
const dkim = extract('dkim');
|
|
1534
|
+
const dmarc = extract('dmarc');
|
|
1535
|
+
return {
|
|
1536
|
+
spf,
|
|
1537
|
+
dkim,
|
|
1538
|
+
dmarc,
|
|
1539
|
+
authentic: spf === 'pass' && dkim === 'pass' && dmarc === 'pass',
|
|
1540
|
+
raw: header,
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
// ---------------------------------------------------------------------------
|
|
1378
1544
|
// Watch: folder label mapping
|
|
1379
1545
|
// ---------------------------------------------------------------------------
|
|
1380
1546
|
const WATCH_FOLDER_LABELS = {
|