@elizaos/plugin-inbox 2.0.3-beta.6 → 2.0.3-beta.7
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/dist/actions/inbox.d.ts +69 -0
- package/dist/actions/inbox.d.ts.map +1 -0
- package/dist/actions/inbox.js +345 -0
- package/dist/actions/inbox.js.map +1 -0
- package/dist/components/inbox/InboxSpatialView.d.ts +54 -0
- package/dist/components/inbox/InboxSpatialView.d.ts.map +1 -0
- package/dist/components/inbox/InboxSpatialView.js +171 -0
- package/dist/components/inbox/InboxSpatialView.js.map +1 -0
- package/dist/components/inbox/InboxView.d.ts +64 -0
- package/dist/components/inbox/InboxView.d.ts.map +1 -0
- package/dist/components/inbox/InboxView.js +169 -0
- package/dist/components/inbox/InboxView.js.map +1 -0
- package/dist/components/inbox/inbox-view-bundle.d.ts +2 -0
- package/dist/components/inbox/inbox-view-bundle.d.ts.map +1 -0
- package/dist/components/inbox/inbox-view-bundle.js +5 -0
- package/dist/components/inbox/inbox-view-bundle.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +1729 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +79 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/sql.d.ts +32 -0
- package/dist/db/sql.d.ts.map +1 -0
- package/dist/db/sql.js +130 -0
- package/dist/db/sql.js.map +1 -0
- package/dist/inbox/channel-deep-links.d.ts +7 -0
- package/dist/inbox/channel-deep-links.d.ts.map +1 -0
- package/dist/inbox/channel-deep-links.js +97 -0
- package/dist/inbox/channel-deep-links.js.map +1 -0
- package/dist/inbox/config.d.ts +7 -0
- package/dist/inbox/config.d.ts.map +1 -0
- package/dist/inbox/config.js +61 -0
- package/dist/inbox/config.js.map +1 -0
- package/dist/inbox/email-curation.d.ts +174 -0
- package/dist/inbox/email-curation.d.ts.map +1 -0
- package/dist/inbox/email-curation.js +1056 -0
- package/dist/inbox/email-curation.js.map +1 -0
- package/dist/inbox/email-unsubscribe-types.d.ts +71 -0
- package/dist/inbox/email-unsubscribe-types.d.ts.map +1 -0
- package/dist/inbox/email-unsubscribe-types.js +1 -0
- package/dist/inbox/email-unsubscribe-types.js.map +1 -0
- package/dist/inbox/gmail-normalize.d.ts +99 -0
- package/dist/inbox/gmail-normalize.d.ts.map +1 -0
- package/dist/inbox/gmail-normalize.js +937 -0
- package/dist/inbox/gmail-normalize.js.map +1 -0
- package/dist/inbox/google-gmail-seam.d.ts +52 -0
- package/dist/inbox/google-gmail-seam.d.ts.map +1 -0
- package/dist/inbox/google-gmail-seam.js +263 -0
- package/dist/inbox/google-gmail-seam.js.map +1 -0
- package/dist/inbox/message-fetcher.d.ts +47 -0
- package/dist/inbox/message-fetcher.d.ts.map +1 -0
- package/dist/inbox/message-fetcher.js +461 -0
- package/dist/inbox/message-fetcher.js.map +1 -0
- package/dist/inbox/migration.d.ts +46 -0
- package/dist/inbox/migration.d.ts.map +1 -0
- package/dist/inbox/migration.js +114 -0
- package/dist/inbox/migration.js.map +1 -0
- package/dist/inbox/reflection.d.ts +40 -0
- package/dist/inbox/reflection.d.ts.map +1 -0
- package/dist/inbox/reflection.js +142 -0
- package/dist/inbox/reflection.js.map +1 -0
- package/dist/inbox/repository.d.ts +58 -0
- package/dist/inbox/repository.d.ts.map +1 -0
- package/dist/inbox/repository.js +376 -0
- package/dist/inbox/repository.js.map +1 -0
- package/dist/inbox/service.d.ts +149 -0
- package/dist/inbox/service.d.ts.map +1 -0
- package/dist/inbox/service.js +247 -0
- package/dist/inbox/service.js.map +1 -0
- package/dist/inbox/triage-classifier.d.ts +28 -0
- package/dist/inbox/triage-classifier.d.ts.map +1 -0
- package/dist/inbox/triage-classifier.js +306 -0
- package/dist/inbox/triage-classifier.js.map +1 -0
- package/dist/inbox/types.d.ts +124 -0
- package/dist/inbox/types.d.ts.map +1 -0
- package/dist/inbox/types.js +1 -0
- package/dist/inbox/types.js.map +1 -0
- package/dist/inbox/unsubscribe-repository.d.ts +14 -0
- package/dist/inbox/unsubscribe-repository.d.ts.map +1 -0
- package/dist/inbox/unsubscribe-repository.js +112 -0
- package/dist/inbox/unsubscribe-repository.js.map +1 -0
- package/dist/inbox/unsubscribe-service.d.ts +41 -0
- package/dist/inbox/unsubscribe-service.d.ts.map +1 -0
- package/dist/inbox/unsubscribe-service.js +351 -0
- package/dist/inbox/unsubscribe-service.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +38 -0
- package/dist/plugin.js.map +1 -0
- package/dist/providers/cross-channel-context.d.ts +21 -0
- package/dist/providers/cross-channel-context.d.ts.map +1 -0
- package/dist/providers/cross-channel-context.js +96 -0
- package/dist/providers/cross-channel-context.js.map +1 -0
- package/dist/providers/inbox-triage.d.ts +12 -0
- package/dist/providers/inbox-triage.d.ts.map +1 -0
- package/dist/providers/inbox-triage.js +98 -0
- package/dist/providers/inbox-triage.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +21 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/register.d.ts +9 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +5 -0
- package/dist/register.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/dist/views/bundle.js +315 -0
- package/dist/views/bundle.js.map +1 -0
- package/package.json +9 -9
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
fail,
|
|
4
|
+
GOOGLE_CALENDAR_CACHE_TTL_MS,
|
|
5
|
+
GOOGLE_GMAIL_CACHE_TTL_MS,
|
|
6
|
+
LIFEOPS_GMAIL_BULK_OPERATIONS,
|
|
7
|
+
LIFEOPS_GMAIL_DRAFT_TONES,
|
|
8
|
+
LIFEOPS_GMAIL_SPAM_REVIEW_STATUSES,
|
|
9
|
+
normalizeEnumValue,
|
|
10
|
+
normalizeFiniteNumber,
|
|
11
|
+
normalizeOptionalString,
|
|
12
|
+
requireNonEmptyString
|
|
13
|
+
} from "@elizaos/shared";
|
|
14
|
+
function normalizeGmailSearchQuery(value) {
|
|
15
|
+
const query = requireNonEmptyString(value, "query");
|
|
16
|
+
if (query.length > 500) {
|
|
17
|
+
fail(400, "query must be 500 characters or fewer");
|
|
18
|
+
}
|
|
19
|
+
return query;
|
|
20
|
+
}
|
|
21
|
+
function normalizeGmailBulkOperation(value) {
|
|
22
|
+
return normalizeEnumValue(value, "operation", LIFEOPS_GMAIL_BULK_OPERATIONS);
|
|
23
|
+
}
|
|
24
|
+
function normalizeGmailUnrespondedOlderThanDays(value) {
|
|
25
|
+
if (value === void 0 || value === null || value === "") {
|
|
26
|
+
return 3;
|
|
27
|
+
}
|
|
28
|
+
const days = Math.trunc(normalizeFiniteNumber(value, "olderThanDays"));
|
|
29
|
+
if (days < 1 || days > 3650) {
|
|
30
|
+
fail(400, "olderThanDays must be between 1 and 3650");
|
|
31
|
+
}
|
|
32
|
+
return days;
|
|
33
|
+
}
|
|
34
|
+
function parseGmailRelativeDuration(value) {
|
|
35
|
+
const match = value.trim().toLowerCase().match(/^(\d+)([dmy])$/);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const amount = Number(match[1]);
|
|
40
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const unit = match[2];
|
|
44
|
+
const days = unit === "d" ? amount : unit === "m" ? amount * 30 : amount * 365;
|
|
45
|
+
return days * 24 * 60 * 60 * 1e3;
|
|
46
|
+
}
|
|
47
|
+
function parseGmailDateBoundary(value) {
|
|
48
|
+
const normalized = value.trim().replace(/\//g, "-");
|
|
49
|
+
const match = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
50
|
+
if (!match) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const year = Number(match[1]);
|
|
54
|
+
const month = Number(match[2]);
|
|
55
|
+
const day = Number(match[3]);
|
|
56
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day) || month < 1 || month > 12 || day < 1 || day > 31) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return Date.UTC(year, month - 1, day, 0, 0, 0, 0);
|
|
60
|
+
}
|
|
61
|
+
function splitMailboxLikeList(value) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
let current = "";
|
|
64
|
+
let inQuotes = false;
|
|
65
|
+
let angleDepth = 0;
|
|
66
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
67
|
+
const char = value[index];
|
|
68
|
+
const next = value[index + 1];
|
|
69
|
+
if (char === '"') {
|
|
70
|
+
inQuotes = !inQuotes;
|
|
71
|
+
current += char;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!inQuotes && char === "<") {
|
|
75
|
+
angleDepth += 1;
|
|
76
|
+
current += char;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!inQuotes && char === ">") {
|
|
80
|
+
angleDepth = Math.max(0, angleDepth - 1);
|
|
81
|
+
current += char;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!inQuotes && angleDepth === 0 && char === "|" && next === "|") {
|
|
85
|
+
const trimmed2 = current.trim();
|
|
86
|
+
if (trimmed2.length > 0) {
|
|
87
|
+
parts.push(trimmed2);
|
|
88
|
+
}
|
|
89
|
+
current = "";
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!inQuotes && angleDepth === 0 && (char === "," || char === ";" || char === "\n")) {
|
|
94
|
+
const trimmed2 = current.trim();
|
|
95
|
+
if (trimmed2.length > 0) {
|
|
96
|
+
parts.push(trimmed2);
|
|
97
|
+
}
|
|
98
|
+
current = "";
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
current += char;
|
|
102
|
+
}
|
|
103
|
+
const trimmed = current.trim();
|
|
104
|
+
if (trimmed.length > 0) {
|
|
105
|
+
parts.push(trimmed);
|
|
106
|
+
}
|
|
107
|
+
return parts;
|
|
108
|
+
}
|
|
109
|
+
function extractNormalizedEmailAddress(value) {
|
|
110
|
+
const trimmed = value.trim().replace(/^mailto:/i, "");
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const angleMatch = trimmed.match(/<\s*([^<>\s@]+@[^<>\s@]+)\s*>/u);
|
|
115
|
+
const rawCandidate = angleMatch?.[1] ?? trimmed.match(/([^\s<>()"';,]+@[^\s<>()"';,]+)/u)?.[1] ?? trimmed;
|
|
116
|
+
const normalized = rawCandidate.trim().replace(/^["']+|["']+$/g, "").replace(/[>;,\s]+$/g, "").toLowerCase();
|
|
117
|
+
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/u.test(normalized) ? normalized : null;
|
|
118
|
+
}
|
|
119
|
+
function normalizeOptionalMessageIdArray(value, field) {
|
|
120
|
+
if (value === void 0) {
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(value)) {
|
|
124
|
+
fail(400, `${field} must be an array`);
|
|
125
|
+
}
|
|
126
|
+
const items = [];
|
|
127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
128
|
+
for (const [index, candidate] of value.entries()) {
|
|
129
|
+
const item = requireNonEmptyString(candidate, `${field}[${index}]`);
|
|
130
|
+
if (seen.has(item)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
seen.add(item);
|
|
134
|
+
items.push(item);
|
|
135
|
+
}
|
|
136
|
+
if (items.length > 50) {
|
|
137
|
+
fail(400, `${field} must contain 50 items or fewer`);
|
|
138
|
+
}
|
|
139
|
+
return items;
|
|
140
|
+
}
|
|
141
|
+
function normalizeOptionalGmailLabelIdArray(value, field) {
|
|
142
|
+
if (value === void 0) {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(value)) {
|
|
146
|
+
fail(400, `${field} must be an array`);
|
|
147
|
+
}
|
|
148
|
+
const items = [];
|
|
149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const [index, candidate] of value.entries()) {
|
|
151
|
+
const item = requireNonEmptyString(candidate, `${field}[${index}]`);
|
|
152
|
+
if (item.length > 128) {
|
|
153
|
+
fail(400, `${field}[${index}] must be 128 characters or fewer`);
|
|
154
|
+
}
|
|
155
|
+
if (!/^[A-Za-z0-9_:-]+$/.test(item)) {
|
|
156
|
+
fail(400, `${field}[${index}] is not a valid Gmail label id`);
|
|
157
|
+
}
|
|
158
|
+
if (seen.has(item)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
seen.add(item);
|
|
162
|
+
items.push(item);
|
|
163
|
+
}
|
|
164
|
+
if (items.length > 20) {
|
|
165
|
+
fail(400, `${field} must contain 20 items or fewer`);
|
|
166
|
+
}
|
|
167
|
+
return items;
|
|
168
|
+
}
|
|
169
|
+
function normalizeGmailSearchQueryMatches(query, message) {
|
|
170
|
+
const all = [
|
|
171
|
+
message.subject,
|
|
172
|
+
message.from,
|
|
173
|
+
message.fromEmail ?? "",
|
|
174
|
+
message.replyTo ?? "",
|
|
175
|
+
message.snippet,
|
|
176
|
+
...message.to,
|
|
177
|
+
...message.cc,
|
|
178
|
+
...message.labels
|
|
179
|
+
].join(" ").toLowerCase();
|
|
180
|
+
const sender = [message.from, message.fromEmail ?? "", message.replyTo ?? ""].join(" ").toLowerCase();
|
|
181
|
+
const subject = message.subject.toLowerCase();
|
|
182
|
+
const to = message.to.join(" ").toLowerCase();
|
|
183
|
+
const cc = message.cc.join(" ").toLowerCase();
|
|
184
|
+
const labels = message.labels.join(" ").toLowerCase();
|
|
185
|
+
const receivedAtMs = Date.parse(message.receivedAt);
|
|
186
|
+
const nowMs = Date.now();
|
|
187
|
+
const tokens = [];
|
|
188
|
+
let current = "";
|
|
189
|
+
let inQuotes = false;
|
|
190
|
+
let braceDepth = 0;
|
|
191
|
+
for (const char of query.trim()) {
|
|
192
|
+
if (char === '"') {
|
|
193
|
+
inQuotes = !inQuotes;
|
|
194
|
+
current += char;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (!inQuotes && char === "{") {
|
|
198
|
+
braceDepth += 1;
|
|
199
|
+
current += char;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!inQuotes && char === "}") {
|
|
203
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
204
|
+
current += char;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!inQuotes && braceDepth === 0 && /\s/.test(char)) {
|
|
208
|
+
if (current.length > 0) {
|
|
209
|
+
tokens.push(current);
|
|
210
|
+
current = "";
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
current += char;
|
|
215
|
+
}
|
|
216
|
+
if (current.length > 0) {
|
|
217
|
+
tokens.push(current);
|
|
218
|
+
}
|
|
219
|
+
if (tokens.length === 0) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const matchesToken = (token) => {
|
|
223
|
+
const normalizedToken = token.trim();
|
|
224
|
+
if (normalizedToken.length === 0) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
const isNegated = normalizedToken.startsWith("-");
|
|
228
|
+
const tokenBody = isNegated ? normalizedToken.slice(1).trim() : normalizedToken;
|
|
229
|
+
if (!tokenBody) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (tokenBody.startsWith("{") && tokenBody.endsWith("}")) {
|
|
233
|
+
const groupMembers = tokenBody.slice(1, -1).trim().split(/\s+/).map((entry) => entry.trim()).filter(Boolean);
|
|
234
|
+
if (groupMembers.length === 0) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
const groupMatched = groupMembers.some((entry) => matchesToken(entry));
|
|
238
|
+
return isNegated ? !groupMatched : groupMatched;
|
|
239
|
+
}
|
|
240
|
+
const operatorMatch = tokenBody.match(/^([a-z_]+):(.*)$/i);
|
|
241
|
+
const rawValue = operatorMatch?.[2] ?? tokenBody;
|
|
242
|
+
const value = rawValue.replace(/^"|"$/g, "").trim().toLowerCase();
|
|
243
|
+
if (value.length === 0) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
const labelTokens = message.labels.map((label) => label.toLowerCase());
|
|
247
|
+
const hasAttachment = typeof message.metadata.hasAttachments === "boolean" ? message.metadata.hasAttachments === true : /\battach(?:ed|ment|ments)?\b/i.test(
|
|
248
|
+
`${message.subject} ${message.snippet}`
|
|
249
|
+
);
|
|
250
|
+
const matched = (() => {
|
|
251
|
+
if (!operatorMatch) {
|
|
252
|
+
return all.includes(value);
|
|
253
|
+
}
|
|
254
|
+
const operator = (operatorMatch[1] ?? "").toLowerCase();
|
|
255
|
+
switch (operator) {
|
|
256
|
+
case "from":
|
|
257
|
+
if (value === "me") {
|
|
258
|
+
return labelTokens.includes("sent");
|
|
259
|
+
}
|
|
260
|
+
return sender.includes(value);
|
|
261
|
+
case "subject":
|
|
262
|
+
return subject.includes(value);
|
|
263
|
+
case "to":
|
|
264
|
+
return to.includes(value);
|
|
265
|
+
case "cc":
|
|
266
|
+
return cc.includes(value);
|
|
267
|
+
case "label":
|
|
268
|
+
case "labels":
|
|
269
|
+
return labels.includes(value);
|
|
270
|
+
case "category":
|
|
271
|
+
return labelTokens.includes(`category_${value}`);
|
|
272
|
+
case "in":
|
|
273
|
+
return value === "anywhere" ? true : labelTokens.includes(value);
|
|
274
|
+
case "has":
|
|
275
|
+
return value === "attachment" ? hasAttachment : all.includes(value);
|
|
276
|
+
case "is":
|
|
277
|
+
if (value === "unread") {
|
|
278
|
+
return message.isUnread;
|
|
279
|
+
}
|
|
280
|
+
if (value === "read") {
|
|
281
|
+
return !message.isUnread;
|
|
282
|
+
}
|
|
283
|
+
if (value === "important") {
|
|
284
|
+
return message.isImportant;
|
|
285
|
+
}
|
|
286
|
+
if (value === "starred") {
|
|
287
|
+
return labelTokens.includes("starred");
|
|
288
|
+
}
|
|
289
|
+
return all.includes(value);
|
|
290
|
+
case "newer_than": {
|
|
291
|
+
const relativeMs = parseGmailRelativeDuration(value);
|
|
292
|
+
return relativeMs === null ? all.includes(value) : receivedAtMs >= nowMs - relativeMs;
|
|
293
|
+
}
|
|
294
|
+
case "older_than": {
|
|
295
|
+
const relativeMs = parseGmailRelativeDuration(value);
|
|
296
|
+
return relativeMs === null ? all.includes(value) : receivedAtMs <= nowMs - relativeMs;
|
|
297
|
+
}
|
|
298
|
+
case "after": {
|
|
299
|
+
const boundary = parseGmailDateBoundary(value);
|
|
300
|
+
return boundary === null ? all.includes(value) : receivedAtMs >= boundary;
|
|
301
|
+
}
|
|
302
|
+
case "before": {
|
|
303
|
+
const boundary = parseGmailDateBoundary(value);
|
|
304
|
+
return boundary === null ? all.includes(value) : receivedAtMs < boundary;
|
|
305
|
+
}
|
|
306
|
+
default:
|
|
307
|
+
return all.includes(value);
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
return isNegated ? !matched : matched;
|
|
311
|
+
};
|
|
312
|
+
return tokens.every((token) => {
|
|
313
|
+
const normalizedToken = token.trim();
|
|
314
|
+
if (normalizedToken.length === 0) {
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
const operatorMatch = normalizedToken.match(/^([a-z_]+):(.*)$/i);
|
|
318
|
+
const operator = operatorMatch?.[1]?.toLowerCase();
|
|
319
|
+
const operatorValue = operatorMatch?.[2];
|
|
320
|
+
if (operator === "or" && operatorValue) {
|
|
321
|
+
return matchesToken(operatorValue);
|
|
322
|
+
}
|
|
323
|
+
return matchesToken(normalizedToken);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function filterGmailMessagesBySearch(args) {
|
|
327
|
+
const query = normalizeOptionalString(args.query);
|
|
328
|
+
const filtered = query ? args.messages.filter(
|
|
329
|
+
(message) => normalizeGmailSearchQueryMatches(query, message)
|
|
330
|
+
) : args.messages;
|
|
331
|
+
const replyNeededOnly = args.replyNeededOnly === true;
|
|
332
|
+
return filtered.filter((message) => !replyNeededOnly || message.likelyReplyNeeded).sort(compareGmailMessagePriority);
|
|
333
|
+
}
|
|
334
|
+
function compareGmailMessagePriority(left, right) {
|
|
335
|
+
if (left.isImportant !== right.isImportant) {
|
|
336
|
+
return right.isImportant ? 1 : -1;
|
|
337
|
+
}
|
|
338
|
+
if (left.likelyReplyNeeded !== right.likelyReplyNeeded) {
|
|
339
|
+
return right.likelyReplyNeeded ? 1 : -1;
|
|
340
|
+
}
|
|
341
|
+
if (left.isUnread !== right.isUnread) {
|
|
342
|
+
return right.isUnread ? 1 : -1;
|
|
343
|
+
}
|
|
344
|
+
return Date.parse(right.receivedAt) - Date.parse(left.receivedAt);
|
|
345
|
+
}
|
|
346
|
+
function normalizeGmailDraftTone(value) {
|
|
347
|
+
return normalizeEnumValue(
|
|
348
|
+
value ?? "neutral",
|
|
349
|
+
"tone",
|
|
350
|
+
LIFEOPS_GMAIL_DRAFT_TONES
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
function normalizeOptionalStringArray(value, field) {
|
|
354
|
+
if (value === void 0) {
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
const rawValues = Array.isArray(value) ? value : typeof value === "string" ? splitMailboxLikeList(value) : fail(400, `${field} must be an array or string`);
|
|
358
|
+
const items = [];
|
|
359
|
+
const seen = /* @__PURE__ */ new Set();
|
|
360
|
+
for (const [index, candidate] of rawValues.entries()) {
|
|
361
|
+
const source = requireNonEmptyString(candidate, `${field}[${index}]`);
|
|
362
|
+
const item = extractNormalizedEmailAddress(source);
|
|
363
|
+
if (!item) {
|
|
364
|
+
fail(400, `${field}[${index}] must be a valid email address`);
|
|
365
|
+
}
|
|
366
|
+
if (seen.has(item)) {
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
seen.add(item);
|
|
370
|
+
items.push(item);
|
|
371
|
+
}
|
|
372
|
+
return items;
|
|
373
|
+
}
|
|
374
|
+
function normalizeGmailReplyBody(value) {
|
|
375
|
+
const body = requireNonEmptyString(value, "bodyText");
|
|
376
|
+
if (body.length > 8e3) {
|
|
377
|
+
fail(400, "bodyText must be 8000 characters or fewer");
|
|
378
|
+
}
|
|
379
|
+
return body;
|
|
380
|
+
}
|
|
381
|
+
function summarizeGmailSearch(messages) {
|
|
382
|
+
return {
|
|
383
|
+
totalCount: messages.length,
|
|
384
|
+
unreadCount: messages.filter((message) => message.isUnread).length,
|
|
385
|
+
importantCount: messages.filter((message) => message.isImportant).length,
|
|
386
|
+
replyNeededCount: messages.filter((message) => message.likelyReplyNeeded).length
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function summarizeGmailBatchReplyDrafts(drafts) {
|
|
390
|
+
return {
|
|
391
|
+
totalCount: drafts.length,
|
|
392
|
+
sendAllowedCount: drafts.filter((draft) => draft.sendAllowed).length,
|
|
393
|
+
requiresConfirmationCount: drafts.filter(
|
|
394
|
+
(draft) => draft.requiresConfirmation
|
|
395
|
+
).length
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function collectCalendarEventContactEmails(event) {
|
|
399
|
+
const emails = /* @__PURE__ */ new Set();
|
|
400
|
+
const organizerEmail = typeof event.organizer?.email === "string" ? event.organizer.email.trim().toLowerCase() : "";
|
|
401
|
+
if (organizerEmail) {
|
|
402
|
+
emails.add(organizerEmail);
|
|
403
|
+
}
|
|
404
|
+
for (const attendee of event.attendees) {
|
|
405
|
+
const email = attendee.email?.trim().toLowerCase() || "";
|
|
406
|
+
if (email) {
|
|
407
|
+
emails.add(email);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return emails;
|
|
411
|
+
}
|
|
412
|
+
function extractSubjectTokens(subject) {
|
|
413
|
+
return subject.toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length >= 4);
|
|
414
|
+
}
|
|
415
|
+
function findLinkedMailForCalendarEvent(event, messages) {
|
|
416
|
+
const relatedEmails = collectCalendarEventContactEmails(event);
|
|
417
|
+
const subjectTokens = new Set(extractSubjectTokens(event.title));
|
|
418
|
+
return messages.filter((message) => {
|
|
419
|
+
if (message.fromEmail && relatedEmails.has(message.fromEmail.toLowerCase())) {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
if (message.to.some(
|
|
423
|
+
(entry) => relatedEmails.has(entry.trim().toLowerCase())
|
|
424
|
+
) || message.cc.some(
|
|
425
|
+
(entry) => relatedEmails.has(entry.trim().toLowerCase())
|
|
426
|
+
)) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const messageTokens = extractSubjectTokens(message.subject);
|
|
430
|
+
return messageTokens.some((token) => subjectTokens.has(token));
|
|
431
|
+
}).sort((left, right) => {
|
|
432
|
+
const receivedDelta = Date.parse(right.receivedAt) - Date.parse(left.receivedAt);
|
|
433
|
+
if (receivedDelta !== 0) {
|
|
434
|
+
return receivedDelta;
|
|
435
|
+
}
|
|
436
|
+
return compareGmailMessagePriority(left, right);
|
|
437
|
+
}).slice(0, 3);
|
|
438
|
+
}
|
|
439
|
+
function isGmailSyncStateFresh(args) {
|
|
440
|
+
const syncedAtMs = Date.parse(args.syncedAt);
|
|
441
|
+
if (!Number.isFinite(syncedAtMs)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (args.now.getTime() - syncedAtMs > GOOGLE_GMAIL_CACHE_TTL_MS) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
return args.maxResults >= args.requestedMaxResults;
|
|
448
|
+
}
|
|
449
|
+
function summarizeGmailTriage(messages) {
|
|
450
|
+
return {
|
|
451
|
+
unreadCount: messages.filter((message) => message.isUnread).length,
|
|
452
|
+
importantNewCount: messages.filter(
|
|
453
|
+
(message) => message.isUnread && message.isImportant
|
|
454
|
+
).length,
|
|
455
|
+
likelyReplyNeededCount: messages.filter(
|
|
456
|
+
(message) => message.likelyReplyNeeded
|
|
457
|
+
).length
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function summarizeGmailNeedsResponse(messages) {
|
|
461
|
+
return {
|
|
462
|
+
totalCount: messages.length,
|
|
463
|
+
unreadCount: messages.filter((message) => message.isUnread).length,
|
|
464
|
+
importantCount: messages.filter((message) => message.isImportant).length
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function summarizeGmailUnresponded(threads) {
|
|
468
|
+
return {
|
|
469
|
+
totalCount: threads.length,
|
|
470
|
+
oldestDaysWaiting: threads.length > 0 ? Math.max(...threads.map((thread) => thread.daysWaiting)) : null
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function recommendationMessage(message) {
|
|
474
|
+
return {
|
|
475
|
+
messageId: message.id,
|
|
476
|
+
subject: message.subject,
|
|
477
|
+
from: message.from,
|
|
478
|
+
fromEmail: message.fromEmail,
|
|
479
|
+
receivedAt: message.receivedAt,
|
|
480
|
+
snippet: message.snippet,
|
|
481
|
+
labels: message.labels
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function hasGmailLabel(message, labelId) {
|
|
485
|
+
const normalized = labelId.trim().toUpperCase();
|
|
486
|
+
return message.labels.some(
|
|
487
|
+
(label) => label.trim().toUpperCase() === normalized
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
function isGmailSpamReviewCandidate(message) {
|
|
491
|
+
const metadata = message.metadata;
|
|
492
|
+
const metadataClassification = typeof metadata.spamClassification === "string" ? metadata.spamClassification.trim().toLowerCase() : "";
|
|
493
|
+
const metadataThreat = typeof metadata.threatCategory === "string" ? metadata.threatCategory.trim().toLowerCase() : "";
|
|
494
|
+
const triageReason = message.triageReason.trim().toLowerCase();
|
|
495
|
+
return hasGmailLabel(message, "SPAM") || hasGmailLabel(message, "PHISHING") || metadata.spam === true || metadata.phishing === true || metadataClassification === "spam" || metadataClassification === "phishing" || metadataThreat === "phishing" || /\b(?:spam|phish(?:ing)?)\b/.test(triageReason);
|
|
496
|
+
}
|
|
497
|
+
function buildGmailSpamReviewItem(args) {
|
|
498
|
+
const message = args.message;
|
|
499
|
+
const isPhishing = hasGmailLabel(message, "PHISHING") || message.metadata.phishing === true || message.metadata.spamClassification === "phishing" || message.metadata.threatCategory === "phishing" || /\bphish(?:ing)?\b/i.test(message.triageReason);
|
|
500
|
+
const isGmailSpam = hasGmailLabel(message, "SPAM");
|
|
501
|
+
const rationale = isPhishing ? "Gmail or upstream triage flagged this message as a phishing candidate; review it before reporting spam." : isGmailSpam ? "Gmail labels this message as spam; review it before reporting or deleting." : "LifeOps classified this Gmail message as a spam candidate; review it before reporting spam.";
|
|
502
|
+
const confidence = isGmailSpam ? 0.92 : isPhishing ? 0.88 : 0.76;
|
|
503
|
+
return {
|
|
504
|
+
id: createGmailSpamReviewItemId(
|
|
505
|
+
message.agentId,
|
|
506
|
+
message.provider,
|
|
507
|
+
message.side,
|
|
508
|
+
args.grantId,
|
|
509
|
+
message.externalId
|
|
510
|
+
),
|
|
511
|
+
agentId: message.agentId,
|
|
512
|
+
provider: message.provider,
|
|
513
|
+
side: message.side,
|
|
514
|
+
grantId: args.grantId,
|
|
515
|
+
accountEmail: args.accountEmail,
|
|
516
|
+
messageId: message.id,
|
|
517
|
+
externalMessageId: message.externalId,
|
|
518
|
+
threadId: message.threadId,
|
|
519
|
+
subject: message.subject,
|
|
520
|
+
from: message.from,
|
|
521
|
+
fromEmail: message.fromEmail,
|
|
522
|
+
receivedAt: message.receivedAt,
|
|
523
|
+
snippet: message.snippet,
|
|
524
|
+
labels: message.labels,
|
|
525
|
+
rationale,
|
|
526
|
+
confidence,
|
|
527
|
+
status: "pending",
|
|
528
|
+
createdAt: args.now,
|
|
529
|
+
updatedAt: args.now,
|
|
530
|
+
reviewedAt: null
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function normalizeGmailSpamReviewStatus(value, field = "status") {
|
|
534
|
+
return normalizeEnumValue(value, field, LIFEOPS_GMAIL_SPAM_REVIEW_STATUSES);
|
|
535
|
+
}
|
|
536
|
+
function summarizeGmailSpamReviewItems(items) {
|
|
537
|
+
return {
|
|
538
|
+
totalCount: items.length,
|
|
539
|
+
pendingCount: items.filter((item) => item.status === "pending").length,
|
|
540
|
+
confirmedSpamCount: items.filter((item) => item.status === "confirmed_spam").length,
|
|
541
|
+
notSpamCount: items.filter((item) => item.status === "not_spam").length,
|
|
542
|
+
dismissedCount: items.filter((item) => item.status === "dismissed").length
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function isAutomatedLowValueGmailMessage(message) {
|
|
546
|
+
const precedence = typeof message.metadata.precedence === "string" ? message.metadata.precedence.trim().toLowerCase() : "";
|
|
547
|
+
return !message.likelyReplyNeeded && (Boolean(message.metadata.listId) || precedence === "bulk" || precedence === "list" || hasGmailLabel(message, "CATEGORY_PROMOTIONS"));
|
|
548
|
+
}
|
|
549
|
+
function metadataString(metadata, field) {
|
|
550
|
+
const value = metadata[field];
|
|
551
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
552
|
+
}
|
|
553
|
+
function metadataBoolean(metadata, field) {
|
|
554
|
+
return metadata[field] === true;
|
|
555
|
+
}
|
|
556
|
+
function uniqueStrings(values) {
|
|
557
|
+
const items = [];
|
|
558
|
+
const seen = /* @__PURE__ */ new Set();
|
|
559
|
+
for (const value of values) {
|
|
560
|
+
if (!value || seen.has(value)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
seen.add(value);
|
|
564
|
+
items.push(value);
|
|
565
|
+
}
|
|
566
|
+
return items;
|
|
567
|
+
}
|
|
568
|
+
function hasGmailBodyTextContext(message) {
|
|
569
|
+
return ["bodyText", "plainTextBody", "bodyPlainText", "textBody"].some(
|
|
570
|
+
(field) => metadataString(message.metadata, field) !== null
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
function hasGmailReplyHeaderContext(message) {
|
|
574
|
+
return metadataString(message.metadata, "messageIdHeader") !== null || metadataString(message.metadata, "referencesHeader") !== null;
|
|
575
|
+
}
|
|
576
|
+
function gmailPolicySignalsForMessage(message) {
|
|
577
|
+
const precedence = metadataString(message.metadata, "precedence")?.toLowerCase().replace(/\s+/g, "_");
|
|
578
|
+
const autoSubmitted = metadataString(message.metadata, "autoSubmitted")?.toLowerCase().replace(/\s+/g, "_");
|
|
579
|
+
const spamClassification = metadataString(
|
|
580
|
+
message.metadata,
|
|
581
|
+
"spamClassification"
|
|
582
|
+
)?.toLowerCase().replace(/\s+/g, "_");
|
|
583
|
+
const threatCategory = metadataString(message.metadata, "threatCategory")?.toLowerCase().replace(/\s+/g, "_");
|
|
584
|
+
const triageReason = message.triageReason.toLowerCase();
|
|
585
|
+
return uniqueStrings([
|
|
586
|
+
message.likelyReplyNeeded ? "likely_reply_needed" : "reply_not_needed",
|
|
587
|
+
message.isUnread ? "unread" : "read",
|
|
588
|
+
message.isImportant ? "important" : "not_important",
|
|
589
|
+
hasGmailLabel(message, "INBOX") ? "label:inbox" : null,
|
|
590
|
+
hasGmailLabel(message, "SPAM") ? "label:spam" : null,
|
|
591
|
+
hasGmailLabel(message, "PHISHING") ? "label:phishing" : null,
|
|
592
|
+
hasGmailLabel(message, "CATEGORY_PROMOTIONS") ? "label:category_promotions" : null,
|
|
593
|
+
metadataString(message.metadata, "listId") !== null ? "header:list_id" : null,
|
|
594
|
+
precedence ? `header:precedence:${precedence}` : null,
|
|
595
|
+
autoSubmitted ? `header:auto_submitted:${autoSubmitted}` : null,
|
|
596
|
+
metadataBoolean(message.metadata, "spam") ? "metadata:spam" : null,
|
|
597
|
+
metadataBoolean(message.metadata, "phishing") ? "metadata:phishing" : null,
|
|
598
|
+
spamClassification ? `metadata:spam_classification:${spamClassification}` : null,
|
|
599
|
+
threatCategory ? `metadata:threat_category:${threatCategory}` : null,
|
|
600
|
+
triageReason.includes("direct-unread-reply-needed") ? "triage:direct_unread_reply_needed" : null,
|
|
601
|
+
triageReason.includes("automated-header") ? "triage:automated_header" : null
|
|
602
|
+
]);
|
|
603
|
+
}
|
|
604
|
+
function buildGmailRecommendationContextReadiness(args) {
|
|
605
|
+
const bodyAvailableCount = args.messages.filter(
|
|
606
|
+
hasGmailBodyTextContext
|
|
607
|
+
).length;
|
|
608
|
+
const snippetAvailableCount = args.messages.filter(
|
|
609
|
+
(message) => message.snippet.trim().length > 0
|
|
610
|
+
).length;
|
|
611
|
+
const threadLinkAvailableCount = args.messages.filter(
|
|
612
|
+
(message) => message.htmlLink !== null
|
|
613
|
+
).length;
|
|
614
|
+
const replyHeaderAvailableCount = args.messages.filter(
|
|
615
|
+
hasGmailReplyHeaderContext
|
|
616
|
+
).length;
|
|
617
|
+
const requiresBodyReadBeforeDraft = args.kind === "reply";
|
|
618
|
+
const bodyStatus = bodyAvailableCount === args.messages.length ? "available" : snippetAvailableCount > 0 ? "summary_only" : "missing";
|
|
619
|
+
return {
|
|
620
|
+
bodyStatus,
|
|
621
|
+
bodyAvailableCount,
|
|
622
|
+
snippetAvailableCount,
|
|
623
|
+
threadLinkAvailableCount,
|
|
624
|
+
replyHeaderAvailableCount,
|
|
625
|
+
requiresBodyReadBeforeDraft,
|
|
626
|
+
summaryFields: uniqueStrings([
|
|
627
|
+
"subject",
|
|
628
|
+
"sender",
|
|
629
|
+
"recipients",
|
|
630
|
+
"received_at",
|
|
631
|
+
"labels",
|
|
632
|
+
"triage_reason",
|
|
633
|
+
snippetAvailableCount > 0 ? "snippet" : null,
|
|
634
|
+
threadLinkAvailableCount > 0 ? "thread_link" : null,
|
|
635
|
+
replyHeaderAvailableCount > 0 ? "reply_headers" : null
|
|
636
|
+
]),
|
|
637
|
+
missingContext: uniqueStrings([
|
|
638
|
+
bodyAvailableCount === args.messages.length ? null : "body_text",
|
|
639
|
+
requiresBodyReadBeforeDraft && replyHeaderAvailableCount === 0 ? "reply_headers" : null
|
|
640
|
+
])
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function buildRecommendation(args) {
|
|
644
|
+
const messageIds = args.messages.map((message) => message.id);
|
|
645
|
+
if (messageIds.length === 0) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
const destructive = args.destructive === true;
|
|
649
|
+
return {
|
|
650
|
+
id: args.id,
|
|
651
|
+
kind: args.kind,
|
|
652
|
+
title: args.title,
|
|
653
|
+
rationale: args.rationale,
|
|
654
|
+
operation: args.operation,
|
|
655
|
+
messageIds,
|
|
656
|
+
query: args.query ?? null,
|
|
657
|
+
labelIds: args.labelIds ?? [],
|
|
658
|
+
affectedCount: messageIds.length,
|
|
659
|
+
destructive,
|
|
660
|
+
requiresConfirmation: true,
|
|
661
|
+
confidence: args.confidence,
|
|
662
|
+
sampleMessages: args.messages.slice(0, 5).map(recommendationMessage),
|
|
663
|
+
policy: {
|
|
664
|
+
grouping: args.grouping,
|
|
665
|
+
signals: uniqueStrings(
|
|
666
|
+
args.messages.flatMap(
|
|
667
|
+
(message) => gmailPolicySignalsForMessage(message)
|
|
668
|
+
)
|
|
669
|
+
),
|
|
670
|
+
reasons: args.policyReasons,
|
|
671
|
+
exclusionReasons: args.exclusionReasons,
|
|
672
|
+
operationAllowed: args.operation !== null,
|
|
673
|
+
requiresHumanConfirmation: true,
|
|
674
|
+
emailContentIsUntrusted: true
|
|
675
|
+
},
|
|
676
|
+
contextReadiness: buildGmailRecommendationContextReadiness({
|
|
677
|
+
kind: args.kind,
|
|
678
|
+
messages: args.messages
|
|
679
|
+
})
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function buildGmailRecommendations(messages) {
|
|
683
|
+
const recommendations = [];
|
|
684
|
+
const replyMessages = messages.filter(
|
|
685
|
+
(message) => message.likelyReplyNeeded && !isGmailSpamReviewCandidate(message)
|
|
686
|
+
).slice(0, 25);
|
|
687
|
+
recommendations.push(
|
|
688
|
+
buildRecommendation({
|
|
689
|
+
id: "gmail-reply-needed",
|
|
690
|
+
kind: "reply",
|
|
691
|
+
title: "Draft replies for messages that need you",
|
|
692
|
+
rationale: "These direct unread Gmail threads look reply-worthy; spam and phishing candidates stay in review.",
|
|
693
|
+
operation: null,
|
|
694
|
+
messages: replyMessages,
|
|
695
|
+
grouping: "reply_needed",
|
|
696
|
+
policyReasons: [
|
|
697
|
+
"Only messages classified as likely reply-needed are included.",
|
|
698
|
+
"No mailbox mutation is attached; this recommendation prepares draft work only.",
|
|
699
|
+
"Full message bodies should be read before asking a model to draft replies."
|
|
700
|
+
],
|
|
701
|
+
exclusionReasons: [
|
|
702
|
+
"Spam and phishing candidates are excluded from reply drafting.",
|
|
703
|
+
"Automated, list, and promotional mail is excluded by the reply-needed classifier."
|
|
704
|
+
],
|
|
705
|
+
confidence: replyMessages.length > 0 ? 0.84 : 0
|
|
706
|
+
})
|
|
707
|
+
);
|
|
708
|
+
const archiveMessages = messages.filter(
|
|
709
|
+
(message) => hasGmailLabel(message, "INBOX") && !isGmailSpamReviewCandidate(message) && isAutomatedLowValueGmailMessage(message)
|
|
710
|
+
).slice(0, 50);
|
|
711
|
+
recommendations.push(
|
|
712
|
+
buildRecommendation({
|
|
713
|
+
id: "gmail-archive-low-value",
|
|
714
|
+
kind: "archive",
|
|
715
|
+
title: "Archive low-value automated mail",
|
|
716
|
+
rationale: "These inbox messages carry list, bulk, or promotions signals; reply-needed and spam-review messages are excluded.",
|
|
717
|
+
operation: "archive",
|
|
718
|
+
messages: archiveMessages,
|
|
719
|
+
grouping: "automated_low_value",
|
|
720
|
+
policyReasons: [
|
|
721
|
+
"Only current inbox messages are eligible for archive recommendations.",
|
|
722
|
+
"Automated, list, bulk, or promotions signals are required.",
|
|
723
|
+
"Messages that look reply-worthy are not archived by this recommendation."
|
|
724
|
+
],
|
|
725
|
+
exclusionReasons: [
|
|
726
|
+
"Spam and phishing candidates are routed to review instead of archive.",
|
|
727
|
+
"Personal or ambiguous messages without automated-mail signals are excluded."
|
|
728
|
+
],
|
|
729
|
+
confidence: archiveMessages.length > 0 ? 0.78 : 0
|
|
730
|
+
})
|
|
731
|
+
);
|
|
732
|
+
const markReadMessages = messages.filter(
|
|
733
|
+
(message) => message.isUnread && !message.isImportant && !message.likelyReplyNeeded && !isGmailSpamReviewCandidate(message) && isAutomatedLowValueGmailMessage(message)
|
|
734
|
+
).slice(0, 50);
|
|
735
|
+
recommendations.push(
|
|
736
|
+
buildRecommendation({
|
|
737
|
+
id: "gmail-mark-read-low-value",
|
|
738
|
+
kind: "mark_read",
|
|
739
|
+
title: "Mark low-value automated mail as read",
|
|
740
|
+
rationale: "These unread messages are automated or promotional; important, reply-needed, and spam-review messages are excluded.",
|
|
741
|
+
operation: "mark_read",
|
|
742
|
+
messages: markReadMessages,
|
|
743
|
+
grouping: "automated_low_value",
|
|
744
|
+
policyReasons: [
|
|
745
|
+
"Only unread automated or promotional messages are eligible.",
|
|
746
|
+
"Important and reply-needed messages remain visible to the owner.",
|
|
747
|
+
"Mark-read does not delete or move messages."
|
|
748
|
+
],
|
|
749
|
+
exclusionReasons: [
|
|
750
|
+
"Spam and phishing candidates are routed to review instead of mark-read.",
|
|
751
|
+
"Important messages and likely reply-needed threads are excluded."
|
|
752
|
+
],
|
|
753
|
+
confidence: markReadMessages.length > 0 ? 0.74 : 0
|
|
754
|
+
})
|
|
755
|
+
);
|
|
756
|
+
const spamMessages = messages.filter(isGmailSpamReviewCandidate).slice(0, 25);
|
|
757
|
+
recommendations.push(
|
|
758
|
+
buildRecommendation({
|
|
759
|
+
id: "gmail-review-spam",
|
|
760
|
+
kind: "review_spam",
|
|
761
|
+
title: "Review spam folder candidates",
|
|
762
|
+
rationale: "These messages carry Gmail spam, phishing, or upstream spam-review signals and need review before mutation.",
|
|
763
|
+
operation: null,
|
|
764
|
+
messages: spamMessages,
|
|
765
|
+
grouping: "spam_review",
|
|
766
|
+
policyReasons: [
|
|
767
|
+
"Spam and phishing signals are collected into a review-only recommendation.",
|
|
768
|
+
"No delete, trash, or report-spam operation is preselected.",
|
|
769
|
+
"Human confirmation is required before any destructive mailbox action."
|
|
770
|
+
],
|
|
771
|
+
exclusionReasons: [
|
|
772
|
+
"Spam-review candidates are excluded from archive, mark-read, and reply-draft groups."
|
|
773
|
+
],
|
|
774
|
+
destructive: false,
|
|
775
|
+
confidence: spamMessages.length > 0 ? 0.9 : 0
|
|
776
|
+
})
|
|
777
|
+
);
|
|
778
|
+
return recommendations.filter(
|
|
779
|
+
(recommendation) => recommendation !== null
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
function summarizeGmailRecommendations(recommendations) {
|
|
783
|
+
return {
|
|
784
|
+
totalCount: recommendations.length,
|
|
785
|
+
replyCount: recommendations.filter(
|
|
786
|
+
(recommendation) => recommendation.kind === "reply"
|
|
787
|
+
).length,
|
|
788
|
+
archiveCount: recommendations.filter(
|
|
789
|
+
(recommendation) => recommendation.kind === "archive"
|
|
790
|
+
).length,
|
|
791
|
+
markReadCount: recommendations.filter(
|
|
792
|
+
(recommendation) => recommendation.kind === "mark_read"
|
|
793
|
+
).length,
|
|
794
|
+
spamReviewCount: recommendations.filter(
|
|
795
|
+
(recommendation) => recommendation.kind === "review_spam"
|
|
796
|
+
).length,
|
|
797
|
+
destructiveCount: recommendations.filter(
|
|
798
|
+
(recommendation) => recommendation.destructive
|
|
799
|
+
).length
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
import { wrapUntrustedEmailContent } from "@elizaos/shared";
|
|
803
|
+
function buildFallbackGmailReplyDraftBody(args) {
|
|
804
|
+
const recipientLabel = args.message.from.split("<")[0]?.trim() || args.message.fromEmail || "";
|
|
805
|
+
const greeting = recipientLabel ? `${recipientLabel},` : "";
|
|
806
|
+
const subject = args.message.subject.trim() || "your message";
|
|
807
|
+
const bodyCore = args.intent?.trim() ? args.intent.trim() : args.tone === "brief" ? `Thanks for the note about ${subject}. I saw it and will follow up shortly.` : args.tone === "warm" ? `Thanks for reaching out about ${subject}. I reviewed your note and wanted to follow up.` : `Thanks for the note about ${subject}. I reviewed your message and wanted to follow up.`;
|
|
808
|
+
const bodyLines = [greeting, bodyCore, args.senderName].filter(
|
|
809
|
+
(line) => line.trim().length > 0
|
|
810
|
+
);
|
|
811
|
+
if (args.includeQuotedOriginal && args.message.snippet.trim().length > 0) {
|
|
812
|
+
bodyLines.push(
|
|
813
|
+
"",
|
|
814
|
+
...args.message.snippet.trim().split("\n").map((line) => `> ${line.trim()}`)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return bodyLines.join("\n");
|
|
818
|
+
}
|
|
819
|
+
function normalizeGeneratedGmailReplyDraftBody(value) {
|
|
820
|
+
const withoutThink = value.replace(/<think>[\s\S]*?<\/think>/gi, " ").trim();
|
|
821
|
+
if (!withoutThink) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const withoutCodeFences = withoutThink.replace(/^```[a-z0-9_-]*\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
825
|
+
const withoutSubject = withoutCodeFences.replace(/^subject:\s*.+\n+/i, "");
|
|
826
|
+
const normalized = withoutSubject.replace(/\r\n/g, "\n").replace(/^["'`]+|["'`]+$/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
827
|
+
return normalized.length > 0 ? normalized : null;
|
|
828
|
+
}
|
|
829
|
+
function buildGmailReplyPreviewLines(bodyText) {
|
|
830
|
+
const lines = bodyText.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).slice(0, 3);
|
|
831
|
+
return lines.length > 0 ? lines : [bodyText.trim()].filter(Boolean);
|
|
832
|
+
}
|
|
833
|
+
function buildGmailReplyDraft(args) {
|
|
834
|
+
const recipient = args.message.replyTo ?? args.message.fromEmail ?? null;
|
|
835
|
+
if (!recipient) {
|
|
836
|
+
fail(409, "The selected Gmail message has no replyable sender.");
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
messageId: args.message.id,
|
|
840
|
+
threadId: args.message.threadId,
|
|
841
|
+
subject: args.message.subject,
|
|
842
|
+
to: [recipient.toLowerCase()],
|
|
843
|
+
cc: [],
|
|
844
|
+
bodyText: args.bodyText,
|
|
845
|
+
previewLines: buildGmailReplyPreviewLines(args.bodyText),
|
|
846
|
+
sendAllowed: args.sendAllowed,
|
|
847
|
+
requiresConfirmation: true
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function createCalendarEventId(agentId, provider, side, calendarId, externalId) {
|
|
851
|
+
const digest = crypto.createHash("sha256").update(`${agentId}:${provider}:${side}:${calendarId}:${externalId}`).digest("hex");
|
|
852
|
+
return `life-calendar-${digest.slice(0, 32)}`;
|
|
853
|
+
}
|
|
854
|
+
function createGmailMessageId(agentId, provider, side, grantId, externalMessageId) {
|
|
855
|
+
const digest = crypto.createHash("sha256").update(
|
|
856
|
+
`${agentId}:${provider}:${side}:gmail:${grantId}:${externalMessageId}`
|
|
857
|
+
).digest("hex");
|
|
858
|
+
return `life-gmail-${digest.slice(0, 32)}`;
|
|
859
|
+
}
|
|
860
|
+
function createGmailSpamReviewItemId(agentId, provider, side, grantId, externalMessageId) {
|
|
861
|
+
const digest = crypto.createHash("sha256").update(
|
|
862
|
+
`${agentId}:${provider}:${side}:gmail-spam-review:${grantId}:${externalMessageId}`
|
|
863
|
+
).digest("hex");
|
|
864
|
+
return `life-gmail-spam-${digest.slice(0, 32)}`;
|
|
865
|
+
}
|
|
866
|
+
function materializeGmailMessageSummary(args) {
|
|
867
|
+
return {
|
|
868
|
+
id: createGmailMessageId(
|
|
869
|
+
args.agentId,
|
|
870
|
+
"google",
|
|
871
|
+
args.side,
|
|
872
|
+
args.grantId,
|
|
873
|
+
args.message.externalId
|
|
874
|
+
),
|
|
875
|
+
agentId: args.agentId,
|
|
876
|
+
provider: "google",
|
|
877
|
+
side: args.side,
|
|
878
|
+
...args.message,
|
|
879
|
+
grantId: args.grantId,
|
|
880
|
+
accountEmail: args.accountEmail ?? void 0,
|
|
881
|
+
syncedAt: args.syncedAt,
|
|
882
|
+
updatedAt: args.syncedAt
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function isCalendarSyncStateFresh(args) {
|
|
886
|
+
const syncedAtMs = Date.parse(args.syncedAt);
|
|
887
|
+
if (!Number.isFinite(syncedAtMs)) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
if (args.now.getTime() - syncedAtMs > GOOGLE_CALENDAR_CACHE_TTL_MS) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
return Date.parse(args.windowStartAt) <= Date.parse(args.timeMin) && Date.parse(args.windowEndAt) >= Date.parse(args.timeMax);
|
|
894
|
+
}
|
|
895
|
+
export {
|
|
896
|
+
buildFallbackGmailReplyDraftBody,
|
|
897
|
+
buildGmailRecommendations,
|
|
898
|
+
buildGmailReplyDraft,
|
|
899
|
+
buildGmailReplyPreviewLines,
|
|
900
|
+
buildGmailSpamReviewItem,
|
|
901
|
+
collectCalendarEventContactEmails,
|
|
902
|
+
compareGmailMessagePriority,
|
|
903
|
+
createCalendarEventId,
|
|
904
|
+
createGmailMessageId,
|
|
905
|
+
createGmailSpamReviewItemId,
|
|
906
|
+
extractNormalizedEmailAddress,
|
|
907
|
+
extractSubjectTokens,
|
|
908
|
+
filterGmailMessagesBySearch,
|
|
909
|
+
findLinkedMailForCalendarEvent,
|
|
910
|
+
isCalendarSyncStateFresh,
|
|
911
|
+
isGmailSpamReviewCandidate,
|
|
912
|
+
isGmailSyncStateFresh,
|
|
913
|
+
materializeGmailMessageSummary,
|
|
914
|
+
normalizeGeneratedGmailReplyDraftBody,
|
|
915
|
+
normalizeGmailBulkOperation,
|
|
916
|
+
normalizeGmailDraftTone,
|
|
917
|
+
normalizeGmailReplyBody,
|
|
918
|
+
normalizeGmailSearchQuery,
|
|
919
|
+
normalizeGmailSearchQueryMatches,
|
|
920
|
+
normalizeGmailSpamReviewStatus,
|
|
921
|
+
normalizeGmailUnrespondedOlderThanDays,
|
|
922
|
+
normalizeOptionalGmailLabelIdArray,
|
|
923
|
+
normalizeOptionalMessageIdArray,
|
|
924
|
+
normalizeOptionalStringArray,
|
|
925
|
+
parseGmailDateBoundary,
|
|
926
|
+
parseGmailRelativeDuration,
|
|
927
|
+
splitMailboxLikeList,
|
|
928
|
+
summarizeGmailBatchReplyDrafts,
|
|
929
|
+
summarizeGmailNeedsResponse,
|
|
930
|
+
summarizeGmailRecommendations,
|
|
931
|
+
summarizeGmailSearch,
|
|
932
|
+
summarizeGmailSpamReviewItems,
|
|
933
|
+
summarizeGmailTriage,
|
|
934
|
+
summarizeGmailUnresponded,
|
|
935
|
+
wrapUntrustedEmailContent
|
|
936
|
+
};
|
|
937
|
+
//# sourceMappingURL=gmail-normalize.js.map
|