@wirecat/cli-core 0.519.0 → 0.521.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/cli/messenger/schedule-command.d.ts +3 -1
- package/dist/cli/messenger/schedule-command.js +8 -4
- package/dist/mail/cli/index.d.ts +22 -0
- package/dist/mail/cli/index.js +243 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +1 -0
- package/dist/mail/read.d.ts +55 -0
- package/dist/mail/read.js +114 -0
- package/dist/search/lucene/registry.js +11 -0
- package/dist/store/exports.d.ts +1 -0
- package/dist/store/exports.js +1 -0
- package/dist/store/sqlite/emails.d.ts +3 -2
- package/dist/store/sqlite/emails.js +2 -6
- package/dist/store/sqlite/lucene-fields.js +9 -0
- package/dist/store/sqlite/thread-columns.d.ts +24 -0
- package/dist/store/sqlite/thread-columns.js +20 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.521.0 — 01.11.2026
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`schedule exec` runs without a profile.** A timer starts `<cli> schedule exec <file>` with no profile named, and a
|
|
15
|
+
CLI with several profiles refused it ("name a profile") before reading the schedule, so no timed run ever ran. The
|
|
16
|
+
schedule's own environment carries the profile.
|
|
17
|
+
|
|
18
|
+
## 0.520.0 — 01.11.2026
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`@wirecat/cli-core/mail/cli`**: `mailReadCommands({ app, read })` mounts `search messages`, `messages show` and
|
|
23
|
+
`conversations list|show` on a mail CLI, read only and scoped to the profile's mailbox. `conversations list` takes a
|
|
24
|
+
query over `subject`, `emails_count` and `last_email_at`; `store.mail.threads({ where })` and `THREAD_COLUMNS`
|
|
25
|
+
answer it in SQL.
|
|
26
|
+
- **`searchMail`, `showMail`, `hitText`, `attachmentsText`, `withAttachmentText`, `lowerSuspicious`** in
|
|
27
|
+
`@wirecat/cli-core/mail`, moved from memo: mail search with demoted senders last (now also across pages), and an
|
|
28
|
+
email with the text of its attachments.
|
|
29
|
+
- **`list:`** in the query language: a mailing list by its List-Id, or by a domain for every list under it
|
|
30
|
+
(`list:github.com`); mail only.
|
|
31
|
+
|
|
10
32
|
## 0.519.0 — 01.11.2026
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -5,7 +5,9 @@ import { type Messenger } from "./context.js";
|
|
|
5
5
|
/** What the schedule command needs of a CLI: who it is and, per command, where it prints and which profile runs. */
|
|
6
6
|
export interface ScheduleHost {
|
|
7
7
|
app: AppIdentity;
|
|
8
|
-
context(command: Command
|
|
8
|
+
context(command: Command, resolving?: {
|
|
9
|
+
needsProfile?: boolean;
|
|
10
|
+
}): AnswerOutput & {
|
|
9
11
|
env: NodeJS.ProcessEnv;
|
|
10
12
|
profile: string;
|
|
11
13
|
};
|
|
@@ -63,8 +63,8 @@ const withWhen = (command) => command
|
|
|
63
63
|
*/
|
|
64
64
|
export const scheduleCommandFor = ({ app, context: contextOf }) => {
|
|
65
65
|
const command = new Command("schedule").description(`run a ${app.command} command or an agent later or on a timer (systemd, launchd, Task Scheduler); an agent cannot send`);
|
|
66
|
-
const prepare = (self) => {
|
|
67
|
-
const context = contextOf(self);
|
|
66
|
+
const prepare = (self, resolving = {}) => {
|
|
67
|
+
const context = contextOf(self, resolving);
|
|
68
68
|
const system = environmentOf(self).system ?? thisMachine();
|
|
69
69
|
const state = resolvePaths({ appName: app.appName, prefix: app.envPrefix, env: context.env }).state;
|
|
70
70
|
const say = (value, lines) => renderAnswer(self, context, itemOf(value), () => lines.join("\n"));
|
|
@@ -222,11 +222,15 @@ export const scheduleCommandFor = ({ app, context: contextOf }) => {
|
|
|
222
222
|
.description("what a timer runs, given the schedule's file; to run one by hand, use run")
|
|
223
223
|
.argument("<file>")
|
|
224
224
|
.action(async function (file) {
|
|
225
|
-
|
|
225
|
+
// A timer starts it with no profile named; the schedule's own environment carries the profile.
|
|
226
|
+
const { context, system, timer } = prepare(this, { needsProfile: false });
|
|
226
227
|
const last = await runSchedule(app, system, context.env, file, { timer: timer() });
|
|
227
228
|
if (last.exitCode !== 0)
|
|
228
229
|
setExitCode(this, 1);
|
|
229
230
|
});
|
|
230
231
|
return command;
|
|
231
232
|
};
|
|
232
|
-
export const scheduleCommand = (messenger) => scheduleCommandFor({
|
|
233
|
+
export const scheduleCommand = (messenger) => scheduleCommandFor({
|
|
234
|
+
app: messenger.app,
|
|
235
|
+
context: (command, resolving) => messengerContext(command, messenger, resolving),
|
|
236
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { AppIdentity } from "../../ports/app.js";
|
|
3
|
+
import { type AccountKey, type MessageStore } from "../../store/store.js";
|
|
4
|
+
/** What a mail CLI hands the shared commands: how to open the store for one run, and whose mail it reads. */
|
|
5
|
+
export interface MailReads {
|
|
6
|
+
app: AppIdentity;
|
|
7
|
+
/**
|
|
8
|
+
* Runs `work` over the shared store as the selected profile's mailbox, and closes the store after. Every command
|
|
9
|
+
* reads that mailbox alone; the store must take writes, since a search tops up its word index first.
|
|
10
|
+
*/
|
|
11
|
+
read: <T>(command: Command, work: (store: MessageStore, mailbox: AccountKey) => Promise<T>) => Promise<T>;
|
|
12
|
+
}
|
|
13
|
+
export declare const THREAD_QUERY: {
|
|
14
|
+
readonly entity: "mail_thread";
|
|
15
|
+
readonly backend: "sql";
|
|
16
|
+
readonly fields: import("@wirecat/cli-base/query").FieldSpec[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `search messages`, `messages show` and `conversations list|show` over the mail a mail CLI stored: the same
|
|
20
|
+
* query language, ranking and answers as memo's mail search, read only, scoped to the profile's mailbox.
|
|
21
|
+
*/
|
|
22
|
+
export declare const mailReadCommands: (mail: MailReads) => Command[];
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate, sharedCommand } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { queryFieldsOf } from "@wirecat/cli-base/query";
|
|
4
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
+
import * as v from "valibot";
|
|
6
|
+
import { answerEntity } from "../../cli/answer-entity.js";
|
|
7
|
+
import { outputFor } from "../../cli/context.js";
|
|
8
|
+
import { LOCAL_READ } from "../../cli/effects.js";
|
|
9
|
+
import { itemOf, listOf, renderAnswer, snakeKeys } from "../../cli/envelope.js";
|
|
10
|
+
import { listQuery, typedQuery, withQuery } from "../../cli/list-query.js";
|
|
11
|
+
import { messagesAnswer, offsetOf, shownOf } from "../../cli/messenger/search-answers.js";
|
|
12
|
+
import { positiveCount } from "../../cli/paging.js";
|
|
13
|
+
import { formatReference, parseReference } from "../../domain/references.js";
|
|
14
|
+
import { count, reference, text, time, untrusted } from "../../entities/fields.js";
|
|
15
|
+
import { attachmentsText, hitText, searchMail, showMail, withAttachmentText } from "../../mail/read.js";
|
|
16
|
+
import { atDetail } from "../../output-detail.js";
|
|
17
|
+
import { queryFilter, THREAD_COLUMNS } from "../../store/store.js";
|
|
18
|
+
const DEFAULT_LIMIT = 20;
|
|
19
|
+
const MAX_LIMIT = 100;
|
|
20
|
+
const limitOf = (value) => {
|
|
21
|
+
const limit = positiveCount("--limit")(value);
|
|
22
|
+
if (limit > MAX_LIMIT)
|
|
23
|
+
throw new InvalidArgumentError(`at most ${MAX_LIMIT}`);
|
|
24
|
+
return limit;
|
|
25
|
+
};
|
|
26
|
+
const wholeCount = (value) => {
|
|
27
|
+
if (!/^\d+$/.test(value.trim()))
|
|
28
|
+
throw new InvalidArgumentError("expected a whole number from 0");
|
|
29
|
+
return Number(value);
|
|
30
|
+
};
|
|
31
|
+
const detailOf = (command) => {
|
|
32
|
+
const { brief, full } = command.optsWithGlobals();
|
|
33
|
+
return brief ? "brief" : full ? "full" : undefined;
|
|
34
|
+
};
|
|
35
|
+
/** The thread a `chat:email/…` or `msg:email/…` reference names; another mailbox's is refused. */
|
|
36
|
+
const threadOf = (mailbox, given) => {
|
|
37
|
+
const place = parseReference(given);
|
|
38
|
+
if (place.type !== "chat" && place.type !== "message")
|
|
39
|
+
throw new CliError("validation_error", `${given} names no email thread — give chat:email/… or msg:email/…`);
|
|
40
|
+
if (place.provider !== mailbox.provider || place.account !== mailbox.account)
|
|
41
|
+
throw new CliError("not_found", `${given} is not mail of this profile's mailbox ${mailbox.account}`);
|
|
42
|
+
return place.chat;
|
|
43
|
+
};
|
|
44
|
+
const searchCommand = (mail) => {
|
|
45
|
+
const leaf = annotate(new Command("messages"), {
|
|
46
|
+
effects: LOCAL_READ,
|
|
47
|
+
partial: true,
|
|
48
|
+
output: { entity: "message", view: "hit" },
|
|
49
|
+
examples: [
|
|
50
|
+
`${mail.app.command} ya search messages invoice`,
|
|
51
|
+
`${mail.app.command} ya search messages 'from:ann@example.com has:attachment date>30d'`,
|
|
52
|
+
`${mail.app.command} ya search messages 'list:github.com is:unread' --newest`,
|
|
53
|
+
],
|
|
54
|
+
})
|
|
55
|
+
.summary("find stored emails by words and fields")
|
|
56
|
+
.description("search this profile's mail in the local store — what sync read — by words, people, folders, lists and dates; best match first")
|
|
57
|
+
.argument("<query...>", 'strict Lucene query: words, "phrases", AND/OR/NOT, field:value and date ranges')
|
|
58
|
+
.option("--limit <n>", `how many, at most ${MAX_LIMIT}`, limitOf)
|
|
59
|
+
.option("--page <n>", "which page, starting at 1", positiveCount("--page"))
|
|
60
|
+
.option("--newest", "newest first instead of best first")
|
|
61
|
+
.option("--exact", "bare words and quotes match their exact form only, as exact:word does")
|
|
62
|
+
.option("--plain-order", "best first by the words alone, without moving mail from suspicious senders down")
|
|
63
|
+
.option("--timezone <zone>", "the IANA timezone for calendar date boundaries")
|
|
64
|
+
.addHelpText("after", `
|
|
65
|
+
Fields: from: to: cc: bcc: subject: body: mailbox: (or label:) list: (a List-Id or its domain) is:unread|read|starred
|
|
66
|
+
has:attachment, filename:, content: (the text of attachments), size>, date: — \`${mail.app.command} ya search messages 'content:invoice'\`.
|
|
67
|
+
chat: names a thread by its subject. Mail sent to many is in the mailbox bulk; NOT mailbox:bulk leaves it out.
|
|
68
|
+
Mail from senders with signs of fraud comes after the rest of the page; --plain-order keeps it in place.
|
|
69
|
+
--json: the list is items; each hit's reference is ref (msg:email/…), which messages show reads.
|
|
70
|
+
Search guide: https://github.com/WireCatLabs/cli-core/blob/main/docs/search/query-language.md`)
|
|
71
|
+
.action(async function (words) {
|
|
72
|
+
const options = this.opts();
|
|
73
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
74
|
+
const page = options.page ?? 1;
|
|
75
|
+
const query = words.join(" ");
|
|
76
|
+
const found = await mail.read(this, (store, mailbox) => searchMail(store, mailbox, {
|
|
77
|
+
text: query,
|
|
78
|
+
limit,
|
|
79
|
+
mailAccount: mailbox.account,
|
|
80
|
+
...(offsetOf(page, limit) === undefined ? {} : { offset: offsetOf(page, limit) }),
|
|
81
|
+
...(options.newest ? { newest: true } : {}),
|
|
82
|
+
...(options.exact ? { exact: true } : {}),
|
|
83
|
+
...(options.plainOrder ? { plainOrder: true } : {}),
|
|
84
|
+
...(options.timezone === undefined ? {} : { timezone: options.timezone }),
|
|
85
|
+
}, { app: mail.app }));
|
|
86
|
+
const output = outputFor(this);
|
|
87
|
+
const again = {
|
|
88
|
+
query,
|
|
89
|
+
...(options.newest ? { newest: true } : {}),
|
|
90
|
+
...(options.exact ? { exact: true } : {}),
|
|
91
|
+
...(options.plainOrder ? { plain_order: true } : {}),
|
|
92
|
+
...(options.timezone === undefined ? {} : { timezone: options.timezone }),
|
|
93
|
+
};
|
|
94
|
+
const level = detailOf(this);
|
|
95
|
+
renderAnswer(this, output, messagesAnswer("search messages", found, { limit, page, again }, query), () => {
|
|
96
|
+
if (!found.wordsReady)
|
|
97
|
+
output.renderer.note(`the word index is still being built — \`${mail.app.command} store migrate\` finishes it`);
|
|
98
|
+
if (found.items.length === 0)
|
|
99
|
+
output.renderer.note("nothing found — only mail sync stored is searched");
|
|
100
|
+
const shown = shownOf(found.items.length, (page - 1) * limit, found.total);
|
|
101
|
+
if (shown)
|
|
102
|
+
output.renderer.note(shown);
|
|
103
|
+
return found.items.map((item) => `${hitText(item, level)}\n`).join("");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
return new Command("search").summary("search the stored mail").description("find stored emails").addCommand(leaf);
|
|
107
|
+
};
|
|
108
|
+
const shownText = (emails) => `${emails.map((email) => [hitText(email, "full"), attachmentsText(email)].filter(Boolean).join("\n")).join("\n\n")}\n`;
|
|
109
|
+
const messagesCommand = (mail) => {
|
|
110
|
+
const messages = new Command("messages").summary("read stored emails").description("read the emails sync stored");
|
|
111
|
+
annotate(messages.command("show"), {
|
|
112
|
+
effects: LOCAL_READ,
|
|
113
|
+
examples: [
|
|
114
|
+
`${mail.app.command} ya messages show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E'`,
|
|
115
|
+
`${mail.app.command} ya messages show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E' --before-n 2`,
|
|
116
|
+
],
|
|
117
|
+
})
|
|
118
|
+
.summary("one stored email whole, with the text of its attachments")
|
|
119
|
+
.description("one stored email: its people, folders, whole text and the first 4000 characters of each attachment's text; " +
|
|
120
|
+
"--full gives all of it and the HTML body. The store keeps text, not the original message source")
|
|
121
|
+
.argument("<message>", "the msg:email/… reference a search gives")
|
|
122
|
+
.option("--before-n <n>", "also this many earlier emails of its thread", wholeCount, 0)
|
|
123
|
+
.option("--after-n <n>", "also this many later emails of its thread", wholeCount, 0)
|
|
124
|
+
.action(async function (reference) {
|
|
125
|
+
const { beforeN, afterN } = this.opts();
|
|
126
|
+
const full = detailOf(this) === "full";
|
|
127
|
+
const emails = await mail.read(this, async (store, mailbox) => {
|
|
128
|
+
threadOf(mailbox, reference);
|
|
129
|
+
return showMail(store, reference, { before: beforeN, after: afterN, full });
|
|
130
|
+
});
|
|
131
|
+
const output = outputFor(this);
|
|
132
|
+
const shown = emails.map((email) => snakeKeys(atDetail(email)));
|
|
133
|
+
renderAnswer(this, output, beforeN || afterN
|
|
134
|
+
? listOf("messages show", { items: shown, limit: Math.max(1, shown.length), has_more: false })
|
|
135
|
+
: itemOf(shown.find((_, at) => emails[at]?.anchor) ?? shown[0]), () => shownText(emails));
|
|
136
|
+
});
|
|
137
|
+
return messages;
|
|
138
|
+
};
|
|
139
|
+
const mailThread = v.strictObject({
|
|
140
|
+
/** The thread as a `chat:email/…` reference, which `conversations show` takes. */
|
|
141
|
+
ref: reference,
|
|
142
|
+
subject: v.nullable(text),
|
|
143
|
+
emails_count: count,
|
|
144
|
+
last_email_at: v.nullable(time),
|
|
145
|
+
untrusted,
|
|
146
|
+
});
|
|
147
|
+
export const THREAD_QUERY = {
|
|
148
|
+
entity: "mail_thread",
|
|
149
|
+
backend: "sql",
|
|
150
|
+
fields: queryFieldsOf(mailThread, {
|
|
151
|
+
exclude: ["ref", "untrusted"],
|
|
152
|
+
text: ["subject"],
|
|
153
|
+
examples: { subject: "subject:invoice", emails_count: "emails_count>2", last_email_at: "last_email_at>7d" },
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
156
|
+
const conversationsCommand = (mail) => {
|
|
157
|
+
const conversations = new Command("conversations")
|
|
158
|
+
.summary("email threads")
|
|
159
|
+
.description("the threads of the stored mail: an email and its replies");
|
|
160
|
+
const list = withQuery(annotate(conversations.command("list"), {
|
|
161
|
+
effects: LOCAL_READ,
|
|
162
|
+
examples: [
|
|
163
|
+
`${mail.app.command} ya conversations list`,
|
|
164
|
+
`${mail.app.command} ya conversations list subject:invoice emails_count>1`,
|
|
165
|
+
],
|
|
166
|
+
})
|
|
167
|
+
.summary("stored email threads, newest first")
|
|
168
|
+
.description("this profile's email threads, the newest activity first: subject, how many emails, when"), THREAD_QUERY)
|
|
169
|
+
.option("--limit <n>", `how many, at most ${MAX_LIMIT}`, limitOf)
|
|
170
|
+
.option("--page <n>", "which page, starting at 1", positiveCount("--page"))
|
|
171
|
+
.action(async function (words) {
|
|
172
|
+
const options = this.opts();
|
|
173
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
174
|
+
const page = options.page ?? 1;
|
|
175
|
+
const typed = typedQuery(words);
|
|
176
|
+
const query = listQuery(typed, THREAD_QUERY, "conversations list");
|
|
177
|
+
const { threads, mailbox } = await mail.read(this, async (store, mailbox) => {
|
|
178
|
+
const account = await store.accounts.storedAccount(mailbox).catch(() => undefined);
|
|
179
|
+
if (!account)
|
|
180
|
+
return { threads: [], mailbox };
|
|
181
|
+
const where = query ? queryFilter(query, THREAD_COLUMNS, { command: "conversations list" }) : undefined;
|
|
182
|
+
const threads = await store.mail.threads({
|
|
183
|
+
accountId: account.id,
|
|
184
|
+
limit: limit + 1,
|
|
185
|
+
offset: (page - 1) * limit,
|
|
186
|
+
...(where ? { where } : {}),
|
|
187
|
+
});
|
|
188
|
+
return { threads, mailbox };
|
|
189
|
+
});
|
|
190
|
+
const items = threads.slice(0, limit).map((thread) => ({
|
|
191
|
+
ref: formatReference({ type: "chat", ...mailbox, chat: thread.externalId }),
|
|
192
|
+
subject: thread.subject,
|
|
193
|
+
emails_count: thread.emailsCount,
|
|
194
|
+
last_email_at: thread.lastEmailAt === null ? null : new Date(thread.lastEmailAt).toISOString(),
|
|
195
|
+
untrusted: ["subject"],
|
|
196
|
+
}));
|
|
197
|
+
renderAnswer(this, outputFor(this), listOf("conversations list", {
|
|
198
|
+
items,
|
|
199
|
+
limit,
|
|
200
|
+
has_more: threads.length > limit,
|
|
201
|
+
page,
|
|
202
|
+
...(typed.trim() ? { args: { query: typed } } : {}),
|
|
203
|
+
}), () => items
|
|
204
|
+
.map((one) => `${one.ref}\n ${one.last_email_at ?? "time unknown"} ${one.emails_count} ${one.subject ?? ""}\n`)
|
|
205
|
+
.join(""));
|
|
206
|
+
});
|
|
207
|
+
answerEntity(list, "mail_thread", mailThread);
|
|
208
|
+
annotate(conversations.command("show"), {
|
|
209
|
+
effects: LOCAL_READ,
|
|
210
|
+
examples: [
|
|
211
|
+
`${mail.app.command} ya conversations show 'chat:email/me%40example.com/thread-1'`,
|
|
212
|
+
`${mail.app.command} ya conversations show 'msg:email/me%40example.com/thread-1/%3Cid%40example.com%3E'`,
|
|
213
|
+
],
|
|
214
|
+
})
|
|
215
|
+
.summary("one email thread, every email oldest first")
|
|
216
|
+
.description("every stored email of one thread, oldest first, each with the text of its attachments")
|
|
217
|
+
.argument("<conversation>", "a chat:email/… thread from conversations list, or a msg:email/… email in it")
|
|
218
|
+
.action(async function (given) {
|
|
219
|
+
const full = detailOf(this) === "full";
|
|
220
|
+
const emails = await mail.read(this, async (store, mailbox) => {
|
|
221
|
+
const thread = threadOf(mailbox, given);
|
|
222
|
+
const account = await store.accounts.storedAccount(mailbox).catch(() => undefined);
|
|
223
|
+
const hits = account ? await store.mail.threadHits(account.id, thread) : [];
|
|
224
|
+
if (!account || hits.length === 0)
|
|
225
|
+
throw new CliError("not_found", `no stored thread ${given}`);
|
|
226
|
+
return Promise.all(hits.map(async (hit) => hit.attachments?.length
|
|
227
|
+
? withAttachmentText(hit, (await store.mail.email(account.id, hit.id))?.attachments ?? [], full)
|
|
228
|
+
: hit));
|
|
229
|
+
});
|
|
230
|
+
const items = emails.map((email) => snakeKeys(atDetail(email)));
|
|
231
|
+
renderAnswer(this, outputFor(this), listOf("conversations show", { items, limit: Math.max(1, items.length), has_more: false }), () => shownText(emails));
|
|
232
|
+
});
|
|
233
|
+
return conversations;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* `search messages`, `messages show` and `conversations list|show` over the mail a mail CLI stored: the same
|
|
237
|
+
* query language, ranking and answers as memo's mail search, read only, scoped to the profile's mailbox.
|
|
238
|
+
*/
|
|
239
|
+
export const mailReadCommands = sharedCommand((mail) => [
|
|
240
|
+
searchCommand(mail),
|
|
241
|
+
messagesCommand(mail),
|
|
242
|
+
conversationsCommand(mail),
|
|
243
|
+
]);
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { type Address, htmlToText, looksLikeHtml, MAX_BODY_CHARS, type ParsedMai
|
|
|
8
8
|
export { PRESET_NAMES, PRESETS, type PresetName, presetMatches } from "./presets.js";
|
|
9
9
|
export { domainsToAge, groupKey, type Probe, type ProbeGroup, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
10
10
|
export { type MailFolderProgress, mailProgress } from "./progress.js";
|
|
11
|
+
export { ATTACHMENT_CHARS, attachmentsText, hitText, lowerSuspicious, type MailSearch, type ShownMail, searchMail, showMail, withAttachmentText, } from "./read.js";
|
|
11
12
|
export { type FileStamp, FULL_PASS_MS, MBOX_RESUME, type MboxPoint, mergeRanges, parsePoint, type Ranges, type ResumePoint, rangesOf, resumeKey, rulesKey, within, } from "./resume.js";
|
|
12
13
|
export { ruleInput, STORE_ACTION } from "./rule-input.js";
|
|
13
14
|
export { storedMailRule, storedMailRules } from "./rule-stored.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { htmlToText, looksLikeHtml, MAX_BODY_CHARS, parseMail, } from "./parse.j
|
|
|
8
8
|
export { PRESET_NAMES, PRESETS, presetMatches } from "./presets.js";
|
|
9
9
|
export { domainsToAge, groupKey, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
10
10
|
export { mailProgress } from "./progress.js";
|
|
11
|
+
export { ATTACHMENT_CHARS, attachmentsText, hitText, lowerSuspicious, searchMail, showMail, withAttachmentText, } from "./read.js";
|
|
11
12
|
export { FULL_PASS_MS, MBOX_RESUME, mergeRanges, parsePoint, rangesOf, resumeKey, rulesKey, within, } from "./resume.js";
|
|
12
13
|
export { ruleInput, STORE_ACTION } from "./rule-input.js";
|
|
13
14
|
export { storedMailRule, storedMailRules } from "./rule-stored.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Saved } from "../services/message-resolution.js";
|
|
2
|
+
import type { SearchFound } from "../services/messages.js";
|
|
3
|
+
import type { AccountKey, MessageStore, StoredHit } from "../store/store.js";
|
|
4
|
+
/** Mail from senders with signs of fraud after the rest of the page, each part in the order it had. */
|
|
5
|
+
export declare const lowerSuspicious: <T>(items: readonly T[], suspicious: (item: T) => boolean) => T[];
|
|
6
|
+
export interface MailSearch {
|
|
7
|
+
text: string;
|
|
8
|
+
limit: number;
|
|
9
|
+
offset?: number;
|
|
10
|
+
/** Only these mailboxes; every stored one when neither this nor `mailAccount` is given. */
|
|
11
|
+
accounts?: AccountKey[];
|
|
12
|
+
/** Only this mailbox, by its address. */
|
|
13
|
+
mailAccount?: string;
|
|
14
|
+
exact?: boolean;
|
|
15
|
+
timezone?: string;
|
|
16
|
+
newest?: boolean;
|
|
17
|
+
/** Leave mail from suspicious senders where the words put it, instead of after the rest. */
|
|
18
|
+
plainOrder?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Stored mail by the shared query language, best first. Mail whose sender search demotes comes after the rest,
|
|
22
|
+
* unless `plainOrder` or `newest` asks for another order. `runAs` is the account the search runs as.
|
|
23
|
+
*/
|
|
24
|
+
export declare const searchMail: (store: MessageStore, runAs: AccountKey, { text, limit, offset, accounts, mailAccount, exact, timezone, newest, plainOrder }: MailSearch, saved?: Saved) => Promise<SearchFound>;
|
|
25
|
+
/** Enough for a bill's amounts and dates; the whole text is asked for separately. */
|
|
26
|
+
export declare const ATTACHMENT_CHARS = 4000;
|
|
27
|
+
export type ShownMail = StoredHit & {
|
|
28
|
+
anchor?: true;
|
|
29
|
+
};
|
|
30
|
+
interface StoredFile {
|
|
31
|
+
text: string | null;
|
|
32
|
+
extraction: string | null;
|
|
33
|
+
extractionError: string | null;
|
|
34
|
+
}
|
|
35
|
+
/** The extracted text of each attachment, next to the name the email lists; a file without text says why. */
|
|
36
|
+
export declare const withAttachmentText: (hit: ShownMail, files: readonly StoredFile[], full: boolean) => ShownMail;
|
|
37
|
+
/**
|
|
38
|
+
* One stored email by its `msg:email/…` locator, with up to `before` and `after` emails of its thread, each with
|
|
39
|
+
* the text of its attachments: the first `ATTACHMENT_CHARS` of each, or all of it and the HTML body with `full`.
|
|
40
|
+
*/
|
|
41
|
+
export declare const showMail: (store: MessageStore, locator: string, { before, after, full }?: {
|
|
42
|
+
before?: number;
|
|
43
|
+
after?: number;
|
|
44
|
+
full?: boolean;
|
|
45
|
+
}) => Promise<ShownMail[]>;
|
|
46
|
+
/**
|
|
47
|
+
* A found message or email for a reader: its reference first, then when and from whom, who else it went to and
|
|
48
|
+
* where it is filed, its subject or chat, and an excerpt — the whole text only at `full`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const hitText: (item: StoredHit & {
|
|
51
|
+
excerpt?: string;
|
|
52
|
+
}, level?: "brief" | "full") => string;
|
|
53
|
+
/** Each attachment of a shown email with its text indented under it, or why it has none. */
|
|
54
|
+
export declare const attachmentsText: (hit: ShownMail) => string;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { parseLocator } from "../domain/locator.js";
|
|
2
|
+
import { readMail } from "../services/mail-search.js";
|
|
3
|
+
import { searchStore } from "../services/messages-archive-search.js";
|
|
4
|
+
/** Mail from senders with signs of fraud after the rest of the page, each part in the order it had. */
|
|
5
|
+
export const lowerSuspicious = (items, suspicious) => [
|
|
6
|
+
...items.filter((item) => !suspicious(item)),
|
|
7
|
+
...items.filter(suspicious),
|
|
8
|
+
];
|
|
9
|
+
/** A few more than asked, so mail moved down makes room for the next best rather than an empty slot. */
|
|
10
|
+
const LOWERED_SPARE = 20;
|
|
11
|
+
/**
|
|
12
|
+
* Stored mail by the shared query language, best first. Mail whose sender search demotes comes after the rest,
|
|
13
|
+
* unless `plainOrder` or `newest` asks for another order. `runAs` is the account the search runs as.
|
|
14
|
+
*/
|
|
15
|
+
export const searchMail = async (store, runAs, { text, limit, offset = 0, accounts, mailAccount, exact, timezone, newest, plainOrder }, saved = {}) => {
|
|
16
|
+
const lower = !plainOrder && !newest;
|
|
17
|
+
const request = {
|
|
18
|
+
text,
|
|
19
|
+
kind: "mail",
|
|
20
|
+
...(accounts === undefined ? {} : { accounts }),
|
|
21
|
+
...(mailAccount === undefined ? {} : { mailAccount }),
|
|
22
|
+
...(exact ? { exact: true } : {}),
|
|
23
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
24
|
+
...(newest ? { newest: true } : {}),
|
|
25
|
+
};
|
|
26
|
+
if (!lower)
|
|
27
|
+
return searchStore(store, runAs, { ...request, limit, offset }, saved);
|
|
28
|
+
// Lowered from the first hit on, so a later page never repeats or skips what an earlier one moved.
|
|
29
|
+
const found = await searchStore(store, runAs, { ...request, limit: offset + limit + LOWERED_SPARE }, saved);
|
|
30
|
+
const items = lowerSuspicious(found.items, (item) => item.email?.senderScore?.demoted === true);
|
|
31
|
+
const hasMore = found.hasMore || items.length > offset + limit;
|
|
32
|
+
const { nextOffset: _, ...rest } = found;
|
|
33
|
+
return {
|
|
34
|
+
...rest,
|
|
35
|
+
items: items.slice(offset, offset + limit),
|
|
36
|
+
hasMore,
|
|
37
|
+
...(hasMore ? { nextOffset: offset + limit } : {}),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** Enough for a bill's amounts and dates; the whole text is asked for separately. */
|
|
41
|
+
export const ATTACHMENT_CHARS = 4000;
|
|
42
|
+
/** The HTML and AMP alternatives of the body: markup, not attachments, and thousands of characters each. */
|
|
43
|
+
const isBodyAlternative = (attachment) => attachment.mime === "text/html" || attachment.mime === "text/x-amp-html";
|
|
44
|
+
/** The extracted text of each attachment, next to the name the email lists; a file without text says why. */
|
|
45
|
+
export const withAttachmentText = (hit, files, full) => ({
|
|
46
|
+
...hit,
|
|
47
|
+
attachments: (hit.attachments ?? [])
|
|
48
|
+
.map((attachment, at) => ({ attachment, file: files[at] }))
|
|
49
|
+
.filter(({ attachment }) => full || !isBodyAlternative(attachment))
|
|
50
|
+
.map(({ attachment, file }) => {
|
|
51
|
+
const text = file?.text ?? "";
|
|
52
|
+
if (!text)
|
|
53
|
+
return { ...attachment, extraction: file?.extractionError ?? file?.extraction ?? "none" };
|
|
54
|
+
const cut = !full && text.length > ATTACHMENT_CHARS;
|
|
55
|
+
return {
|
|
56
|
+
...attachment,
|
|
57
|
+
text: cut ? text.slice(0, ATTACHMENT_CHARS) : text,
|
|
58
|
+
text_chars: text.length,
|
|
59
|
+
...(cut ? { text_cut: true } : {}),
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* One stored email by its `msg:email/…` locator, with up to `before` and `after` emails of its thread, each with
|
|
65
|
+
* the text of its attachments: the first `ATTACHMENT_CHARS` of each, or all of it and the HTML body with `full`.
|
|
66
|
+
*/
|
|
67
|
+
export const showMail = async (store, locator, { before = 0, after = 0, full = false } = {}) => {
|
|
68
|
+
const shown = await readMail(store, locator, { before, after });
|
|
69
|
+
const account = await store.accounts.storedAccount({ provider: "email", account: parseLocator(locator).account });
|
|
70
|
+
if (!account)
|
|
71
|
+
return shown;
|
|
72
|
+
return Promise.all(shown.map(async (hit) => {
|
|
73
|
+
if (!hit.attachments?.length)
|
|
74
|
+
return hit;
|
|
75
|
+
const email = await store.mail.email(account.id, hit.id);
|
|
76
|
+
return withAttachmentText(hit, email?.attachments ?? [], full);
|
|
77
|
+
}));
|
|
78
|
+
};
|
|
79
|
+
const line = (text) => text.replace(/\s+/g, " ").trim();
|
|
80
|
+
/**
|
|
81
|
+
* A found message or email for a reader: its reference first, then when and from whom, who else it went to and
|
|
82
|
+
* where it is filed, its subject or chat, and an excerpt — the whole text only at `full`.
|
|
83
|
+
*/
|
|
84
|
+
export const hitText = (item, level) => {
|
|
85
|
+
const mail = item.email;
|
|
86
|
+
const names = (people) => people.map(({ name, address }) => line(name ?? address)).join(", ");
|
|
87
|
+
const between = mail
|
|
88
|
+
? [
|
|
89
|
+
mail.to.length ? `to ${names(mail.to)}` : "",
|
|
90
|
+
mail.cc.length ? `cc ${names(mail.cc)}` : "",
|
|
91
|
+
mail.labels.join(", "),
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(" · ")
|
|
95
|
+
: "";
|
|
96
|
+
const who = line(item.senderName ?? item.senderId ?? "unknown");
|
|
97
|
+
return [
|
|
98
|
+
item.locator,
|
|
99
|
+
` ${item.timestamp} ${who}${level === "brief" || !between ? "" : ` ${between}`}`,
|
|
100
|
+
...(level === "brief" || !item.chatTitle ? [] : [` ${line(item.chatTitle)}`]),
|
|
101
|
+
` ${level === "full" ? item.text.replace(/\n/g, "\n ") : line(item.excerpt ?? item.text)}`,
|
|
102
|
+
].join("\n");
|
|
103
|
+
};
|
|
104
|
+
/** Each attachment of a shown email with its text indented under it, or why it has none. */
|
|
105
|
+
export const attachmentsText = (hit) => (hit.attachments ?? [])
|
|
106
|
+
.map((attachment) => {
|
|
107
|
+
const file = attachment;
|
|
108
|
+
const name = file.name ?? file.kind;
|
|
109
|
+
if (file.text === undefined)
|
|
110
|
+
return ` ── attachment ${name}: no text (${file.extraction}) ──`;
|
|
111
|
+
const cut = file.text.length < (file.text_chars ?? 0) ? `, first ${file.text.length} of ${file.text_chars}` : "";
|
|
112
|
+
return ` ── attachment ${name} (${file.text_chars} characters${cut}) ──\n ${file.text.replace(/\n/g, "\n ")}`;
|
|
113
|
+
})
|
|
114
|
+
.join("\n");
|
|
@@ -125,6 +125,17 @@ export const QUERY_FIELDS = [
|
|
|
125
125
|
normalization: "the same as mailbox:; mail only",
|
|
126
126
|
support: "A1",
|
|
127
127
|
},
|
|
128
|
+
{
|
|
129
|
+
name: "list",
|
|
130
|
+
example: "list:github.com",
|
|
131
|
+
index: "emails.headers list-id",
|
|
132
|
+
aliases: [],
|
|
133
|
+
type: "kw",
|
|
134
|
+
operators: terms,
|
|
135
|
+
normalization: "the List-Id of a mailing list, lower case; a domain matches every list under it; mail only",
|
|
136
|
+
support: "A1",
|
|
137
|
+
validate: filled,
|
|
138
|
+
},
|
|
128
139
|
{
|
|
129
140
|
name: "is",
|
|
130
141
|
example: "is:unread",
|
package/dist/store/exports.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export { savedTaskCategories } from "./sqlite/task-category-settings.js";
|
|
|
54
54
|
export { DEFAULT_AGENT_CLOSE, savedAgentClose } from "./sqlite/task-close.js";
|
|
55
55
|
export { DEFAULT_FIRST_PASS_PER_CHAT, firstPassPerChatOf, savedFirstPassPerChat } from "./sqlite/task-first-pass.js";
|
|
56
56
|
export { inboxKey } from "./sqlite/tasks.js";
|
|
57
|
+
export { THREAD_COLUMNS } from "./sqlite/thread-columns.js";
|
|
57
58
|
export { dropAllUnusedVectors, vectorIndexCounts } from "./sqlite/vector-index.js";
|
|
58
59
|
export type { MatchPlace, ScoredHit } from "./sqlite/words.js";
|
|
59
60
|
export type { TaskDetails } from "./sqlite/work-tasks.js";
|
package/dist/store/exports.js
CHANGED
|
@@ -24,4 +24,5 @@ export { savedTaskCategories } from "./sqlite/task-category-settings.js";
|
|
|
24
24
|
export { DEFAULT_AGENT_CLOSE, savedAgentClose } from "./sqlite/task-close.js";
|
|
25
25
|
export { DEFAULT_FIRST_PASS_PER_CHAT, firstPassPerChatOf, savedFirstPassPerChat } from "./sqlite/task-first-pass.js";
|
|
26
26
|
export { inboxKey } from "./sqlite/tasks.js";
|
|
27
|
+
export { THREAD_COLUMNS } from "./sqlite/thread-columns.js";
|
|
27
28
|
export { dropAllUnusedVectors, vectorIndexCounts } from "./sqlite/vector-index.js";
|
|
@@ -8,6 +8,7 @@ import { type MailRulesStore } from "./mail-rules.js";
|
|
|
8
8
|
import { type AttachmentInput } from "./meetings.js";
|
|
9
9
|
import type { StoreContext } from "./open.js";
|
|
10
10
|
import { type SyncStateSave } from "./sync.js";
|
|
11
|
+
import { type ThreadFilter } from "./thread-columns.js";
|
|
11
12
|
export type { EmailLocationInput, FolderEmail };
|
|
12
13
|
export interface EmailAddress {
|
|
13
14
|
address: string;
|
|
@@ -206,8 +207,8 @@ export interface MailStore {
|
|
|
206
207
|
saveThread(input: ThreadSave): Promise<ThreadDetails>;
|
|
207
208
|
/** Several threads and sync state in one transaction: a slice of mail commits with its resume point, or not at all. */
|
|
208
209
|
saveThreads(inputs: ThreadSave[], states?: SyncStateSave[]): Promise<ThreadDetails[]>;
|
|
209
|
-
/** Newest activity first
|
|
210
|
-
threads(filter?:
|
|
210
|
+
/** Newest activity first; `where` is a list query compiled over `THREAD_COLUMNS`. */
|
|
211
|
+
threads(filter?: ThreadFilter): Promise<EmailThread[]>;
|
|
211
212
|
thread(id: number): Promise<ThreadDetails | null>;
|
|
212
213
|
email(accountId: number, externalId: string): Promise<Email | null>;
|
|
213
214
|
/** Newest first. */
|
|
@@ -11,6 +11,7 @@ import { mailRulesOver } from "./mail-rules.js";
|
|
|
11
11
|
import { attachmentOf, saveAttachment, wordsQuery } from "./meetings.js";
|
|
12
12
|
import { stemmerCache } from "./stems.js";
|
|
13
13
|
import { writeState } from "./sync.js";
|
|
14
|
+
import { threadRows } from "./thread-columns.js";
|
|
14
15
|
import { write } from "./transaction.js";
|
|
15
16
|
import { dropUnusedVectors } from "./vector-index.js";
|
|
16
17
|
import { dot } from "./vectors.js";
|
|
@@ -432,12 +433,7 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
|
|
|
432
433
|
return ids.map((id) => threadDetails(database, id));
|
|
433
434
|
},
|
|
434
435
|
async threads(filter = {}) {
|
|
435
|
-
|
|
436
|
-
return database
|
|
437
|
-
.prepare(`SELECT * FROM email_threads WHERE (? IS NULL OR account_id = ?) AND (? OR deleted_at IS NULL)
|
|
438
|
-
ORDER BY coalesce(last_email_at, 0) DESC, id DESC LIMIT ? OFFSET ?`)
|
|
439
|
-
.all(filter.accountId ?? null, filter.accountId ?? null, filter.includeDeleted ? 1 : 0, limit, offset)
|
|
440
|
-
.map(threadOf);
|
|
436
|
+
return threadRows(database, filter).map(threadOf);
|
|
441
437
|
},
|
|
442
438
|
async thread(id) {
|
|
443
439
|
return threadDetails(database, id);
|
|
@@ -381,6 +381,14 @@ const mailbox = ({ value }, { mail }) => always(mail
|
|
|
381
381
|
`m.id IN (SELECT em.email_id FROM email_mailboxes em WHERE em.mailbox_id IN
|
|
382
382
|
(SELECT b.id FROM mailboxes b WHERE lower(b.external_id) = lower(?) OR b.name_folded = ?))`, value, fold(value))
|
|
383
383
|
: bound("0"));
|
|
384
|
+
// Mail stored in full keeps List-Id in its headers; mail only recorded, in its metadata.
|
|
385
|
+
const list = ({ value }, { mail }) => {
|
|
386
|
+
const wanted = value.trim().toLowerCase();
|
|
387
|
+
return always(mail
|
|
388
|
+
? bound(`m.id IN (SELECT id FROM (SELECT id, coalesce(json_extract(headers, '$."list-id"'),
|
|
389
|
+
json_extract(metadata, '$.listId')) AS list_id FROM emails) WHERE list_id = ? OR list_id LIKE '%.' || ? ESCAPE '\\')`, wanted, wanted.replace(/[\\%_]/g, "\\$&"))
|
|
390
|
+
: bound("0"));
|
|
391
|
+
};
|
|
384
392
|
const is = ({ value }, { mail }) => {
|
|
385
393
|
const state = value.toLowerCase();
|
|
386
394
|
return always(state === "starred"
|
|
@@ -441,6 +449,7 @@ export const FIELD_COMPILERS = {
|
|
|
441
449
|
source: notesOnly,
|
|
442
450
|
mailbox,
|
|
443
451
|
label: mailbox,
|
|
452
|
+
list,
|
|
444
453
|
is,
|
|
445
454
|
subject,
|
|
446
455
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
import type { MailFilter } from "./emails.js";
|
|
3
|
+
import type { Where } from "./query-filter.js";
|
|
4
|
+
/** A thread list query's columns. `subject:` finds a word or phrase at a word's start, `word*` included. */
|
|
5
|
+
export declare const THREAD_COLUMNS: {
|
|
6
|
+
readonly subject: {
|
|
7
|
+
readonly where: ({ value }: import("@wirecat/cli-base/query").Predicate) => {
|
|
8
|
+
sql: string;
|
|
9
|
+
params: string[];
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
readonly emails_count: {
|
|
13
|
+
readonly type: "num";
|
|
14
|
+
readonly sql: "email_threads.emails_count";
|
|
15
|
+
};
|
|
16
|
+
readonly last_email_at: {
|
|
17
|
+
readonly type: "time";
|
|
18
|
+
readonly sql: "email_threads.last_email_at";
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type ThreadFilter = MailFilter & {
|
|
22
|
+
where?: Where;
|
|
23
|
+
};
|
|
24
|
+
export declare const threadRows: (database: CacheDatabase, filter: ThreadFilter) => Record<string, unknown>[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { fold } from "../normalize.js";
|
|
2
|
+
import { page } from "./events.js";
|
|
3
|
+
/** A thread list query's columns. `subject:` finds a word or phrase at a word's start, `word*` included. */
|
|
4
|
+
export const THREAD_COLUMNS = {
|
|
5
|
+
subject: {
|
|
6
|
+
where: ({ value }) => ({
|
|
7
|
+
sql: "instr(' ' || coalesce(email_threads.subject_folded, lower(email_threads.subject)), ' ' || ?) > 0",
|
|
8
|
+
params: [fold(value).replace(/\*+$/, "")],
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
emails_count: { type: "num", sql: "email_threads.emails_count" },
|
|
12
|
+
last_email_at: { type: "time", sql: "email_threads.last_email_at" },
|
|
13
|
+
};
|
|
14
|
+
export const threadRows = (database, filter) => {
|
|
15
|
+
const [limit, offset] = page(filter);
|
|
16
|
+
return database
|
|
17
|
+
.prepare(`SELECT * FROM email_threads WHERE (? IS NULL OR account_id = ?) AND (? OR deleted_at IS NULL)
|
|
18
|
+
AND (${filter.where?.sql ?? "1"}) ORDER BY coalesce(last_email_at, 0) DESC, id DESC LIMIT ? OFFSET ?`)
|
|
19
|
+
.all(filter.accountId ?? null, filter.accountId ?? null, filter.includeDeleted ? 1 : 0, ...(filter.where?.params ?? []), limit, offset);
|
|
20
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.521.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",
|
|
@@ -129,6 +129,10 @@
|
|
|
129
129
|
"types": "./dist/mail/index.d.ts",
|
|
130
130
|
"default": "./dist/mail/index.js"
|
|
131
131
|
},
|
|
132
|
+
"./mail/cli": {
|
|
133
|
+
"types": "./dist/mail/cli/index.d.ts",
|
|
134
|
+
"default": "./dist/mail/cli/index.js"
|
|
135
|
+
},
|
|
132
136
|
"./mail/testing": {
|
|
133
137
|
"types": "./dist/mail/testing/index.d.ts",
|
|
134
138
|
"default": "./dist/mail/testing/index.js"
|