@wirecat/cli-core 0.513.0 → 0.515.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/CHANGELOG.md +22 -0
- package/dist/mail/index.d.ts +3 -0
- package/dist/mail/index.js +3 -0
- package/dist/mail/labels.d.ts +5 -0
- package/dist/mail/labels.js +27 -0
- package/dist/mail/progress.d.ts +10 -0
- package/dist/mail/progress.js +29 -0
- package/dist/mail/rule-stored.d.ts +9 -0
- package/dist/mail/rule-stored.js +57 -0
- package/dist/runs/error-detail.js +3 -0
- package/package.json +1 -1
- package/parity.json +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.515.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`storedMailRules(store, account, query?)`** reads one account's mail rules from the store in the shape
|
|
15
|
+
`syncMail` takes as `account.rules`, and `storedMailRule(stored)` reads one; a rule with a condition this build does
|
|
16
|
+
not know is left out. An account the store has not seen has no rules.
|
|
17
|
+
- **`labelName(raw)`, `systemLabel(name)` and `decodeMailboxName(name)`** name Gmail labels as people read them
|
|
18
|
+
(`\Inbox` and `Inbox` are one label, modified UTF-7 is decoded), moved from memo.
|
|
19
|
+
- **`mailProgress(store, key)`** gives each synced folder's window, done and total from the saved resume points,
|
|
20
|
+
without asking the mailbox. It treats only `not_found` as "no account yet" and passes other store errors on.
|
|
21
|
+
|
|
22
|
+
## 0.514.0 — 01.11.2026
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **A failed run's record masks a WhatsApp id as `[id]`**: `<number>[:device]@s.whatsapp.net`, a
|
|
27
|
+
group's `<number>-<number>@g.us`, `@c.us`, `@broadcast`, `@newsletter` and an `@lid` of any length. Before, such an
|
|
28
|
+
id was masked only when it looked like an email, and a 16-digit `@lid` or a `@newsletter` id stayed in the record.
|
|
29
|
+
- **The parity check requires `projects` and `projects evidence` in max and tg**, which ship them in max-cli 0.94.0
|
|
30
|
+
and tg-cli 0.110.0 (tg-cli#798, max-cli#886); wa stays planned.
|
|
31
|
+
|
|
10
32
|
## 0.513.0 — 01.11.2026
|
|
11
33
|
|
|
12
34
|
### Added
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -3,11 +3,14 @@ export { factsOf } from "./facts.js";
|
|
|
3
3
|
export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
|
|
4
4
|
export { dropMissing, type Gone, tooManyGone } from "./gone.js";
|
|
5
5
|
export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHeadersOf } from "./headers.js";
|
|
6
|
+
export { decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
6
7
|
export { type Address, htmlToText, looksLikeHtml, MAX_BODY_CHARS, type ParsedMail, parseMail, } from "./parse.js";
|
|
7
8
|
export { PRESET_NAMES, PRESETS, type PresetName, presetMatches } from "./presets.js";
|
|
8
9
|
export { domainsToAge, groupKey, type Probe, type ProbeGroup, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
10
|
+
export { type MailFolderProgress, mailProgress } from "./progress.js";
|
|
9
11
|
export { type FileStamp, FULL_PASS_MS, MBOX_RESUME, type MboxPoint, mergeRanges, parsePoint, type Ranges, type ResumePoint, rangesOf, resumeKey, rulesKey, within, } from "./resume.js";
|
|
10
12
|
export { ruleInput, STORE_ACTION } from "./rule-input.js";
|
|
13
|
+
export { storedMailRule, storedMailRules } from "./rule-stored.js";
|
|
11
14
|
export { mapProposedTags, ruleTagger, settleRuleTags, type TagOutcome } from "./rule-tags.js";
|
|
12
15
|
export { conditionKeys, decide, describeMatch, ENVELOPE, envelopeBox, envelopeBoxOf, type MailAction, type MailFacts, type MailRule, type MailRuleMatch, matches, sameMatch, tagsOf, } from "./rules.js";
|
|
13
16
|
export { type DomainAge, FRAUD_SIGNALS, FREE_MAIL, registrableOf, SCORE_RULE, type Scored, type Signal, scoreGroup, WEIGHTS, } from "./score.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -3,11 +3,14 @@ export { factsOf } from "./facts.js";
|
|
|
3
3
|
export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
|
|
4
4
|
export { dropMissing, tooManyGone } from "./gone.js";
|
|
5
5
|
export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
|
6
|
+
export { decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
6
7
|
export { htmlToText, looksLikeHtml, MAX_BODY_CHARS, parseMail, } from "./parse.js";
|
|
7
8
|
export { PRESET_NAMES, PRESETS, presetMatches } from "./presets.js";
|
|
8
9
|
export { domainsToAge, groupKey, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
10
|
+
export { mailProgress } from "./progress.js";
|
|
9
11
|
export { FULL_PASS_MS, MBOX_RESUME, mergeRanges, parsePoint, rangesOf, resumeKey, rulesKey, within, } from "./resume.js";
|
|
10
12
|
export { ruleInput, STORE_ACTION } from "./rule-input.js";
|
|
13
|
+
export { storedMailRule, storedMailRules } from "./rule-stored.js";
|
|
11
14
|
export { mapProposedTags, ruleTagger, settleRuleTags } from "./rule-tags.js";
|
|
12
15
|
export { conditionKeys, decide, describeMatch, ENVELOPE, envelopeBox, envelopeBoxOf, matches, sameMatch, tagsOf, } from "./rules.js";
|
|
13
16
|
export { FRAUD_SIGNALS, FREE_MAIL, registrableOf, SCORE_RULE, scoreGroup, WEIGHTS, } from "./score.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** IMAP's modified UTF-7 (RFC 3501 §5.1.3): `&` opens base64 of UTF-16BE, `,` stands for `/`, `-` closes. */
|
|
2
|
+
export declare const decodeMailboxName: (name: string) => string;
|
|
3
|
+
export declare const systemLabel: (name: string) => string;
|
|
4
|
+
/** A label as people read it: `\\Important` and `Important` are one label, and names are decoded. */
|
|
5
|
+
export declare const labelName: (raw: string) => string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const SYSTEM_LABELS = {
|
|
2
|
+
inbox: "Inbox",
|
|
3
|
+
sent: "Sent",
|
|
4
|
+
important: "Important",
|
|
5
|
+
starred: "Starred",
|
|
6
|
+
draft: "Drafts",
|
|
7
|
+
drafts: "Drafts",
|
|
8
|
+
spam: "Spam",
|
|
9
|
+
trash: "Trash",
|
|
10
|
+
junk: "Spam",
|
|
11
|
+
};
|
|
12
|
+
/** IMAP's modified UTF-7 (RFC 3501 §5.1.3): `&` opens base64 of UTF-16BE, `,` stands for `/`, `-` closes. */
|
|
13
|
+
export const decodeMailboxName = (name) => name.replace(/&([^-]*)-/g, (_, run) => {
|
|
14
|
+
if (run === "")
|
|
15
|
+
return "&";
|
|
16
|
+
const bytes = Buffer.from(run.replace(/,/g, "/"), "base64");
|
|
17
|
+
let text = "";
|
|
18
|
+
for (let at = 0; at + 1 < bytes.length; at += 2)
|
|
19
|
+
text += String.fromCharCode(bytes.readUInt16BE(at));
|
|
20
|
+
return text;
|
|
21
|
+
});
|
|
22
|
+
export const systemLabel = (name) => SYSTEM_LABELS[name.toLowerCase()] ?? name;
|
|
23
|
+
/** A label as people read it: `\\Important` and `Important` are one label, and names are decoded. */
|
|
24
|
+
export const labelName = (raw) => {
|
|
25
|
+
const system = raw.startsWith("\\") ? raw.slice(1) : raw;
|
|
26
|
+
return SYSTEM_LABELS[system.toLowerCase()] ?? decodeMailboxName(system);
|
|
27
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AccountKey, MessageStore } from "../store/index.js";
|
|
2
|
+
export interface MailFolderProgress {
|
|
3
|
+
folder: string;
|
|
4
|
+
since: string;
|
|
5
|
+
done: number;
|
|
6
|
+
total: number;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
/** How far the sync of each stored folder is, from the points the runs saved; the mailbox is not asked. */
|
|
10
|
+
export declare const mailProgress: (store: MessageStore, key: AccountKey) => Promise<MailFolderProgress[]>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { parsePoint, resumeKey } from "./resume.js";
|
|
3
|
+
/** How far the sync of each stored folder is, from the points the runs saved; the mailbox is not asked. */
|
|
4
|
+
export const mailProgress = async (store, key) => {
|
|
5
|
+
let accountId;
|
|
6
|
+
try {
|
|
7
|
+
accountId = (await store.accounts.storedAccount(key)).id;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof CliError && error.code === "not_found")
|
|
11
|
+
return [];
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
const folders = [];
|
|
15
|
+
for (const { kind, externalId: folder } of await store.mail.mailboxes(accountId)) {
|
|
16
|
+
if (kind !== null)
|
|
17
|
+
continue;
|
|
18
|
+
const point = parsePoint((await store.sync.syncState(key, resumeKey(folder)))?.value);
|
|
19
|
+
if (point !== undefined)
|
|
20
|
+
folders.push({
|
|
21
|
+
folder,
|
|
22
|
+
since: point.window.since,
|
|
23
|
+
done: point.window.done,
|
|
24
|
+
total: point.window.total,
|
|
25
|
+
updatedAt: point.updatedAt,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return folders;
|
|
29
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
+
import type { MessageStore, MailRule as StoredRule } from "../store/index.js";
|
|
3
|
+
import type { MailRule } from "./rules.js";
|
|
4
|
+
/** A stored rule as the sync reads it; `undefined` for one this build cannot read, so it is left to the build that can. */
|
|
5
|
+
export declare const storedMailRule: (stored: StoredRule) => MailRule | undefined;
|
|
6
|
+
/** One account's rules from the store, for `account.rules` of the sync; none when the store has not seen the account. */
|
|
7
|
+
export declare const storedMailRules: (store: MessageStore, account: {
|
|
8
|
+
address: string;
|
|
9
|
+
}, query?: QueryAst) => Promise<MailRule[]>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { PRESET_NAMES } from "./presets.js";
|
|
3
|
+
const ACTION_OF = { full: "keep", envelope: "envelope", skip: "skip" };
|
|
4
|
+
const KINDS = {
|
|
5
|
+
from: "string",
|
|
6
|
+
from_contains: "string",
|
|
7
|
+
domain: "string",
|
|
8
|
+
to: "string",
|
|
9
|
+
list_id: "string",
|
|
10
|
+
subject: "string",
|
|
11
|
+
older_than_days: "number",
|
|
12
|
+
larger_than_bytes: "number",
|
|
13
|
+
has_attachments: "boolean",
|
|
14
|
+
label: "string",
|
|
15
|
+
unread: "boolean",
|
|
16
|
+
bulk: "boolean",
|
|
17
|
+
score_at_least: "number",
|
|
18
|
+
preset: "string",
|
|
19
|
+
};
|
|
20
|
+
/** A stored rule as the sync reads it; `undefined` for one this build cannot read, so it is left to the build that can. */
|
|
21
|
+
export const storedMailRule = (stored) => {
|
|
22
|
+
const action = ACTION_OF[String(stored.actions.store)];
|
|
23
|
+
const entries = Object.entries(stored.conditions);
|
|
24
|
+
const known = entries.every(([key, value]) => key in KINDS &&
|
|
25
|
+
typeof value === KINDS[key] &&
|
|
26
|
+
!(key === "preset" && !PRESET_NAMES.includes(value)));
|
|
27
|
+
if (action === undefined || !known || entries.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
const tags = Array.isArray(stored.actions.tags) ? stored.actions.tags.filter((tag) => typeof tag === "string") : [];
|
|
30
|
+
const skipped = stored.actions.tags_skipped;
|
|
31
|
+
const tagsSkipped = skipped !== null && typeof skipped === "object" && !Array.isArray(skipped)
|
|
32
|
+
? Object.fromEntries(Object.entries(skipped).filter(([, why]) => typeof why === "string"))
|
|
33
|
+
: {};
|
|
34
|
+
return {
|
|
35
|
+
id: stored.id,
|
|
36
|
+
match: Object.fromEntries(entries),
|
|
37
|
+
action,
|
|
38
|
+
...(tags.length ? { tags: tags } : {}),
|
|
39
|
+
...(Object.keys(tagsSkipped).length ? { tags_skipped: tagsSkipped } : {}),
|
|
40
|
+
...(stored.reason === null ? {} : { reason: stored.reason }),
|
|
41
|
+
...(stored.protected ? { protected: true } : {}),
|
|
42
|
+
...(stored.source === "owner" ? {} : { source: stored.source }),
|
|
43
|
+
decided_at: new Date(stored.decidedAt).toISOString(),
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** One account's rules from the store, for `account.rules` of the sync; none when the store has not seen the account. */
|
|
47
|
+
export const storedMailRules = async (store, account, query) => {
|
|
48
|
+
try {
|
|
49
|
+
const { id } = await store.accounts.storedAccount({ provider: "email", account: account.address.toLowerCase() });
|
|
50
|
+
return (await store.mail.rules.list(id, query)).flatMap((stored) => storedMailRule(stored) ?? []);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof CliError && error.code === "not_found")
|
|
54
|
+
return [];
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
@@ -117,6 +117,8 @@ export const endpointTemplate = (raw) => {
|
|
|
117
117
|
/** A phone number as people type it with its country code, and one written as digits only without the "+". */
|
|
118
118
|
export const INTERNATIONAL_PHONE = /\+\d[\d\s()-]{6,}\d/.source;
|
|
119
119
|
export const BARE_PHONE = /(^|[^\w-])\d{11,15}\b/.source;
|
|
120
|
+
/** A WhatsApp jid: `<number>[:device]@s.whatsapp.net`, a group's `<number>-<time>@g.us`, an `@lid` of any length. */
|
|
121
|
+
const MESSENGER_ID = /\b\d+(?:-\d+)?(?::\d+)?@(?:s\.whatsapp\.net|g\.us|c\.us|lid|broadcast|newsletter)\b/g;
|
|
120
122
|
/**
|
|
121
123
|
* An error's sentence with what could be somebody's words or a secret taken out: anything in quotes, an email, a
|
|
122
124
|
* phone number, a secret `withoutSecrets` finds, a bot token, a bearer or a long opaque string, a URL's query, the
|
|
@@ -128,6 +130,7 @@ export const redactText = (text) => {
|
|
|
128
130
|
let out = home && home !== "/" ? text.split(home).join("~") : text;
|
|
129
131
|
out = withoutSecrets(out.replace(/"[^"\n]*"|“[^”\n]*”|«[^»\n]*»/g, '"[redacted]"'))
|
|
130
132
|
.replace(/\b(bearer|basic|token|key|password|secret)(\s*[:=]\s*|\s+)[^\s'"]+/gi, "$1$2[redacted]")
|
|
133
|
+
.replace(MESSENGER_ID, "[id]")
|
|
131
134
|
.replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "[email]")
|
|
132
135
|
.replace(new RegExp(INTERNATIONAL_PHONE, "g"), "[number]")
|
|
133
136
|
.replace(new RegExp(BARE_PHONE, "g"), "$1[number]")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.515.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -3678,18 +3678,14 @@
|
|
|
3678
3678
|
}
|
|
3679
3679
|
},
|
|
3680
3680
|
"projects": {
|
|
3681
|
-
"in": [],
|
|
3681
|
+
"in": ["max", "tg"],
|
|
3682
3682
|
"planned": {
|
|
3683
|
-
"max": "max-cli#886, on the cli-core release with this row",
|
|
3684
|
-
"tg": "tg-cli#798, on the cli-core release with this row",
|
|
3685
3683
|
"wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
|
|
3686
3684
|
}
|
|
3687
3685
|
},
|
|
3688
3686
|
"projects evidence": {
|
|
3689
|
-
"in": [],
|
|
3687
|
+
"in": ["max", "tg"],
|
|
3690
3688
|
"planned": {
|
|
3691
|
-
"max": "max-cli#886, on the cli-core release with this row",
|
|
3692
|
-
"tg": "tg-cli#798, on the cli-core release with this row",
|
|
3693
3689
|
"wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
|
|
3694
3690
|
},
|
|
3695
3691
|
"options": {
|