@wirecat/cli-core 0.486.0 → 0.488.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.
@@ -6,3 +6,5 @@ export declare const setEmailFlags: (database: CacheDatabase, accountId: number,
6
6
  read?: boolean;
7
7
  flagged?: boolean;
8
8
  }>, now: number) => number;
9
+ export declare const markEmailsDeleted: (database: CacheDatabase, accountId: number, externalIds: string[], now: number) => number;
10
+ export declare const markEmailsRestored: (database: CacheDatabase, accountId: number, externalIds: string[], now: number) => number;
@@ -1,4 +1,6 @@
1
+ import { dropAttachmentChunks } from "./email-index.js";
1
2
  import { flag } from "./events.js";
3
+ import { dropUnusedVectors } from "./vector-index.js";
2
4
  export const recountEmailThread = (database, threadId, now) => database
3
5
  .prepare(`UPDATE email_threads SET
4
6
  emails_count = (SELECT count(*) FROM emails WHERE email_thread_id = ? AND deleted_at IS NULL),
@@ -23,3 +25,29 @@ export const setEmailFlags = (database, accountId, flags, now) => {
23
25
  }
24
26
  return changed;
25
27
  };
28
+ export const markEmailsDeleted = (database, accountId, externalIds, now) => {
29
+ const mark = database.prepare(`UPDATE emails SET deleted_at = ?, body_text = NULL, body_html = NULL, snippet = NULL, updated_at = ?
30
+ WHERE account_id = ? AND external_id = ? AND deleted_at IS NULL
31
+ RETURNING id, email_thread_id`);
32
+ const forget = database.prepare("UPDATE attachments SET text = NULL, normalized_text = NULL WHERE attachable_type = 'email' AND attachable_id = ?");
33
+ const unlocate = database.prepare("DELETE FROM email_locations WHERE email_id = ?");
34
+ const threads = externalIds.flatMap((id) => mark.all(now, now, accountId, id).map((r) => {
35
+ const pieces = dropAttachmentChunks(database, Number(r.id));
36
+ forget.run(Number(r.id));
37
+ unlocate.run(Number(r.id));
38
+ dropUnusedVectors(database, pieces);
39
+ return Number(r.email_thread_id);
40
+ }));
41
+ for (const id of new Set(threads))
42
+ recountEmailThread(database, id, now);
43
+ return threads.length;
44
+ };
45
+ export const markEmailsRestored = (database, accountId, externalIds, now) => {
46
+ const unmark = database.prepare(`UPDATE emails SET deleted_at = NULL, updated_at = ?
47
+ WHERE account_id = ? AND external_id = ? AND deleted_at IS NOT NULL
48
+ RETURNING email_thread_id`);
49
+ const threads = externalIds.flatMap((id) => unmark.all(now, accountId, id).map((r) => Number(r.email_thread_id)));
50
+ for (const id of new Set(threads))
51
+ recountEmailThread(database, id, now);
52
+ return threads.length;
53
+ };
@@ -3,9 +3,11 @@ import type { Attachment, Metadata } from "../../meetings/index.js";
3
3
  import type { Stemmer, Stemmers } from "../../search/stem.js";
4
4
  import type { CacheDatabase } from "../driver.js";
5
5
  import type { ConversationSummary, StoredHit } from "../types.js";
6
+ import { type EmailLocationInput, type FolderEmail } from "./email-locations.js";
6
7
  import { type MailRulesStore } from "./mail-rules.js";
7
8
  import { type AttachmentInput } from "./meetings.js";
8
9
  import type { StoreContext } from "./open.js";
10
+ export type { EmailLocationInput, FolderEmail };
9
11
  export interface EmailAddress {
10
12
  address: string;
11
13
  name: string | null;
@@ -43,6 +45,8 @@ export interface EmailInput {
43
45
  mailRuleId?: number | null;
44
46
  /** Added to the mailboxes the email is already in; one scan sees one folder, so none is taken away. */
45
47
  mailboxes?: MailboxInput[];
48
+ /** Added to the locations the email has; a UID already known in that folder now points here. */
49
+ locations?: EmailLocationInput[];
46
50
  /** Replaced when given; the text is word-indexed. */
47
51
  attachments?: AttachmentInput[];
48
52
  }
@@ -228,6 +232,15 @@ export interface MailStore {
228
232
  read?: boolean;
229
233
  flagged?: boolean;
230
234
  }>, now: number): Promise<number>;
235
+ /** The live emails whose location in `folder` has this UIDVALIDITY, oldest UID first. */
236
+ folderEmails(accountId: number, folder: string, uidvalidity: string): Promise<FolderEmail[]>;
237
+ /**
238
+ * Records where emails the store holds are in `folder`, by Message-ID; one it does not hold is left out. Locations
239
+ * of the folder under another UIDVALIDITY are dropped: its UIDs mean nothing now.
240
+ */
241
+ setLocations(accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>): Promise<number>;
242
+ /** Forgets that these UIDs of `folder` hold anything; an email left with no location there leaves the folder too. */
243
+ forgetLocations(accountId: number, folder: string, uidvalidity: string, uids: number[]): Promise<void>;
231
244
  /** Marks emails gone at the source and drops their text; saving one again brings it back. Answers how many changed. */
232
245
  markDeleted(accountId: number, externalIds: string[], now: number): Promise<number>;
233
246
  /**
@@ -2,7 +2,8 @@ import { formatLocator } from "../../domain/locator.js";
2
2
  import { formatReference } from "../../domain/references.js";
3
3
  import { fold, normalize } from "../normalize.js";
4
4
  import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
5
- import { recountEmailThread as recount, setEmailFlags } from "./email-state.js";
5
+ import { folderEmails, forgetLocations, locate, setLocations, } from "./email-locations.js";
6
+ import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
6
7
  import { bool, flag, fromJson, int, page, str, toJson } from "./events.js";
7
8
  import { folded } from "./folded.js";
8
9
  import { ensurePerson } from "./identities.js";
@@ -228,6 +229,8 @@ const saveEmail = (database, orm, accountId, threadId, mail, now) => {
228
229
  }
229
230
  for (const box of mail.mailboxes ?? [])
230
231
  join(database, emailId, mailboxPk(database, accountId, box, now), now);
232
+ for (const where of mail.locations ?? [])
233
+ locate(database, accountId, emailId, where);
231
234
  if (mail.attachments !== undefined) {
232
235
  const before = dropAttachmentChunks(database, emailId);
233
236
  const positions = mail.attachments.map(({ position }) => position);
@@ -545,34 +548,11 @@ export const mailStoreOver = ({ database, orm }, stemmerFor = stemmerCache()) =>
545
548
  return setEmailFlags(database, accountId, flags, now);
546
549
  });
547
550
  },
548
- async markDeleted(accountId, externalIds, now) {
549
- return write(database, () => {
550
- const mark = database.prepare(`UPDATE emails SET deleted_at = ?, body_text = NULL, body_html = NULL, snippet = NULL, updated_at = ?
551
- WHERE account_id = ? AND external_id = ? AND deleted_at IS NULL
552
- RETURNING id, email_thread_id`);
553
- const forget = database.prepare("UPDATE attachments SET text = NULL, normalized_text = NULL WHERE attachable_type = 'email' AND attachable_id = ?");
554
- const threads = externalIds.flatMap((id) => mark.all(now, now, accountId, id).map((r) => {
555
- const pieces = dropAttachmentChunks(database, Number(r.id));
556
- forget.run(Number(r.id));
557
- dropUnusedVectors(database, pieces);
558
- return Number(r.email_thread_id);
559
- }));
560
- for (const id of new Set(threads))
561
- recount(database, id, now);
562
- return threads.length;
563
- });
564
- },
565
- async markRestored(accountId, externalIds, now) {
566
- return write(database, () => {
567
- const unmark = database.prepare(`UPDATE emails SET deleted_at = NULL, updated_at = ?
568
- WHERE account_id = ? AND external_id = ? AND deleted_at IS NOT NULL
569
- RETURNING email_thread_id`);
570
- const threads = externalIds.flatMap((id) => unmark.all(now, accountId, id).map((r) => Number(r.email_thread_id)));
571
- for (const id of new Set(threads))
572
- recount(database, id, now);
573
- return threads.length;
574
- });
575
- },
551
+ folderEmails: async (accountId, folder, uidvalidity) => folderEmails(database, accountId, folder, uidvalidity),
552
+ setLocations: async (accountId, folder, uidvalidity, found) => write(database, () => setLocations(database, accountId, folder, uidvalidity, found)),
553
+ forgetLocations: async (accountId, folder, uidvalidity, uids) => write(database, () => forgetLocations(database, accountId, folder, uidvalidity, uids)),
554
+ markDeleted: async (accountId, externalIds, now) => write(database, () => markEmailsDeleted(database, accountId, externalIds, now)),
555
+ markRestored: async (accountId, externalIds, now) => write(database, () => markEmailsRestored(database, accountId, externalIds, now)),
576
556
  async chunksToEmbed(model, { after, limit }) {
577
557
  drainEmailIndex(database, stemmerFor);
578
558
  const file = database.prepare("SELECT text FROM attachments WHERE id = ?");
@@ -52,6 +52,7 @@ export const MANIFEST = [
52
52
  { name: "20261029181000_version-37-contact-mention-triggers", version: 37, minCompatible: 34 },
53
53
  { name: "20261029224106_version-38-folded-names", version: 38, minCompatible: 34 },
54
54
  { name: "20261029224107_version-38-folded-triggers", version: 38, minCompatible: 34 },
55
+ { name: "20261030114008_version-39-email-locations", version: 39, minCompatible: 34 },
55
56
  ];
56
57
  export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
57
58
  const migrations = [];
@@ -830,5 +830,12 @@ export const GENERATED = [
830
830
  "CREATE TRIGGER email_threads_subject_folded_au AFTER UPDATE OF subject ON email_threads\n WHEN new.subject IS NOT old.subject AND new.subject_folded IS old.subject_folded BEGIN\n UPDATE email_threads SET subject_folded = NULL WHERE id = new.id;\nEND;",
831
831
  "CREATE TRIGGER mailboxes_name_folded_au AFTER UPDATE OF name ON mailboxes\n WHEN new.name IS NOT old.name AND new.name_folded IS old.name_folded BEGIN\n UPDATE mailboxes SET name_folded = NULL WHERE id = new.id;\nEND;"
832
832
  ]
833
+ },
834
+ {
835
+ "name": "20261030114008_version-39-email-locations",
836
+ "statements": [
837
+ "CREATE TABLE `email_locations` (\n\t`mailbox_id` integer NOT NULL,\n\t`uidvalidity` text NOT NULL,\n\t`uid` integer NOT NULL,\n\t`email_id` integer NOT NULL,\n\tCONSTRAINT `email_locations_pk` PRIMARY KEY(`mailbox_id`, `uidvalidity`, `uid`),\n\tCONSTRAINT `fk_email_locations_mailbox_id_mailboxes_id_fk` FOREIGN KEY (`mailbox_id`) REFERENCES `mailboxes`(`id`),\n\tCONSTRAINT `fk_email_locations_email_id_emails_id_fk` FOREIGN KEY (`email_id`) REFERENCES `emails`(`id`)\n);",
838
+ "CREATE INDEX `email_locations_by_email_id` ON `email_locations` (`email_id`);"
839
+ ]
833
840
  }
834
841
  ];
@@ -6172,6 +6172,78 @@ export declare const emailMailboxes: import("drizzle-orm/sqlite-core").SQLiteTab
6172
6172
  dialect: 'sqlite';
6173
6173
  isAlias: false;
6174
6174
  }>;
6175
+ export declare const emailLocations: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
6176
+ name: "email_locations";
6177
+ schema: undefined;
6178
+ columns: {
6179
+ mailboxId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6180
+ name: string;
6181
+ tableName: "email_locations";
6182
+ dataType: "number int53";
6183
+ data: number;
6184
+ driverParam: number;
6185
+ notNull: true;
6186
+ hasDefault: false;
6187
+ isPrimaryKey: false;
6188
+ isAutoincrement: false;
6189
+ hasRuntimeDefault: false;
6190
+ enumValues: undefined;
6191
+ baseColumn: never;
6192
+ identity: undefined;
6193
+ generated: undefined;
6194
+ }, {}>;
6195
+ uidvalidity: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6196
+ name: string;
6197
+ tableName: "email_locations";
6198
+ dataType: "string";
6199
+ data: string;
6200
+ driverParam: string;
6201
+ notNull: true;
6202
+ hasDefault: false;
6203
+ isPrimaryKey: false;
6204
+ isAutoincrement: false;
6205
+ hasRuntimeDefault: false;
6206
+ enumValues: [string, ...string[]];
6207
+ baseColumn: never;
6208
+ identity: undefined;
6209
+ generated: undefined;
6210
+ }, {}>;
6211
+ uid: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6212
+ name: string;
6213
+ tableName: "email_locations";
6214
+ dataType: "number int53";
6215
+ data: number;
6216
+ driverParam: number;
6217
+ notNull: true;
6218
+ hasDefault: false;
6219
+ isPrimaryKey: false;
6220
+ isAutoincrement: false;
6221
+ hasRuntimeDefault: false;
6222
+ enumValues: undefined;
6223
+ baseColumn: never;
6224
+ identity: undefined;
6225
+ generated: undefined;
6226
+ }, {}>;
6227
+ emailId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6228
+ name: string;
6229
+ tableName: "email_locations";
6230
+ dataType: "number int53";
6231
+ data: number;
6232
+ driverParam: number;
6233
+ notNull: true;
6234
+ hasDefault: false;
6235
+ isPrimaryKey: false;
6236
+ isAutoincrement: false;
6237
+ hasRuntimeDefault: false;
6238
+ enumValues: undefined;
6239
+ baseColumn: never;
6240
+ identity: undefined;
6241
+ generated: undefined;
6242
+ }, {}>;
6243
+ };
6244
+ dialect: 'sqlite';
6245
+ isAlias: false;
6246
+ }>;
6175
6247
  export declare const emailIndexPending: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
6176
6248
  name: "email_index_pending";
6177
6249
  schema: undefined;
@@ -695,6 +695,19 @@ export const emailMailboxes = sqliteTable("email_mailboxes", {
695
695
  primaryKey({ columns: [table.emailId, table.mailboxId] }),
696
696
  index("email_mailboxes_by_mailbox_id").on(table.mailboxId),
697
697
  ]);
698
+ export const emailLocations = sqliteTable("email_locations", {
699
+ mailboxId: integer("mailbox_id")
700
+ .notNull()
701
+ .references(() => mailboxes.id),
702
+ uidvalidity: text("uidvalidity").notNull(),
703
+ uid: integer("uid").notNull(),
704
+ emailId: integer("email_id")
705
+ .notNull()
706
+ .references(() => emails.id),
707
+ }, (table) => [
708
+ primaryKey({ columns: [table.mailboxId, table.uidvalidity, table.uid] }),
709
+ index("email_locations_by_email_id").on(table.emailId),
710
+ ]);
698
711
  export const emailIndexPending = sqliteTable("email_index_pending", {
699
712
  id: integer("id").notNull(),
700
713
  indexableType: text("indexable_type").notNull(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.486.0",
3
+ "version": "0.488.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",