@oratis/lisa 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +2 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +20 -1
- package/dist/agent.js.map +1 -1
- package/dist/autonomy/state.d.ts +14 -0
- package/dist/autonomy/state.d.ts.map +1 -0
- package/dist/autonomy/state.js +54 -0
- package/dist/autonomy/state.js.map +1 -0
- package/dist/cli/mail.d.ts +2 -0
- package/dist/cli/mail.d.ts.map +1 -0
- package/dist/cli/mail.js +209 -0
- package/dist/cli/mail.js.map +1 -0
- package/dist/cli/pair.d.ts +1 -5
- package/dist/cli/pair.d.ts.map +1 -1
- package/dist/cli/pair.js +58 -20
- package/dist/cli/pair.js.map +1 -1
- package/dist/cli.js +15 -14
- package/dist/cli.js.map +1 -1
- package/dist/consent/store.d.ts +2 -2
- package/dist/consent/store.d.ts.map +1 -1
- package/dist/consent/store.js +3 -2
- package/dist/consent/store.js.map +1 -1
- package/dist/edition.d.ts +24 -0
- package/dist/edition.d.ts.map +1 -0
- package/dist/edition.js +19 -0
- package/dist/edition.js.map +1 -0
- package/dist/heartbeat/runner.d.ts.map +1 -1
- package/dist/heartbeat/runner.js +8 -0
- package/dist/heartbeat/runner.js.map +1 -1
- package/dist/idle/runner.d.ts.map +1 -1
- package/dist/idle/runner.js +7 -0
- package/dist/idle/runner.js.map +1 -1
- package/dist/mail/accounts.d.ts +20 -0
- package/dist/mail/accounts.d.ts.map +1 -0
- package/dist/mail/accounts.js +113 -0
- package/dist/mail/accounts.js.map +1 -0
- package/dist/mail/alerts.d.ts +29 -0
- package/dist/mail/alerts.d.ts.map +1 -0
- package/dist/mail/alerts.js +40 -0
- package/dist/mail/alerts.js.map +1 -0
- package/dist/mail/classify.d.ts +26 -0
- package/dist/mail/classify.d.ts.map +1 -0
- package/dist/mail/classify.js +191 -0
- package/dist/mail/classify.js.map +1 -0
- package/dist/mail/connectors/gmail.d.ts +55 -0
- package/dist/mail/connectors/gmail.d.ts.map +1 -0
- package/dist/mail/connectors/gmail.js +87 -0
- package/dist/mail/connectors/gmail.js.map +1 -0
- package/dist/mail/connectors/imap.d.ts +13 -0
- package/dist/mail/connectors/imap.d.ts.map +1 -0
- package/dist/mail/connectors/imap.js +136 -0
- package/dist/mail/connectors/imap.js.map +1 -0
- package/dist/mail/digest.d.ts +21 -0
- package/dist/mail/digest.d.ts.map +1 -0
- package/dist/mail/digest.js +89 -0
- package/dist/mail/digest.js.map +1 -0
- package/dist/mail/google-oauth.d.ts +50 -0
- package/dist/mail/google-oauth.d.ts.map +1 -0
- package/dist/mail/google-oauth.js +74 -0
- package/dist/mail/google-oauth.js.map +1 -0
- package/dist/mail/hosts.d.ts +3 -0
- package/dist/mail/hosts.d.ts.map +1 -0
- package/dist/mail/hosts.js +19 -0
- package/dist/mail/hosts.js.map +1 -0
- package/dist/mail/scheduler.d.ts +9 -0
- package/dist/mail/scheduler.d.ts.map +1 -0
- package/dist/mail/scheduler.js +21 -0
- package/dist/mail/scheduler.js.map +1 -0
- package/dist/mail/service.d.ts +32 -0
- package/dist/mail/service.d.ts.map +1 -0
- package/dist/mail/service.js +129 -0
- package/dist/mail/service.js.map +1 -0
- package/dist/mail/store.d.ts +7 -0
- package/dist/mail/store.d.ts.map +1 -0
- package/dist/mail/store.js +58 -0
- package/dist/mail/store.js.map +1 -0
- package/dist/mail/types.d.ts +114 -0
- package/dist/mail/types.d.ts.map +1 -0
- package/dist/mail/types.js +26 -0
- package/dist/mail/types.js.map +1 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +43 -21
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +67 -64
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +77 -71
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/registry.d.ts +10 -0
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +26 -0
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/stream-retry.d.ts +37 -0
- package/dist/providers/stream-retry.d.ts.map +1 -0
- package/dist/providers/stream-retry.js +92 -0
- package/dist/providers/stream-retry.js.map +1 -0
- package/dist/providers/types.d.ts +3 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/subagent.d.ts +2 -0
- package/dist/subagent.d.ts.map +1 -1
- package/dist/subagent.js +1 -0
- package/dist/subagent.js.map +1 -1
- package/dist/web/cloudAuth.d.ts +57 -0
- package/dist/web/cloudAuth.d.ts.map +1 -0
- package/dist/web/cloudAuth.js +139 -0
- package/dist/web/cloudAuth.js.map +1 -0
- package/dist/web/island.d.ts.map +1 -1
- package/dist/web/island.js +1 -0
- package/dist/web/island.js.map +1 -1
- package/dist/web/lisa-client.d.ts +1 -1
- package/dist/web/lisa-client.d.ts.map +1 -1
- package/dist/web/lisa-client.js +703 -40
- package/dist/web/lisa-client.js.map +1 -1
- package/dist/web/lisa-css.d.ts +1 -1
- package/dist/web/lisa-css.d.ts.map +1 -1
- package/dist/web/lisa-css.js +568 -43
- package/dist/web/lisa-css.js.map +1 -1
- package/dist/web/lisa-html.d.ts +1 -1
- package/dist/web/lisa-html.d.ts.map +1 -1
- package/dist/web/lisa-html.js +71 -21
- package/dist/web/lisa-html.js.map +1 -1
- package/dist/web/pairing.d.ts +37 -0
- package/dist/web/pairing.d.ts.map +1 -0
- package/dist/web/pairing.js +79 -0
- package/dist/web/pairing.js.map +1 -0
- package/dist/web/push.d.ts +12 -1
- package/dist/web/push.d.ts.map +1 -1
- package/dist/web/push.js +19 -4
- package/dist/web/push.js.map +1 -1
- package/dist/web/qr-svg.d.ts +11 -0
- package/dist/web/qr-svg.d.ts.map +1 -0
- package/dist/web/qr-svg.js +41 -0
- package/dist/web/qr-svg.js.map +1 -0
- package/dist/web/server.d.ts +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +342 -12
- package/dist/web/server.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IMAP connector (imapflow) — app-password / authorization-code auth.
|
|
3
|
+
*
|
|
4
|
+
* Fetches ENVELOPE + flags + a BOUNDED text snippet only (≤ ~4 KB downloaded,
|
|
5
|
+
* truncated further), never whole bodies — the privacy contract. Read-only:
|
|
6
|
+
* opens the mailbox read-only and never sets flags / moves / deletes.
|
|
7
|
+
*/
|
|
8
|
+
import { ImapFlow } from "imapflow";
|
|
9
|
+
const SNIPPET_FETCH_BYTES = 4096;
|
|
10
|
+
const SNIPPET_CHARS = 400;
|
|
11
|
+
/** First text/plain (preferred) else text/html leaf part id, or null. Pure-ish. */
|
|
12
|
+
function findTextPart(node) {
|
|
13
|
+
if (!node)
|
|
14
|
+
return null;
|
|
15
|
+
const stack = [node];
|
|
16
|
+
let htmlFallback = null;
|
|
17
|
+
while (stack.length) {
|
|
18
|
+
const n = stack.shift();
|
|
19
|
+
const type = (n.type ?? "").toLowerCase();
|
|
20
|
+
if (n.part && type === "text/plain")
|
|
21
|
+
return { part: n.part, html: false };
|
|
22
|
+
if (n.part && type === "text/html" && !htmlFallback)
|
|
23
|
+
htmlFallback = { part: n.part, html: true };
|
|
24
|
+
if (n.childNodes)
|
|
25
|
+
stack.push(...n.childNodes);
|
|
26
|
+
}
|
|
27
|
+
return htmlFallback;
|
|
28
|
+
}
|
|
29
|
+
function stripHtml(s) {
|
|
30
|
+
return s
|
|
31
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
32
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
33
|
+
.replace(/<[^>]+>/g, " ")
|
|
34
|
+
.replace(/ /gi, " ")
|
|
35
|
+
.replace(/&/gi, "&")
|
|
36
|
+
.replace(/</gi, "<")
|
|
37
|
+
.replace(/>/gi, ">");
|
|
38
|
+
}
|
|
39
|
+
async function streamToString(stream, maxBytes) {
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let total = 0;
|
|
42
|
+
for await (const chunk of stream) {
|
|
43
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
44
|
+
chunks.push(buf);
|
|
45
|
+
total += buf.length;
|
|
46
|
+
if (total >= maxBytes)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
50
|
+
}
|
|
51
|
+
export class ImapConnector {
|
|
52
|
+
client;
|
|
53
|
+
connected = false;
|
|
54
|
+
constructor(account, secret) {
|
|
55
|
+
if (!account.host)
|
|
56
|
+
throw new Error(`imap account ${account.id} has no host`);
|
|
57
|
+
if (!secret.password)
|
|
58
|
+
throw new Error(`imap account ${account.id} has no password`);
|
|
59
|
+
const port = account.port ?? 993;
|
|
60
|
+
this.client = new ImapFlow({
|
|
61
|
+
host: account.host,
|
|
62
|
+
port,
|
|
63
|
+
secure: port === 993,
|
|
64
|
+
auth: { user: account.email, pass: secret.password },
|
|
65
|
+
logger: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async listSince(opts) {
|
|
69
|
+
if (!this.connected) {
|
|
70
|
+
await this.client.connect();
|
|
71
|
+
this.connected = true;
|
|
72
|
+
}
|
|
73
|
+
const lock = await this.client.getMailboxLock("INBOX", { readOnly: true });
|
|
74
|
+
const out = [];
|
|
75
|
+
try {
|
|
76
|
+
const found = await this.client.search({ since: new Date(opts.sinceMs) }, { uid: true });
|
|
77
|
+
const uids = (Array.isArray(found) ? found : []).slice(-opts.limit);
|
|
78
|
+
if (uids.length === 0)
|
|
79
|
+
return [];
|
|
80
|
+
for await (const msg of this.client.fetch(uids, { uid: true, envelope: true, flags: true, bodyStructure: true }, { uid: true })) {
|
|
81
|
+
out.push(await this.toRaw(msg, opts));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
lock.release();
|
|
86
|
+
}
|
|
87
|
+
out.sort((a, b) => b.date - a.date);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
async toRaw(msg, _opts) {
|
|
91
|
+
const env = msg.envelope ?? {};
|
|
92
|
+
const f = env.from?.[0] ?? {};
|
|
93
|
+
const address = f.address ?? "";
|
|
94
|
+
const from = f.name ? `${f.name} <${address}>` : address;
|
|
95
|
+
let snippet = "";
|
|
96
|
+
const part = findTextPart(msg.bodyStructure);
|
|
97
|
+
if (part) {
|
|
98
|
+
try {
|
|
99
|
+
const dl = await this.client.download(String(msg.uid), part.part, {
|
|
100
|
+
uid: true,
|
|
101
|
+
maxBytes: SNIPPET_FETCH_BYTES,
|
|
102
|
+
});
|
|
103
|
+
let text = await streamToString(dl.content, SNIPPET_FETCH_BYTES);
|
|
104
|
+
if (part.html)
|
|
105
|
+
text = stripHtml(text);
|
|
106
|
+
snippet = text.replace(/\s+/g, " ").trim().slice(0, SNIPPET_CHARS);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
snippet = "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
uid: String(msg.uid),
|
|
114
|
+
accountId: "", // filled by the caller (service) — connector is account-agnostic
|
|
115
|
+
from,
|
|
116
|
+
fromAddress: address,
|
|
117
|
+
subject: env.subject ?? "",
|
|
118
|
+
date: env.date ? new Date(env.date).getTime() : Date.now(),
|
|
119
|
+
snippet,
|
|
120
|
+
flags: msg.flags ? [...msg.flags] : [],
|
|
121
|
+
mailbox: "INBOX",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async close() {
|
|
125
|
+
if (!this.connected)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
await this.client.logout();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* already closed */
|
|
132
|
+
}
|
|
133
|
+
this.connected = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=imap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imap.js","sourceRoot":"","sources":["../../../src/mail/connectors/imap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily digest builder — pure. Turns classified MailItems into a DailyDigest:
|
|
3
|
+
* counts, per-category buckets, the "needs you" list, and a templated summary.
|
|
4
|
+
* (The service may replace `summary` with a model-written narrative.)
|
|
5
|
+
*/
|
|
6
|
+
import { type DailyDigest, type DigestBucket, type MailCategory, type MailItem } from "./types.js";
|
|
7
|
+
export interface BuildDigestOpts {
|
|
8
|
+
date: string;
|
|
9
|
+
accountIds: string[];
|
|
10
|
+
/** Count of unread among the swept set; defaults to total. */
|
|
11
|
+
unread?: number;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildDigest(items: MailItem[], opts: BuildDigestOpts): DailyDigest;
|
|
15
|
+
/** Deterministic one-paragraph summary. Pure. */
|
|
16
|
+
export declare function templateSummary(total: number, needsYou: MailItem[], buckets: DigestBucket[]): string;
|
|
17
|
+
/** Format a digest as a compact plain-text message (push body / chat / CLI). Pure. */
|
|
18
|
+
export declare function formatDigestText(d: DailyDigest): string;
|
|
19
|
+
/** A category is "actionable" if items in it usually want the user. */
|
|
20
|
+
export declare function isActionableCategory(c: MailCategory): boolean;
|
|
21
|
+
//# sourceMappingURL=digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/mail/digest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACd,MAAM,YAAY,CAAC;AAWpB,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,eAAe,GAAG,WAAW,CA+BjF;AAED,iDAAiD;AACjD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAmBpG;AAOD,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CASvD;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,YAAY,GAAG,OAAO,CAE7D"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily digest builder — pure. Turns classified MailItems into a DailyDigest:
|
|
3
|
+
* counts, per-category buckets, the "needs you" list, and a templated summary.
|
|
4
|
+
* (The service may replace `summary` with a model-written narrative.)
|
|
5
|
+
*/
|
|
6
|
+
import { MAIL_CATEGORIES, } from "./types.js";
|
|
7
|
+
const BUCKET_SAMPLE = 3;
|
|
8
|
+
const NEEDS_YOU_CAP = 12;
|
|
9
|
+
/** importance desc, then newest first. */
|
|
10
|
+
function byImportanceThenDate(a, b) {
|
|
11
|
+
if (b.importance !== a.importance)
|
|
12
|
+
return b.importance - a.importance;
|
|
13
|
+
return b.date - a.date;
|
|
14
|
+
}
|
|
15
|
+
export function buildDigest(items, opts) {
|
|
16
|
+
const now = opts.now ?? Date.now;
|
|
17
|
+
const sorted = [...items].sort(byImportanceThenDate);
|
|
18
|
+
const needsYou = sorted.filter((i) => i.importance >= 2).slice(0, NEEDS_YOU_CAP);
|
|
19
|
+
const buckets = [];
|
|
20
|
+
for (const category of MAIL_CATEGORIES) {
|
|
21
|
+
const inCat = sorted.filter((i) => i.category === category);
|
|
22
|
+
if (inCat.length === 0)
|
|
23
|
+
continue;
|
|
24
|
+
buckets.push({ category, count: inCat.length, items: inCat.slice(0, BUCKET_SAMPLE) });
|
|
25
|
+
}
|
|
26
|
+
// Most-populous buckets first, but always surface action-y categories near the top.
|
|
27
|
+
const priority = { urgent: 0, security: 1, personal: 2, finance: 3, calendar: 4, work: 5 };
|
|
28
|
+
buckets.sort((a, b) => {
|
|
29
|
+
const pa = priority[a.category] ?? 9;
|
|
30
|
+
const pb = priority[b.category] ?? 9;
|
|
31
|
+
if (pa !== pb)
|
|
32
|
+
return pa - pb;
|
|
33
|
+
return b.count - a.count;
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
date: opts.date,
|
|
37
|
+
generatedAt: now(),
|
|
38
|
+
accountIds: [...opts.accountIds],
|
|
39
|
+
total: items.length,
|
|
40
|
+
unread: opts.unread ?? items.length,
|
|
41
|
+
needsYou,
|
|
42
|
+
buckets,
|
|
43
|
+
summary: templateSummary(items.length, needsYou, buckets),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Deterministic one-paragraph summary. Pure. */
|
|
47
|
+
export function templateSummary(total, needsYou, buckets) {
|
|
48
|
+
if (total === 0)
|
|
49
|
+
return "No new mail.";
|
|
50
|
+
const parts = [`${total} new email${total === 1 ? "" : "s"}.`];
|
|
51
|
+
if (needsYou.length > 0) {
|
|
52
|
+
const top = needsYou
|
|
53
|
+
.slice(0, 3)
|
|
54
|
+
.map((i) => `“${i.subject.slice(0, 48)}” (${shortFrom(i.from)})`)
|
|
55
|
+
.join("; ");
|
|
56
|
+
parts.push(`${needsYou.length} need${needsYou.length === 1 ? "s" : ""} you: ${top}.`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
parts.push("Nothing needs your attention.");
|
|
60
|
+
}
|
|
61
|
+
const breakdown = buckets
|
|
62
|
+
.filter((b) => !["urgent", "personal"].includes(b.category) || b.count > 0)
|
|
63
|
+
.slice(0, 6)
|
|
64
|
+
.map((b) => `${b.category} ${b.count}`)
|
|
65
|
+
.join(" · ");
|
|
66
|
+
if (breakdown)
|
|
67
|
+
parts.push(`Breakdown: ${breakdown}.`);
|
|
68
|
+
return parts.join(" ");
|
|
69
|
+
}
|
|
70
|
+
function shortFrom(from) {
|
|
71
|
+
const m = from.match(/^\s*"?([^"<]+?)"?\s*</);
|
|
72
|
+
return (m?.[1] ?? from).trim().slice(0, 32);
|
|
73
|
+
}
|
|
74
|
+
/** Format a digest as a compact plain-text message (push body / chat / CLI). Pure. */
|
|
75
|
+
export function formatDigestText(d) {
|
|
76
|
+
const lines = [`📬 Mail digest · ${d.date}`, d.summary];
|
|
77
|
+
if (d.needsYou.length) {
|
|
78
|
+
lines.push("", "Needs you:");
|
|
79
|
+
for (const i of d.needsYou.slice(0, 5)) {
|
|
80
|
+
lines.push(` • [${i.importance === 3 ? "‼" : "!"}] ${i.subject.slice(0, 60)} — ${shortFrom(i.from)}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
/** A category is "actionable" if items in it usually want the user. */
|
|
86
|
+
export function isActionableCategory(c) {
|
|
87
|
+
return c === "urgent" || c === "personal" || c === "calendar" || c === "finance" || c === "work";
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/mail/digest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 for Gmail (read-only) — no SDK, just fetch + the documented
|
|
3
|
+
* token endpoints. Used by the loopback "installed app" flow (CLI) and by the
|
|
4
|
+
* Gmail connector's token refresh.
|
|
5
|
+
*
|
|
6
|
+
* Setup the user does once: create an OAuth client (type "Desktop app") in a
|
|
7
|
+
* Google Cloud project, enable the Gmail API, and provide the client id/secret
|
|
8
|
+
* (LISA_GOOGLE_CLIENT_ID / _SECRET, or --client-id/--client-secret). Tokens are
|
|
9
|
+
* stored per-account in the 0600 secrets file; only the gmail.readonly scope is
|
|
10
|
+
* requested.
|
|
11
|
+
*/
|
|
12
|
+
export declare const GMAIL_SCOPE = "https://www.googleapis.com/auth/gmail.readonly";
|
|
13
|
+
export interface GoogleTokens {
|
|
14
|
+
accessToken: string;
|
|
15
|
+
/** Present on the initial exchange (access_type=offline); absent on refresh. */
|
|
16
|
+
refreshToken?: string;
|
|
17
|
+
/** Absolute expiry, epoch ms. */
|
|
18
|
+
expiry: number;
|
|
19
|
+
}
|
|
20
|
+
export type FetchLike = (url: string, init: {
|
|
21
|
+
method: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
body: string;
|
|
24
|
+
}) => Promise<{
|
|
25
|
+
ok: boolean;
|
|
26
|
+
status: number;
|
|
27
|
+
text(): Promise<string>;
|
|
28
|
+
}>;
|
|
29
|
+
/** Build the consent URL. Pure. */
|
|
30
|
+
export declare function buildAuthUrl(o: {
|
|
31
|
+
clientId: string;
|
|
32
|
+
redirectUri: string;
|
|
33
|
+
state: string;
|
|
34
|
+
}): string;
|
|
35
|
+
/** True if the access token is expired (60s skew). Pure. */
|
|
36
|
+
export declare function tokenExpired(expiry: number, now: number): boolean;
|
|
37
|
+
/** Exchange an auth code for tokens. */
|
|
38
|
+
export declare function exchangeCode(o: {
|
|
39
|
+
code: string;
|
|
40
|
+
clientId: string;
|
|
41
|
+
clientSecret: string;
|
|
42
|
+
redirectUri: string;
|
|
43
|
+
}, fetchImpl?: FetchLike, now?: number): Promise<GoogleTokens>;
|
|
44
|
+
/** Refresh an access token (refresh_token is reused, not returned). */
|
|
45
|
+
export declare function refreshAccessToken(o: {
|
|
46
|
+
refreshToken: string;
|
|
47
|
+
clientId: string;
|
|
48
|
+
clientSecret: string;
|
|
49
|
+
}, fetchImpl?: FetchLike, now?: number): Promise<GoogleTokens>;
|
|
50
|
+
//# sourceMappingURL=google-oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-oauth.d.ts","sourceRoot":"","sources":["../../src/mail/google-oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,mDAAmD,CAAC;AAI5E,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAEvE,mCAAmC;AACnC,wBAAgB,YAAY,CAAC,CAAC,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAWhG;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEjE;AAwBD,wCAAwC;AACxC,wBAAsB,YAAY,CAChC,CAAC,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAChF,SAAS,GAAE,SAAyC,EACpD,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,YAAY,CAAC,CAYvB;AAED,uEAAuE;AACvE,wBAAsB,kBAAkB,CACtC,CAAC,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EACnE,SAAS,GAAE,SAAyC,EACpD,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,YAAY,CAAC,CAYvB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google OAuth2 for Gmail (read-only) — no SDK, just fetch + the documented
|
|
3
|
+
* token endpoints. Used by the loopback "installed app" flow (CLI) and by the
|
|
4
|
+
* Gmail connector's token refresh.
|
|
5
|
+
*
|
|
6
|
+
* Setup the user does once: create an OAuth client (type "Desktop app") in a
|
|
7
|
+
* Google Cloud project, enable the Gmail API, and provide the client id/secret
|
|
8
|
+
* (LISA_GOOGLE_CLIENT_ID / _SECRET, or --client-id/--client-secret). Tokens are
|
|
9
|
+
* stored per-account in the 0600 secrets file; only the gmail.readonly scope is
|
|
10
|
+
* requested.
|
|
11
|
+
*/
|
|
12
|
+
export const GMAIL_SCOPE = "https://www.googleapis.com/auth/gmail.readonly";
|
|
13
|
+
const AUTH_BASE = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
14
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
15
|
+
/** Build the consent URL. Pure. */
|
|
16
|
+
export function buildAuthUrl(o) {
|
|
17
|
+
const q = new URLSearchParams({
|
|
18
|
+
client_id: o.clientId,
|
|
19
|
+
redirect_uri: o.redirectUri,
|
|
20
|
+
response_type: "code",
|
|
21
|
+
scope: GMAIL_SCOPE,
|
|
22
|
+
access_type: "offline",
|
|
23
|
+
prompt: "consent", // force a refresh_token even on re-auth
|
|
24
|
+
state: o.state,
|
|
25
|
+
});
|
|
26
|
+
return `${AUTH_BASE}?${q.toString()}`;
|
|
27
|
+
}
|
|
28
|
+
/** True if the access token is expired (60s skew). Pure. */
|
|
29
|
+
export function tokenExpired(expiry, now) {
|
|
30
|
+
return now >= expiry - 60_000;
|
|
31
|
+
}
|
|
32
|
+
function parseTokens(json, now) {
|
|
33
|
+
const accessToken = String(json.access_token ?? "");
|
|
34
|
+
if (!accessToken)
|
|
35
|
+
throw new Error("no access_token in token response");
|
|
36
|
+
const ttl = typeof json.expires_in === "number" ? json.expires_in : 3600;
|
|
37
|
+
return {
|
|
38
|
+
accessToken,
|
|
39
|
+
refreshToken: typeof json.refresh_token === "string" ? json.refresh_token : undefined,
|
|
40
|
+
expiry: now + ttl * 1000,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function postToken(body, fetchImpl, now) {
|
|
44
|
+
const res = await fetchImpl(TOKEN_URL, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
47
|
+
body: body.toString(),
|
|
48
|
+
});
|
|
49
|
+
const text = await res.text();
|
|
50
|
+
if (!res.ok)
|
|
51
|
+
throw new Error(`google token endpoint ${res.status}: ${text.slice(0, 200)}`);
|
|
52
|
+
return parseTokens(JSON.parse(text), now);
|
|
53
|
+
}
|
|
54
|
+
/** Exchange an auth code for tokens. */
|
|
55
|
+
export async function exchangeCode(o, fetchImpl = fetch, now = Date.now()) {
|
|
56
|
+
return postToken(new URLSearchParams({
|
|
57
|
+
code: o.code,
|
|
58
|
+
client_id: o.clientId,
|
|
59
|
+
client_secret: o.clientSecret,
|
|
60
|
+
redirect_uri: o.redirectUri,
|
|
61
|
+
grant_type: "authorization_code",
|
|
62
|
+
}), fetchImpl, now);
|
|
63
|
+
}
|
|
64
|
+
/** Refresh an access token (refresh_token is reused, not returned). */
|
|
65
|
+
export async function refreshAccessToken(o, fetchImpl = fetch, now = Date.now()) {
|
|
66
|
+
const t = await postToken(new URLSearchParams({
|
|
67
|
+
refresh_token: o.refreshToken,
|
|
68
|
+
client_id: o.clientId,
|
|
69
|
+
client_secret: o.clientSecret,
|
|
70
|
+
grant_type: "refresh_token",
|
|
71
|
+
}), fetchImpl, now);
|
|
72
|
+
return { ...t, refreshToken: t.refreshToken ?? o.refreshToken };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=google-oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"google-oauth.js","sourceRoot":"","sources":["../../src/mail/google-oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosts.d.ts","sourceRoot":"","sources":["../../src/mail/hosts.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAgB3D"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Infer the IMAP host from an email domain (common providers). Pure. */
|
|
2
|
+
export function inferHost(email) {
|
|
3
|
+
const domain = (email.split("@")[1] ?? "").toLowerCase();
|
|
4
|
+
const map = {
|
|
5
|
+
"qq.com": "imap.qq.com",
|
|
6
|
+
"163.com": "imap.163.com",
|
|
7
|
+
"126.com": "imap.126.com",
|
|
8
|
+
"gmail.com": "imap.gmail.com",
|
|
9
|
+
"googlemail.com": "imap.gmail.com",
|
|
10
|
+
"outlook.com": "outlook.office365.com",
|
|
11
|
+
"hotmail.com": "outlook.office365.com",
|
|
12
|
+
"live.com": "outlook.office365.com",
|
|
13
|
+
"icloud.com": "imap.mail.me.com",
|
|
14
|
+
"me.com": "imap.mail.me.com",
|
|
15
|
+
"yahoo.com": "imap.mail.yahoo.com",
|
|
16
|
+
};
|
|
17
|
+
return map[domain] ?? (domain ? `imap.${domain}` : undefined);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=hosts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hosts.js","sourceRoot":"","sources":["../../src/mail/hosts.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACzD,MAAM,GAAG,GAA2B;QAClC,QAAQ,EAAE,aAAa;QACvB,SAAS,EAAE,cAAc;QACzB,SAAS,EAAE,cAAc;QACzB,WAAW,EAAE,gBAAgB;QAC7B,gBAAgB,EAAE,gBAAgB;QAClC,aAAa,EAAE,uBAAuB;QACtC,aAAa,EAAE,uBAAuB;QACtC,UAAU,EAAE,uBAAuB;QACnC,YAAY,EAAE,kBAAkB;QAChC,QAAQ,EAAE,kBAAkB;QAC5B,WAAW,EAAE,qBAAqB;KACnC,CAAC;IACF,OAAO,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_DIGEST_HOUR = 8;
|
|
2
|
+
/** Target local hour (0-23) for the daily digest; LISA_MAIL_DIGEST_HOUR overrides. */
|
|
3
|
+
export declare function digestHour(env?: NodeJS.ProcessEnv): number;
|
|
4
|
+
/**
|
|
5
|
+
* Is a daily digest due? True when we haven't produced one for the local day yet
|
|
6
|
+
* AND it's at/after the target hour. Pure.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isDigestDue(lastDigestDate: string | null, now: Date, targetHour: number): boolean;
|
|
9
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/mail/scheduler.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAGvE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAGjG"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily-digest scheduling — pure decision logic. The server polls this on a
|
|
3
|
+
* timer; the actual sweep + push happens in server.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { localDate } from "./service.js";
|
|
6
|
+
export const DEFAULT_DIGEST_HOUR = 8;
|
|
7
|
+
/** Target local hour (0-23) for the daily digest; LISA_MAIL_DIGEST_HOUR overrides. */
|
|
8
|
+
export function digestHour(env = process.env) {
|
|
9
|
+
const h = Number(env.LISA_MAIL_DIGEST_HOUR);
|
|
10
|
+
return Number.isInteger(h) && h >= 0 && h <= 23 ? h : DEFAULT_DIGEST_HOUR;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Is a daily digest due? True when we haven't produced one for the local day yet
|
|
14
|
+
* AND it's at/after the target hour. Pure.
|
|
15
|
+
*/
|
|
16
|
+
export function isDigestDue(lastDigestDate, now, targetHour) {
|
|
17
|
+
if (lastDigestDate === localDate(now.getTime()))
|
|
18
|
+
return false;
|
|
19
|
+
return now.getHours() >= targetHour;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/mail/scheduler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,sFAAsF;AACtF,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC;AAC5E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,cAA6B,EAAE,GAAS,EAAE,UAAkB;IACtF,IAAI,cAAc,KAAK,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,GAAG,CAAC,QAAQ,EAAE,IAAI,UAAU,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Provider } from "../providers/types.js";
|
|
2
|
+
import type { DailyDigest, MailAccount, MailConnector, MailItem, MailSecret } from "./types.js";
|
|
3
|
+
export type ConnectorFactory = (account: MailAccount, secret: MailSecret) => MailConnector;
|
|
4
|
+
/** Local YYYY-MM-DD for a timestamp. */
|
|
5
|
+
export declare function localDate(ms: number): string;
|
|
6
|
+
export interface SweepOpts {
|
|
7
|
+
/** Window start; default 24h before now. */
|
|
8
|
+
sinceMs?: number;
|
|
9
|
+
/** Per-account cap; default 200. */
|
|
10
|
+
limit?: number;
|
|
11
|
+
connectorFactory?: ConnectorFactory;
|
|
12
|
+
provider?: Provider;
|
|
13
|
+
model?: string;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export interface SweepResult {
|
|
18
|
+
items: MailItem[];
|
|
19
|
+
digest: DailyDigest;
|
|
20
|
+
/** Items not previously seen (for alerts). */
|
|
21
|
+
newItems: MailItem[];
|
|
22
|
+
/** True when the consent gate blocked the sweep. */
|
|
23
|
+
blocked?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function sweepAll(opts?: SweepOpts): Promise<SweepResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Lightweight intraday poll: fetch, classify ONLY the unseen messages, mark seen.
|
|
28
|
+
* Returns just the freshly-classified items (for important-mail alerts). Does NOT
|
|
29
|
+
* rebuild/save the digest — that stays a daily artifact (or a manual sweep).
|
|
30
|
+
*/
|
|
31
|
+
export declare function pollNewMail(opts?: SweepOpts): Promise<MailItem[]>;
|
|
32
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/mail/service.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhG,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,KAAK,aAAa,CAAC;AAW3F,wCAAwC;AACxC,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG5C;AAED,MAAM,WAAW,SAAS;IACxB,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,SAAc,GAAG,OAAO,CAAC,WAAW,CAAC,CA6DzE;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,SAAc,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA+B3E"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail sweep service — the once-a-day (or on-demand) pipeline:
|
|
3
|
+
* consent gate → per account: connect → list (metadata+snippet) → classify →
|
|
4
|
+
* dedup vs seen → build digest → persist.
|
|
5
|
+
*
|
|
6
|
+
* Connector + classify provider are injectable so the whole pipeline is
|
|
7
|
+
* unit-testable without a real mailbox or a live model.
|
|
8
|
+
*/
|
|
9
|
+
import { isGranted } from "../consent/store.js";
|
|
10
|
+
import { loadAccounts, getSecret, markSwept, setSecret } from "./accounts.js";
|
|
11
|
+
import { classifyMail } from "./classify.js";
|
|
12
|
+
import { buildDigest } from "./digest.js";
|
|
13
|
+
import { saveDigest, loadSeen, markSeen } from "./store.js";
|
|
14
|
+
import { ImapConnector } from "./connectors/imap.js";
|
|
15
|
+
import { GmailConnector } from "./connectors/gmail.js";
|
|
16
|
+
function defaultConnector(account, secret) {
|
|
17
|
+
if (account.provider === "imap")
|
|
18
|
+
return new ImapConnector(account, secret);
|
|
19
|
+
if (account.provider === "gmail") {
|
|
20
|
+
// Persist refreshed access tokens back to the 0600 secrets file.
|
|
21
|
+
return new GmailConnector(account, secret, { onTokenRefresh: (t) => setSecret(account.id, t) });
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`unknown mail provider: ${account.provider}`);
|
|
24
|
+
}
|
|
25
|
+
/** Local YYYY-MM-DD for a timestamp. */
|
|
26
|
+
export function localDate(ms) {
|
|
27
|
+
const d = new Date(ms);
|
|
28
|
+
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
export async function sweepAll(opts = {}) {
|
|
31
|
+
const now = opts.now ?? Date.now;
|
|
32
|
+
const sinceMs = opts.sinceMs ?? now() - 24 * 60 * 60 * 1000;
|
|
33
|
+
const limit = opts.limit ?? 200;
|
|
34
|
+
const factory = opts.connectorFactory ?? defaultConnector;
|
|
35
|
+
if (!isGranted("mail")) {
|
|
36
|
+
return {
|
|
37
|
+
items: [],
|
|
38
|
+
digest: buildDigest([], { date: localDate(now()), accountIds: [], now }),
|
|
39
|
+
newItems: [],
|
|
40
|
+
blocked: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const accounts = loadAccounts().filter((a) => a.enabled);
|
|
44
|
+
const allItems = [];
|
|
45
|
+
const allNew = [];
|
|
46
|
+
let unread = 0;
|
|
47
|
+
for (const account of accounts) {
|
|
48
|
+
const secret = getSecret(account.id);
|
|
49
|
+
if (!secret)
|
|
50
|
+
continue;
|
|
51
|
+
let connector = null;
|
|
52
|
+
try {
|
|
53
|
+
connector = factory(account, secret);
|
|
54
|
+
const raws = (await connector.listSince({ sinceMs, limit })).map((r) => ({ ...r, accountId: account.id }));
|
|
55
|
+
if (raws.length === 0) {
|
|
56
|
+
markSwept(account.id, now());
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const seen = loadSeen(account.id);
|
|
60
|
+
const freshUids = new Set(raws.filter((r) => !seen.has(r.uid)).map((r) => r.uid));
|
|
61
|
+
unread += raws.filter((r) => !(r.flags ?? []).includes("\\Seen")).length;
|
|
62
|
+
const items = await classifyMail(raws, {
|
|
63
|
+
provider: opts.provider,
|
|
64
|
+
model: opts.model,
|
|
65
|
+
now,
|
|
66
|
+
signal: opts.signal,
|
|
67
|
+
});
|
|
68
|
+
allItems.push(...items);
|
|
69
|
+
allNew.push(...items.filter((i) => freshUids.has(i.uid)));
|
|
70
|
+
markSeen(account.id, raws.map((r) => r.uid));
|
|
71
|
+
markSwept(account.id, now());
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// one bad account (auth error, network) must not sink the whole sweep
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
if (connector)
|
|
78
|
+
await connector.close().catch(() => { });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const digest = buildDigest(allItems, {
|
|
82
|
+
date: localDate(now()),
|
|
83
|
+
accountIds: accounts.map((a) => a.id),
|
|
84
|
+
unread,
|
|
85
|
+
now,
|
|
86
|
+
});
|
|
87
|
+
saveDigest(digest);
|
|
88
|
+
return { items: allItems, digest, newItems: allNew };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Lightweight intraday poll: fetch, classify ONLY the unseen messages, mark seen.
|
|
92
|
+
* Returns just the freshly-classified items (for important-mail alerts). Does NOT
|
|
93
|
+
* rebuild/save the digest — that stays a daily artifact (or a manual sweep).
|
|
94
|
+
*/
|
|
95
|
+
export async function pollNewMail(opts = {}) {
|
|
96
|
+
const now = opts.now ?? Date.now;
|
|
97
|
+
const sinceMs = opts.sinceMs ?? now() - 24 * 60 * 60 * 1000;
|
|
98
|
+
const limit = opts.limit ?? 200;
|
|
99
|
+
const factory = opts.connectorFactory ?? defaultConnector;
|
|
100
|
+
if (!isGranted("mail"))
|
|
101
|
+
return [];
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const account of loadAccounts().filter((a) => a.enabled)) {
|
|
104
|
+
const secret = getSecret(account.id);
|
|
105
|
+
if (!secret)
|
|
106
|
+
continue;
|
|
107
|
+
let connector = null;
|
|
108
|
+
try {
|
|
109
|
+
connector = factory(account, secret);
|
|
110
|
+
const raws = (await connector.listSince({ sinceMs, limit })).map((r) => ({ ...r, accountId: account.id }));
|
|
111
|
+
const seen = loadSeen(account.id);
|
|
112
|
+
const fresh = raws.filter((r) => !seen.has(r.uid));
|
|
113
|
+
if (fresh.length) {
|
|
114
|
+
out.push(...(await classifyMail(fresh, { provider: opts.provider, model: opts.model, now, signal: opts.signal })));
|
|
115
|
+
}
|
|
116
|
+
markSeen(account.id, raws.map((r) => r.uid));
|
|
117
|
+
markSwept(account.id, now());
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// skip a failing account
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (connector)
|
|
124
|
+
await connector.close().catch(() => { });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=service.js.map
|