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/src/gmail-client.ts
CHANGED
|
@@ -22,6 +22,18 @@ import type { AccountId } from './auth.js'
|
|
|
22
22
|
// Types
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
|
|
25
|
+
export type AuthVerdict = 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror' | 'bestguesspass'
|
|
26
|
+
|
|
27
|
+
export interface AuthResult {
|
|
28
|
+
spf: AuthVerdict
|
|
29
|
+
dkim: AuthVerdict
|
|
30
|
+
dmarc: AuthVerdict
|
|
31
|
+
/** true when all three are 'pass' — the email is fully authenticated */
|
|
32
|
+
authentic: boolean
|
|
33
|
+
/** raw Authentication-Results header value */
|
|
34
|
+
raw: string
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
export interface Sender {
|
|
26
38
|
name?: string
|
|
27
39
|
email: string
|
|
@@ -50,6 +62,8 @@ export interface ParsedMessage {
|
|
|
50
62
|
mimeType: string // 'text/plain' or 'text/html'
|
|
51
63
|
textBody: string | null // decoded text/plain body when available (for reply parsing)
|
|
52
64
|
attachments: AttachmentMeta[]
|
|
65
|
+
/** SPF/DKIM/DMARC authentication results from Gmail. null for sent/draft messages. */
|
|
66
|
+
auth: AuthResult | null
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export interface AttachmentMeta {
|
|
@@ -78,10 +92,16 @@ export interface ThreadListItem {
|
|
|
78
92
|
snippet: string
|
|
79
93
|
subject: string
|
|
80
94
|
from: Sender
|
|
95
|
+
to: Sender[]
|
|
96
|
+
cc: Sender[]
|
|
81
97
|
date: string
|
|
82
98
|
labelIds: string[]
|
|
83
99
|
unread: boolean
|
|
100
|
+
starred: boolean
|
|
84
101
|
messageCount: number
|
|
102
|
+
inReplyTo: string | null
|
|
103
|
+
hasAttachments: boolean
|
|
104
|
+
listUnsubscribe: string | null
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
export interface ThreadListResult {
|
|
@@ -806,7 +826,8 @@ export class GmailClient {
|
|
|
806
826
|
)
|
|
807
827
|
if (messageIds instanceof Error) return messageIds
|
|
808
828
|
if (messageIds.length === 0) return
|
|
809
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['UNREAD'] })
|
|
829
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['UNREAD'] })
|
|
830
|
+
if (mod instanceof Error) return mod
|
|
810
831
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
811
832
|
}
|
|
812
833
|
|
|
@@ -816,7 +837,8 @@ export class GmailClient {
|
|
|
816
837
|
)
|
|
817
838
|
if (messageIds instanceof Error) return messageIds
|
|
818
839
|
if (messageIds.length === 0) return
|
|
819
|
-
await this.batchModifyMessages(messageIds, { addLabelIds: ['UNREAD'] })
|
|
840
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['UNREAD'] })
|
|
841
|
+
if (mod instanceof Error) return mod
|
|
820
842
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
821
843
|
}
|
|
822
844
|
|
|
@@ -824,7 +846,8 @@ export class GmailClient {
|
|
|
824
846
|
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
825
847
|
if (messageIds instanceof Error) return messageIds
|
|
826
848
|
if (messageIds.length === 0) return
|
|
827
|
-
await this.batchModifyMessages(messageIds, { addLabelIds: ['STARRED'] })
|
|
849
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['STARRED'] })
|
|
850
|
+
if (mod instanceof Error) return mod
|
|
828
851
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
829
852
|
}
|
|
830
853
|
|
|
@@ -834,7 +857,8 @@ export class GmailClient {
|
|
|
834
857
|
)
|
|
835
858
|
if (messageIds instanceof Error) return messageIds
|
|
836
859
|
if (messageIds.length === 0) return
|
|
837
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['STARRED'] })
|
|
860
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['STARRED'] })
|
|
861
|
+
if (mod instanceof Error) return mod
|
|
838
862
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
839
863
|
}
|
|
840
864
|
|
|
@@ -861,10 +885,11 @@ export class GmailClient {
|
|
|
861
885
|
if (messageIds instanceof Error) return messageIds
|
|
862
886
|
if (messageIds.length === 0) return
|
|
863
887
|
|
|
864
|
-
await this.batchModifyMessages(messageIds, {
|
|
888
|
+
const mod = await this.batchModifyMessages(messageIds, {
|
|
865
889
|
addLabelIds: resolvedAdd.filter((r): r is string => typeof r === 'string'),
|
|
866
890
|
removeLabelIds: resolvedRemove,
|
|
867
891
|
})
|
|
892
|
+
if (mod instanceof Error) return mod
|
|
868
893
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
869
894
|
}
|
|
870
895
|
|
|
@@ -892,7 +917,28 @@ export class GmailClient {
|
|
|
892
917
|
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
893
918
|
if (messageIds instanceof Error) return messageIds
|
|
894
919
|
if (messageIds.length === 0) return
|
|
895
|
-
await this.batchModifyMessages(messageIds, { removeLabelIds: ['INBOX'] })
|
|
920
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['INBOX'] })
|
|
921
|
+
if (mod instanceof Error) return mod
|
|
922
|
+
await this.invalidateAfterThreadMutation(threadIds)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async markAsSpam({ threadIds }: { threadIds: string[] }): Promise<void | AuthError | ApiError> {
|
|
926
|
+
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
927
|
+
if (messageIds instanceof Error) return messageIds
|
|
928
|
+
if (messageIds.length === 0) return
|
|
929
|
+
const mod = await this.batchModifyMessages(messageIds, { addLabelIds: ['SPAM'], removeLabelIds: ['INBOX'] })
|
|
930
|
+
if (mod instanceof Error) return mod
|
|
931
|
+
await this.invalidateAfterThreadMutation(threadIds)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async unmarkSpam({ threadIds }: { threadIds: string[] }): Promise<void | AuthError | ApiError> {
|
|
935
|
+
const messageIds = await this.getMessageIdsForThreads(threadIds, (labelIds) =>
|
|
936
|
+
labelIds.includes('SPAM'),
|
|
937
|
+
)
|
|
938
|
+
if (messageIds instanceof Error) return messageIds
|
|
939
|
+
if (messageIds.length === 0) return
|
|
940
|
+
const mod = await this.batchModifyMessages(messageIds, { removeLabelIds: ['SPAM'], addLabelIds: ['INBOX'] })
|
|
941
|
+
if (mod instanceof Error) return mod
|
|
896
942
|
await this.invalidateAfterThreadMutation(threadIds)
|
|
897
943
|
}
|
|
898
944
|
|
|
@@ -919,10 +965,11 @@ export class GmailClient {
|
|
|
919
965
|
const threadIds = res.threads.map((t) => t.id)
|
|
920
966
|
const messageIds = await this.getMessageIdsForThreads(threadIds)
|
|
921
967
|
if (messageIds instanceof Error) return messageIds
|
|
922
|
-
await this.batchModifyMessages(messageIds, {
|
|
968
|
+
const mod = await this.batchModifyMessages(messageIds, {
|
|
923
969
|
addLabelIds: ['TRASH'],
|
|
924
970
|
removeLabelIds: ['SPAM', 'INBOX'],
|
|
925
971
|
})
|
|
972
|
+
if (mod instanceof Error) return mod
|
|
926
973
|
|
|
927
974
|
totalDeleted += threadIds.length
|
|
928
975
|
pageToken = res.nextPageToken ?? undefined
|
|
@@ -1021,6 +1068,61 @@ export class GmailClient {
|
|
|
1021
1068
|
await this.invalidateLabels()
|
|
1022
1069
|
}
|
|
1023
1070
|
|
|
1071
|
+
// =========================================================================
|
|
1072
|
+
// Filters
|
|
1073
|
+
// =========================================================================
|
|
1074
|
+
|
|
1075
|
+
async listFilters(): Promise<{ parsed: gmail_v1.Schema$Filter[] } | AuthError | ApiError> {
|
|
1076
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1077
|
+
withRetry(() => this.gmail.users.settings.filters.list({ userId: 'me' })),
|
|
1078
|
+
)
|
|
1079
|
+
if (res instanceof Error) return res
|
|
1080
|
+
return { parsed: res.data.filter ?? [] }
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async createFilter(opts: {
|
|
1084
|
+
from?: string
|
|
1085
|
+
query?: string
|
|
1086
|
+
addLabelIds?: string[]
|
|
1087
|
+
removeLabelIds?: string[]
|
|
1088
|
+
}): Promise<gmail_v1.Schema$Filter | AuthError | ApiError> {
|
|
1089
|
+
const criteria: gmail_v1.Schema$FilterCriteria = {}
|
|
1090
|
+
if (opts.from) criteria.from = opts.from
|
|
1091
|
+
if (opts.query) criteria.query = opts.query
|
|
1092
|
+
|
|
1093
|
+
const action: gmail_v1.Schema$FilterAction = {}
|
|
1094
|
+
if (opts.addLabelIds?.length) action.addLabelIds = opts.addLabelIds
|
|
1095
|
+
if (opts.removeLabelIds?.length) action.removeLabelIds = opts.removeLabelIds
|
|
1096
|
+
|
|
1097
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1098
|
+
withRetry(() =>
|
|
1099
|
+
this.gmail.users.settings.filters.create({
|
|
1100
|
+
userId: 'me',
|
|
1101
|
+
requestBody: { criteria, action },
|
|
1102
|
+
}),
|
|
1103
|
+
),
|
|
1104
|
+
)
|
|
1105
|
+
if (res instanceof Error) return res
|
|
1106
|
+
return res.data
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
async deleteFilter(filterId: string): Promise<void | AuthError | ApiError> {
|
|
1110
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1111
|
+
withRetry(() =>
|
|
1112
|
+
this.gmail.users.settings.filters.delete({
|
|
1113
|
+
userId: 'me',
|
|
1114
|
+
id: filterId,
|
|
1115
|
+
}),
|
|
1116
|
+
),
|
|
1117
|
+
)
|
|
1118
|
+
if (res instanceof Error) return res
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/** Resolve a label name to its ID, auto-creating if missing. Public wrapper for filter commands. */
|
|
1122
|
+
async resolveLabel(nameOrId: string): Promise<string | AuthError | ApiError> {
|
|
1123
|
+
return this.resolveLabelId(nameOrId)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1024
1126
|
// =========================================================================
|
|
1025
1127
|
// Label counts (unread counts per folder/label)
|
|
1026
1128
|
// =========================================================================
|
|
@@ -1194,6 +1296,20 @@ export class GmailClient {
|
|
|
1194
1296
|
|
|
1195
1297
|
const { body, mimeType, textBody } = this.extractBody(message.payload ?? {})
|
|
1196
1298
|
|
|
1299
|
+
// Authentication-Results: multiple MTAs can add this header. Prefer the one
|
|
1300
|
+
// stamped by Gmail's trusted authserv-id (mx.google.com) to avoid trusting
|
|
1301
|
+
// headers injected by upstream/untrusted relays.
|
|
1302
|
+
const authHeaders = headers
|
|
1303
|
+
.filter((h) => h.name?.toLowerCase() === 'authentication-results')
|
|
1304
|
+
.map((h) => h.value ?? '')
|
|
1305
|
+
.filter((v) => v.length > 0)
|
|
1306
|
+
const trustedAuthHeader =
|
|
1307
|
+
authHeaders.find((v) => v.trim().toLowerCase().startsWith('mx.google.com'))
|
|
1308
|
+
?? authHeaders[0]
|
|
1309
|
+
?? null
|
|
1310
|
+
const isSentOrDraft = labelIds.includes('SENT') || labelIds.includes('DRAFT')
|
|
1311
|
+
const auth = trustedAuthHeader && !isSentOrDraft ? parseAuthResults(trustedAuthHeader) : null
|
|
1312
|
+
|
|
1197
1313
|
return {
|
|
1198
1314
|
id: message.id ?? '',
|
|
1199
1315
|
threadId: message.threadId ?? '',
|
|
@@ -1220,6 +1336,7 @@ export class GmailClient {
|
|
|
1220
1336
|
mimeType,
|
|
1221
1337
|
textBody,
|
|
1222
1338
|
attachments: this.extractAttachmentMeta(message.payload?.parts ?? []),
|
|
1339
|
+
auth,
|
|
1223
1340
|
}
|
|
1224
1341
|
}
|
|
1225
1342
|
|
|
@@ -1260,16 +1377,47 @@ export class GmailClient {
|
|
|
1260
1377
|
}
|
|
1261
1378
|
}
|
|
1262
1379
|
|
|
1380
|
+
// Parse recipients from latest message
|
|
1381
|
+
const toHeader = getHeader('to') ?? ''
|
|
1382
|
+
const ccHeaders = headers
|
|
1383
|
+
.filter((h) => h.name?.toLowerCase() === 'cc')
|
|
1384
|
+
.map((h) => h.value ?? '')
|
|
1385
|
+
.filter((v) => v.length > 0)
|
|
1386
|
+
|
|
1387
|
+
// Check if any non-draft message in the thread is a reply (has In-Reply-To header)
|
|
1388
|
+
const inReplyTo =
|
|
1389
|
+
nonDraftMessages
|
|
1390
|
+
.map(
|
|
1391
|
+
(m) =>
|
|
1392
|
+
m.payload?.headers?.find((h) => h.name?.toLowerCase() === 'in-reply-to')?.value ??
|
|
1393
|
+
null,
|
|
1394
|
+
)
|
|
1395
|
+
.find((v) => v !== null) ?? null
|
|
1396
|
+
|
|
1397
|
+
// Check if any message has attachments (non-inline)
|
|
1398
|
+
const hasAttachments = messages.some((m) => this.hasNonInlineAttachments(m.payload))
|
|
1399
|
+
|
|
1400
|
+
// List-Unsubscribe from latest message
|
|
1401
|
+
const listUnsubscribe = getHeader('list-unsubscribe') ?? null
|
|
1402
|
+
|
|
1263
1403
|
return {
|
|
1264
1404
|
id: raw.id ?? '',
|
|
1265
1405
|
historyId: raw.historyId ?? null,
|
|
1266
1406
|
snippet: sanitizeSnippet(latest?.snippet ?? ''),
|
|
1267
1407
|
subject: (getHeader('subject') ?? '(no subject)').replace(/"/g, '').trim(),
|
|
1268
1408
|
from: displayFrom,
|
|
1409
|
+
to: toHeader ? parseAddressList(toHeader) : [],
|
|
1410
|
+
cc: ccHeaders.length > 0
|
|
1411
|
+
? ccHeaders.filter((h) => h.trim().length > 0).flatMap((h) => parseAddressList(h))
|
|
1412
|
+
: [],
|
|
1269
1413
|
date: getHeader('date') ?? '',
|
|
1270
1414
|
labelIds: allLabels,
|
|
1271
1415
|
unread: allLabels.includes('UNREAD'),
|
|
1416
|
+
starred: allLabels.includes('STARRED'),
|
|
1272
1417
|
messageCount: nonDraftMessages.length,
|
|
1418
|
+
inReplyTo,
|
|
1419
|
+
hasAttachments,
|
|
1420
|
+
listUnsubscribe,
|
|
1273
1421
|
}
|
|
1274
1422
|
}
|
|
1275
1423
|
|
|
@@ -1393,6 +1541,24 @@ export class GmailClient {
|
|
|
1393
1541
|
return results
|
|
1394
1542
|
}
|
|
1395
1543
|
|
|
1544
|
+
/** Quick check: does the message payload tree contain any non-inline attachments?
|
|
1545
|
+
* Checks the root part itself (some messages have attachment metadata there)
|
|
1546
|
+
* then recurses into child parts. */
|
|
1547
|
+
private hasNonInlineAttachments(part: gmail_v1.Schema$MessagePart | undefined): boolean {
|
|
1548
|
+
if (!part) return false
|
|
1549
|
+
if (part.filename && part.filename.length > 0 && part.body?.attachmentId) {
|
|
1550
|
+
const disposition =
|
|
1551
|
+
part.headers?.find((h) => h.name?.toLowerCase() === 'content-disposition')?.value ?? ''
|
|
1552
|
+
const hasContentId = part.headers?.some((h) => h.name?.toLowerCase() === 'content-id')
|
|
1553
|
+
const isInline = disposition.toLowerCase().includes('inline')
|
|
1554
|
+
if (!isInline || !hasContentId) return true
|
|
1555
|
+
}
|
|
1556
|
+
for (const child of part.parts ?? []) {
|
|
1557
|
+
if (this.hasNonInlineAttachments(child)) return true
|
|
1558
|
+
}
|
|
1559
|
+
return false
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1396
1562
|
async getEmailAliases(): Promise<Array<{ email: string; name?: string; primary: boolean }> | AuthError | ApiError> {
|
|
1397
1563
|
const profile = await this.getProfile()
|
|
1398
1564
|
if (profile instanceof Error) return profile
|
|
@@ -1821,22 +1987,25 @@ export class GmailClient {
|
|
|
1821
1987
|
private async batchModifyMessages(
|
|
1822
1988
|
messageIds: string[],
|
|
1823
1989
|
body: { addLabelIds?: string[]; removeLabelIds?: string[] },
|
|
1824
|
-
) {
|
|
1990
|
+
): Promise<void | AuthError | ApiError> {
|
|
1825
1991
|
if (messageIds.length === 0) return
|
|
1826
1992
|
|
|
1827
1993
|
// Gmail batchModify accepts up to 1000 IDs
|
|
1828
1994
|
const chunkSize = 1000
|
|
1829
1995
|
for (let i = 0; i < messageIds.length; i += chunkSize) {
|
|
1830
1996
|
const chunk = messageIds.slice(i, i + chunkSize)
|
|
1831
|
-
await
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1997
|
+
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
1998
|
+
withRetry(() =>
|
|
1999
|
+
this.gmail.users.messages.batchModify({
|
|
2000
|
+
userId: 'me',
|
|
2001
|
+
requestBody: {
|
|
2002
|
+
ids: chunk,
|
|
2003
|
+
...body,
|
|
2004
|
+
},
|
|
2005
|
+
}),
|
|
2006
|
+
),
|
|
1839
2007
|
)
|
|
2008
|
+
if (res instanceof Error) return res
|
|
1840
2009
|
}
|
|
1841
2010
|
}
|
|
1842
2011
|
|
|
@@ -1875,6 +2044,42 @@ export class GmailClient {
|
|
|
1875
2044
|
}
|
|
1876
2045
|
}
|
|
1877
2046
|
|
|
2047
|
+
// ---------------------------------------------------------------------------
|
|
2048
|
+
// Email authentication: parse the Authentication-Results header
|
|
2049
|
+
// ---------------------------------------------------------------------------
|
|
2050
|
+
// Gmail adds an Authentication-Results header to every received message with
|
|
2051
|
+
// SPF, DKIM, and DMARC verdicts. Format is semi-structured:
|
|
2052
|
+
// Authentication-Results: mx.google.com;
|
|
2053
|
+
// dkim=pass header.i=@example.com header.s=sel1;
|
|
2054
|
+
// spf=pass (...) smtp.mailfrom=user@example.com;
|
|
2055
|
+
// dmarc=pass (p=REJECT) header.from=example.com
|
|
2056
|
+
// We extract the verdict keyword after each protocol name.
|
|
2057
|
+
// ---------------------------------------------------------------------------
|
|
2058
|
+
|
|
2059
|
+
const AUTH_VERDICTS = new Set(['pass', 'fail', 'softfail', 'neutral', 'none', 'temperror', 'permerror', 'bestguesspass'])
|
|
2060
|
+
|
|
2061
|
+
/** Parse a Gmail Authentication-Results header into structured verdicts. */
|
|
2062
|
+
export function parseAuthResults(header: string): AuthResult {
|
|
2063
|
+
const lower = header.toLowerCase()
|
|
2064
|
+
const extract = (protocol: string): AuthVerdict => {
|
|
2065
|
+
// Match "protocol=verdict" — verdict is a word that stops at whitespace, semicolons, or parens
|
|
2066
|
+
const re = new RegExp(`\\b${protocol}\\s*=\\s*([a-z]+)`)
|
|
2067
|
+
const match = re.exec(lower)
|
|
2068
|
+
const verdict = match?.[1] ?? 'none'
|
|
2069
|
+
return AUTH_VERDICTS.has(verdict) ? verdict as AuthVerdict : 'none'
|
|
2070
|
+
}
|
|
2071
|
+
const spf = extract('spf')
|
|
2072
|
+
const dkim = extract('dkim')
|
|
2073
|
+
const dmarc = extract('dmarc')
|
|
2074
|
+
return {
|
|
2075
|
+
spf,
|
|
2076
|
+
dkim,
|
|
2077
|
+
dmarc,
|
|
2078
|
+
authentic: spf === 'pass' && dkim === 'pass' && dmarc === 'pass',
|
|
2079
|
+
raw: header,
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1878
2083
|
// ---------------------------------------------------------------------------
|
|
1879
2084
|
// Watch: folder label mapping
|
|
1880
2085
|
// ---------------------------------------------------------------------------
|