@bobfrankston/mailx-store 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/db.d.ts CHANGED
@@ -4,12 +4,30 @@
4
4
  * Message bodies are NOT here -- they live in the MessageStore backend.
5
5
  */
6
6
  import type { MessageEnvelope, Folder, EmailAddress, PagedResult, MessageQuery } from "@bobfrankston/mailx-types";
7
+ /** User-configured patterns from contacts.jsonc `denylistPatterns`. Compiled
8
+ * once on settings load; invalid regexes are skipped with a warning. */
9
+ export declare function setContactsDenyPatterns(patterns: string[]): void;
7
10
  export declare class MailxDB {
8
11
  private db;
9
12
  constructor(dbDir: string);
10
13
  /** Fail loud + early if expected columns are missing. Cheap (PRAGMA only
11
14
  * runs at startup). The user-facing message names the recovery command. */
12
15
  private verifySchema;
16
+ /** One-shot: rewrite absolute body_path entries to relative-to-store
17
+ * paths. Idempotent; flag stored in kv. Caller passes a FileMessageStore
18
+ * whose `rewriteAbsoluteToRelative` does the actual mapping (mailx-store
19
+ * doesn't depend on file-store directly to avoid a cycle). */
20
+ runOneShotBodyPathMigration(rewriteFn: (rows: Array<{
21
+ id: number;
22
+ body_path: string;
23
+ }>, update: (id: number, p: string) => void) => number): void;
24
+ /** One-shot purge: when the in-DB filter rules tighten (new prefix or
25
+ * domain matchers), historical contacts harvested under the old rules
26
+ * remain in the table and keep being written back to contacts.jsonc on
27
+ * every cloud save. Bumping the version below re-runs the purge once
28
+ * per device. The kv flag (`contacts:purge_v`) records the last version
29
+ * that ran so we don't keep doing the work on every startup. */
30
+ private runOneShotJunkContactPurge;
13
31
  /** Fetch a string from the kv table. Returns null when not set. */
14
32
  getKv(scope: string, key: string): string | null;
15
33
  /** Upsert a kv row. Pass `null` to delete. */
@@ -201,6 +219,17 @@ export declare class MailxDB {
201
219
  private _denylist;
202
220
  isAddressDenylisted(emailLower: string): boolean;
203
221
  setContactsDenylist(emails: string[]): void;
222
+ /** Priority sender / domain index — populated from contacts.jsonc on reload.
223
+ * Used by the client to flag list rows. Source of truth is the JSONC; this
224
+ * is just a fast in-memory lookup. */
225
+ private _prioritySenders;
226
+ private _priorityDomains;
227
+ setPriorityIndex(senders: string[], domains: string[]): void;
228
+ getPriorityIndex(): {
229
+ senders: string[];
230
+ domains: string[];
231
+ };
232
+ isPrioritySender(addr: string): boolean;
204
233
  /** Callback fired when local-DB contacts mutations happen (sends adding
205
234
  * to discovered, corpus seeder finding new addresses). The service
206
235
  * registers a debounced cloud flush here so the GDrive copy stays in
@@ -237,8 +266,11 @@ export declare class MailxDB {
237
266
  source?: string;
238
267
  organization?: string;
239
268
  org?: string;
269
+ priority?: boolean;
240
270
  }[];
241
271
  denylist?: string[];
272
+ denylistPatterns?: string[];
273
+ priorityDomains?: string[];
242
274
  discovered?: {
243
275
  name?: string;
244
276
  email: string;
package/db.js CHANGED
@@ -7,18 +7,33 @@ import { DatabaseSync } from "node:sqlite";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import * as path from "node:path";
9
9
  import * as fs from "node:fs";
10
- /** Addresses that have no business in autocomplete. Standard automated
11
- * sender patterns plus name-side hints ("MAILER-DAEMON" etc.). The exact
12
- * match list keeps surprise low; the regex catches the long tail of
13
- * *-bounces@, no-reply variants, and listserv-style addresses. */
14
- const JUNK_LOCAL_RE = /^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$/i;
15
- const JUNK_LOCAL_SUFFIX_RE = /(-bounces|\+bounces|-noreply|-no-reply|-notifications?|-mailer)$/i;
10
+ import { CONTACT_RULES } from "@bobfrankston/mailx-types";
11
+ /** Addresses that have no business in autocomplete. Patterns load from
12
+ * contact-rules.jsonc in mailx-types a single source of truth shipped
13
+ * with every release. To change the rules edit the JSONC and rebuild. */
14
+ const JUNK_LOCAL_RE = new RegExp(CONTACT_RULES.junk.localExact, "i");
15
+ const JUNK_LOCAL_SUFFIX_RE = new RegExp(CONTACT_RULES.junk.localSuffix, "i");
16
+ const JUNK_LOCAL_PREFIX_RE = new RegExp(CONTACT_RULES.junk.localPrefix, "i");
17
+ const JUNK_DOMAIN_RE = new RegExp(CONTACT_RULES.junk.domain, "i");
18
+ const JUNK_LOCAL_ONEOFF_RE = new RegExp(CONTACT_RULES.junk.localOneoff, "i");
19
+ /** Optional user-configurable pattern list, refreshed when contacts.jsonc
20
+ * reloads. Each entry is a regex compiled lazily in setContactsDenyPatterns. */
21
+ let _denyPatterns = [];
16
22
  function isJunkContact(email, name) {
17
- const local = email.split("@")[0] || "";
23
+ const lower = (email || "").toLowerCase();
24
+ const at = lower.indexOf("@");
25
+ const local = at >= 0 ? lower.slice(0, at) : lower;
26
+ const domain = at >= 0 ? lower.slice(at + 1) : "";
18
27
  if (JUNK_LOCAL_RE.test(local))
19
28
  return true;
20
29
  if (JUNK_LOCAL_SUFFIX_RE.test(local))
21
30
  return true;
31
+ if (JUNK_LOCAL_PREFIX_RE.test(local))
32
+ return true;
33
+ if (JUNK_LOCAL_ONEOFF_RE.test(local))
34
+ return true;
35
+ if (domain && JUNK_DOMAIN_RE.test(domain))
36
+ return true;
22
37
  // Bare numeric / hex addresses (rotating IDs from automated systems)
23
38
  // — three or fewer chars is too short to be useful regardless.
24
39
  if (local.length < 2)
@@ -26,8 +41,27 @@ function isJunkContact(email, name) {
26
41
  const lname = (name || "").trim().toLowerCase();
27
42
  if (lname.includes("mailer-daemon") || lname.includes("postmaster"))
28
43
  return true;
44
+ for (const re of _denyPatterns) {
45
+ if (re.test(lower))
46
+ return true;
47
+ }
29
48
  return false;
30
49
  }
50
+ /** User-configured patterns from contacts.jsonc `denylistPatterns`. Compiled
51
+ * once on settings load; invalid regexes are skipped with a warning. */
52
+ export function setContactsDenyPatterns(patterns) {
53
+ _denyPatterns = [];
54
+ for (const p of patterns) {
55
+ if (!p)
56
+ continue;
57
+ try {
58
+ _denyPatterns.push(new RegExp(p, "i"));
59
+ }
60
+ catch (e) {
61
+ console.warn(`[contacts] invalid denylistPattern "${p}": ${e.message}`);
62
+ }
63
+ }
64
+ }
31
65
  const SCHEMA = `
32
66
  CREATE TABLE IF NOT EXISTS accounts (
33
67
  id TEXT PRIMARY KEY,
@@ -364,6 +398,58 @@ export class MailxDB {
364
398
  throw new Error(`[mailx-store] table "${table}" is missing columns [${missing.join(", ")}] — schema migration did not complete. Run 'mailx -rebuild' to rebuild the local store.`);
365
399
  }
366
400
  }
401
+ this.runOneShotJunkContactPurge();
402
+ }
403
+ /** One-shot: rewrite absolute body_path entries to relative-to-store
404
+ * paths. Idempotent; flag stored in kv. Caller passes a FileMessageStore
405
+ * whose `rewriteAbsoluteToRelative` does the actual mapping (mailx-store
406
+ * doesn't depend on file-store directly to avoid a cycle). */
407
+ runOneShotBodyPathMigration(rewriteFn) {
408
+ const TARGET = "v1-relative";
409
+ const last = this.getKv("body_paths", "migrate_v");
410
+ if (last === TARGET)
411
+ return;
412
+ try {
413
+ const rows = this.db.prepare("SELECT id, body_path FROM messages WHERE body_path IS NOT NULL AND body_path != ''").all();
414
+ const upd = this.db.prepare("UPDATE messages SET body_path = ? WHERE id = ?");
415
+ const update = (id, newPath) => upd.run(newPath, id);
416
+ const n = rewriteFn(rows, update);
417
+ this.setKv("body_paths", "migrate_v", TARGET);
418
+ if (n > 0)
419
+ console.log(` [body-paths] one-shot: rewrote ${n} of ${rows.length} body_path entries to relative`);
420
+ }
421
+ catch (e) {
422
+ console.warn(`[body-paths] one-shot migration failed: ${e.message}`);
423
+ }
424
+ }
425
+ /** One-shot purge: when the in-DB filter rules tighten (new prefix or
426
+ * domain matchers), historical contacts harvested under the old rules
427
+ * remain in the table and keep being written back to contacts.jsonc on
428
+ * every cloud save. Bumping the version below re-runs the purge once
429
+ * per device. The kv flag (`contacts:purge_v`) records the last version
430
+ * that ran so we don't keep doing the work on every startup. */
431
+ runOneShotJunkContactPurge() {
432
+ const TARGET = CONTACT_RULES.rulesVersion; // bump in contact-rules.jsonc to re-run
433
+ const last = this.getKv("contacts", "purge_v");
434
+ if (last === TARGET)
435
+ return;
436
+ let dropped = 0;
437
+ try {
438
+ const rows = this.db.prepare("SELECT id, email, name FROM contacts WHERE source IN ('discovered','sent','received')").all();
439
+ const del = this.db.prepare("DELETE FROM contacts WHERE id = ?");
440
+ for (const r of rows) {
441
+ if (isJunkContact(r.email || "", r.name || "")) {
442
+ del.run(r.id);
443
+ dropped++;
444
+ }
445
+ }
446
+ this.setKv("contacts", "purge_v", TARGET);
447
+ if (dropped > 0)
448
+ console.log(` [contacts] one-shot purge: dropped ${dropped} junk rows (${TARGET})`);
449
+ }
450
+ catch (e) {
451
+ console.warn(`[contacts] one-shot purge failed: ${e.message}`);
452
+ }
367
453
  }
368
454
  /** Fetch a string from the kv table. Returns null when not set. */
369
455
  getKv(scope, key) {
@@ -1150,6 +1236,8 @@ export class MailxDB {
1150
1236
  const lower = email.toLowerCase();
1151
1237
  if (this.isAddressDenylisted(lower))
1152
1238
  return;
1239
+ if (isJunkContact(lower, name))
1240
+ return;
1153
1241
  const now = Date.now();
1154
1242
  // discovered tier holds one row per email — bump if present, else
1155
1243
  // insert. Doesn't touch preferred or google rows for the same email;
@@ -1172,6 +1260,29 @@ export class MailxDB {
1172
1260
  setContactsDenylist(emails) {
1173
1261
  this._denylist = new Set(emails.map(e => (e || "").trim().toLowerCase()).filter(Boolean));
1174
1262
  }
1263
+ /** Priority sender / domain index — populated from contacts.jsonc on reload.
1264
+ * Used by the client to flag list rows. Source of truth is the JSONC; this
1265
+ * is just a fast in-memory lookup. */
1266
+ _prioritySenders = new Set();
1267
+ _priorityDomains = new Set();
1268
+ setPriorityIndex(senders, domains) {
1269
+ this._prioritySenders = new Set((senders || []).map(s => (s || "").trim().toLowerCase()).filter(Boolean));
1270
+ this._priorityDomains = new Set((domains || []).map(d => (d || "").trim().toLowerCase()).filter(Boolean));
1271
+ }
1272
+ getPriorityIndex() {
1273
+ return {
1274
+ senders: Array.from(this._prioritySenders).sort(),
1275
+ domains: Array.from(this._priorityDomains).sort(),
1276
+ };
1277
+ }
1278
+ isPrioritySender(addr) {
1279
+ const lower = (addr || "").trim().toLowerCase();
1280
+ if (this._prioritySenders.has(lower))
1281
+ return true;
1282
+ const at = lower.indexOf("@");
1283
+ const domain = at >= 0 ? lower.slice(at + 1) : "";
1284
+ return !!domain && this._priorityDomains.has(domain);
1285
+ }
1175
1286
  /** Callback fired when local-DB contacts mutations happen (sends adding
1176
1287
  * to discovered, corpus seeder finding new addresses). The service
1177
1288
  * registers a debounced cloud flush here so the GDrive copy stays in
@@ -1312,8 +1423,18 @@ export class MailxDB {
1312
1423
  applyContactsConfig(cfg) {
1313
1424
  const preferred = Array.isArray(cfg.preferred) ? cfg.preferred : [];
1314
1425
  const denylist = Array.isArray(cfg.denylist) ? cfg.denylist : [];
1426
+ const denylistPatterns = Array.isArray(cfg.denylistPatterns) ? cfg.denylistPatterns : [];
1427
+ const priorityDomains = Array.isArray(cfg.priorityDomains) ? cfg.priorityDomains : [];
1315
1428
  const discovered = Array.isArray(cfg.discovered) ? cfg.discovered : [];
1316
1429
  this.setContactsDenylist(denylist);
1430
+ setContactsDenyPatterns(denylistPatterns);
1431
+ // Priority index — preferred[] entries with priority:true plus the
1432
+ // top-level priorityDomains[]. Address book and visual highlight
1433
+ // are independent: not every preferred contact is priority.
1434
+ const prioritySenders = preferred
1435
+ .filter(e => e && e.priority === true && e.email)
1436
+ .map(e => e.email);
1437
+ this.setPriorityIndex(prioritySenders, priorityDomains);
1317
1438
  // Wipe and rewrite preferred-tier rows owned by contacts.jsonc.
1318
1439
  // The address-book UI's legacy `upsertContact` still writes
1319
1440
  // source='manual' rows; those are owned by the address-book code
package/file-store.d.ts CHANGED
@@ -14,20 +14,38 @@ import type { MessageStore } from "@bobfrankston/mailx-types";
14
14
  export declare class FileMessageStore implements MessageStore {
15
15
  private basePath;
16
16
  constructor(basePath: string);
17
- /** Fresh opaque path per call. No inputs from the caller affect the name. */
18
- private newMessagePath;
19
- /** Verify a given path resolves inside this store's basePath — refuses
20
- * any value that doesn't (cheap directory-traversal guard). */
21
- private inStore;
22
- /** Write a new body. Always a fresh UUID path. Caller MUST persist the
23
- * returned path in the DB (`body_path`) and use it for all reads. The
24
- * (folderId, uid) args are kept for interface compatibility; they do
25
- * NOT affect the filename. */
17
+ /** Fresh opaque path per call, returned RELATIVE to `basePath`. The DB
18
+ * stores this relative form so the body store can be relocated (or
19
+ * the user folder renamed e.g. `.mailx .rmfmail`) without the
20
+ * body_path entries breaking. */
21
+ private newRelativePath;
22
+ /** Resolve a stored path. Accepts either a relative path (post-2026-05
23
+ * format) or an absolute path (legacy entries until next-launch
24
+ * migration rewrites them). Either way, refuse anything outside the
25
+ * store as a directory-traversal guard. */
26
+ private resolveStored;
27
+ /** Write a new body. Returns a path RELATIVE to basePath; caller stores
28
+ * it in `body_path`. The (folderId, uid) args are kept for interface
29
+ * compatibility; they do NOT affect the filename. */
26
30
  putMessage(accountId: string, _folderId: number, _uid: number, raw: Buffer): Promise<string>;
27
- /** Read by absolute path (DB `body_path`). The primary read API. */
28
- readByPath(fullPath: string): Promise<Buffer>;
29
- hasByPath(fullPath: string): Promise<boolean>;
30
- unlinkByPath(fullPath: string): Promise<void>;
31
+ /** Read by stored path (relative or absolute). */
32
+ readByPath(stored: string): Promise<Buffer>;
33
+ hasByPath(stored: string): Promise<boolean>;
34
+ unlinkByPath(stored: string): Promise<void>;
35
+ /** One-shot migration: rewrite absolute body_path values that point at
36
+ * this store's basePath into relative form. Called once from db.ts on
37
+ * startup (gated by a kv flag); silently skipped after first run.
38
+ * Returns the count of rewritten paths.
39
+ *
40
+ * Bodies whose absolute path is OUTSIDE basePath (the legacy
41
+ * `~/.mailx/mailxstore/...` entries on a machine where basePath is
42
+ * now `~/.rmfmail/mailxstore`) are left alone — the migration in
43
+ * mailx-settings already moves the files; this rewrite is purely
44
+ * about getting body_path to a relative shape. */
45
+ rewriteAbsoluteToRelative(rows: Array<{
46
+ id: number;
47
+ body_path: string;
48
+ }>, update: (id: number, newPath: string) => void): number;
31
49
  getMessagePath(_accountId: string, _folderId: number, _uid: number): string;
32
50
  getMessage(_accountId: string, _folderId: number, _uid: number): Promise<Buffer>;
33
51
  hasMessage(_accountId: string, _folderId: number, _uid: number): Promise<boolean>;
package/file-store.js CHANGED
@@ -19,46 +19,90 @@ export class FileMessageStore {
19
19
  this.basePath = basePath;
20
20
  fs.mkdirSync(basePath, { recursive: true });
21
21
  }
22
- /** Fresh opaque path per call. No inputs from the caller affect the name. */
23
- newMessagePath(accountId) {
22
+ /** Fresh opaque path per call, returned RELATIVE to `basePath`. The DB
23
+ * stores this relative form so the body store can be relocated (or
24
+ * the user folder renamed — e.g. `.mailx → .rmfmail`) without the
25
+ * body_path entries breaking. */
26
+ newRelativePath(accountId) {
24
27
  const uuid = randomUUID().replace(/-/g, "");
25
28
  const prefix = uuid.slice(0, 2);
26
- return path.join(this.basePath, accountId, prefix, `${uuid}.eml`);
29
+ return path.join(accountId, prefix, `${uuid}.eml`);
27
30
  }
28
- /** Verify a given path resolves inside this store's basePath — refuses
29
- * any value that doesn't (cheap directory-traversal guard). */
30
- inStore(fullPath) {
31
- if (!fullPath)
32
- return false;
33
- const rel = path.relative(path.resolve(this.basePath), path.resolve(fullPath));
34
- return !rel.startsWith("..") && !path.isAbsolute(rel);
31
+ /** Resolve a stored path. Accepts either a relative path (post-2026-05
32
+ * format) or an absolute path (legacy entries until next-launch
33
+ * migration rewrites them). Either way, refuse anything outside the
34
+ * store as a directory-traversal guard. */
35
+ resolveStored(p) {
36
+ if (!p)
37
+ return "";
38
+ const abs = path.isAbsolute(p) ? p : path.resolve(this.basePath, p);
39
+ const rel = path.relative(path.resolve(this.basePath), abs);
40
+ if (rel.startsWith("..") || path.isAbsolute(rel))
41
+ return "";
42
+ return abs;
35
43
  }
36
- /** Write a new body. Always a fresh UUID path. Caller MUST persist the
37
- * returned path in the DB (`body_path`) and use it for all reads. The
38
- * (folderId, uid) args are kept for interface compatibility; they do
39
- * NOT affect the filename. */
44
+ /** Write a new body. Returns a path RELATIVE to basePath; caller stores
45
+ * it in `body_path`. The (folderId, uid) args are kept for interface
46
+ * compatibility; they do NOT affect the filename. */
40
47
  async putMessage(accountId, _folderId, _uid, raw) {
41
- const filePath = this.newMessagePath(accountId);
42
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
- fs.writeFileSync(filePath, raw);
44
- return filePath;
48
+ const rel = this.newRelativePath(accountId);
49
+ const abs = path.join(this.basePath, rel);
50
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
51
+ fs.writeFileSync(abs, raw);
52
+ return rel;
45
53
  }
46
- /** Read by absolute path (DB `body_path`). The primary read API. */
47
- async readByPath(fullPath) {
48
- if (!this.inStore(fullPath))
49
- throw new Error(`refusing to read outside store: ${fullPath}`);
50
- return fs.readFileSync(fullPath);
54
+ /** Read by stored path (relative or absolute). */
55
+ async readByPath(stored) {
56
+ const abs = this.resolveStored(stored);
57
+ if (!abs)
58
+ throw new Error(`refusing to read outside store: ${stored}`);
59
+ return fs.readFileSync(abs);
51
60
  }
52
- async hasByPath(fullPath) {
53
- if (!this.inStore(fullPath))
54
- return false;
55
- return fs.existsSync(fullPath);
61
+ async hasByPath(stored) {
62
+ const abs = this.resolveStored(stored);
63
+ return !!abs && fs.existsSync(abs);
56
64
  }
57
- async unlinkByPath(fullPath) {
58
- if (!this.inStore(fullPath))
65
+ async unlinkByPath(stored) {
66
+ const abs = this.resolveStored(stored);
67
+ if (!abs)
59
68
  return;
60
- if (fs.existsSync(fullPath))
61
- fs.unlinkSync(fullPath);
69
+ if (fs.existsSync(abs))
70
+ fs.unlinkSync(abs);
71
+ }
72
+ /** One-shot migration: rewrite absolute body_path values that point at
73
+ * this store's basePath into relative form. Called once from db.ts on
74
+ * startup (gated by a kv flag); silently skipped after first run.
75
+ * Returns the count of rewritten paths.
76
+ *
77
+ * Bodies whose absolute path is OUTSIDE basePath (the legacy
78
+ * `~/.mailx/mailxstore/...` entries on a machine where basePath is
79
+ * now `~/.rmfmail/mailxstore`) are left alone — the migration in
80
+ * mailx-settings already moves the files; this rewrite is purely
81
+ * about getting body_path to a relative shape. */
82
+ rewriteAbsoluteToRelative(rows, update) {
83
+ let n = 0;
84
+ const baseAbs = path.resolve(this.basePath);
85
+ for (const r of rows) {
86
+ if (!r.body_path || !path.isAbsolute(r.body_path))
87
+ continue;
88
+ const rel = path.relative(baseAbs, path.resolve(r.body_path));
89
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
90
+ // Outside this store — try mapping `.mailx → .rmfmail` if the
91
+ // legacy dir was the source. Then re-test.
92
+ const remapped = r.body_path.replace(/[\\/]\.mailx[\\/]mailxstore[\\/]/i, `${path.sep}.rmfmail${path.sep}mailxstore${path.sep}`);
93
+ if (remapped !== r.body_path) {
94
+ const rel2 = path.relative(baseAbs, path.resolve(remapped));
95
+ if (!rel2.startsWith("..") && !path.isAbsolute(rel2)) {
96
+ update(r.id, rel2.replace(/\\/g, "/"));
97
+ n++;
98
+ }
99
+ }
100
+ continue;
101
+ }
102
+ update(r.id, rel.replace(/\\/g, "/"));
103
+ n++;
104
+ }
105
+ return n;
62
106
  }
63
107
  // MessageStore interface compatibility (unused once all callers migrate to
64
108
  // path-based reads). These used to compose {folderId}/{uid}.eml and they
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -9,21 +9,24 @@
9
9
  },
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
- "@bobfrankston/mailx-types": "^0.1.5",
13
- "@bobfrankston/mailx-settings": "^0.1.6"
12
+ "@bobfrankston/mailx-types": "^0.1.6",
13
+ "@bobfrankston/mailx-settings": "^0.1.7"
14
14
  },
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "https://github.com/BobFrankston/mailx-store.git"
18
18
  },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
19
22
  ".dependencies": {
20
23
  "@bobfrankston/mailx-types": "file:../mailx-types",
21
24
  "@bobfrankston/mailx-settings": "file:../mailx-settings"
22
25
  },
23
26
  ".transformedSnapshot": {
24
27
  "dependencies": {
25
- "@bobfrankston/mailx-types": "^0.1.5",
26
- "@bobfrankston/mailx-settings": "^0.1.6"
28
+ "@bobfrankston/mailx-types": "^0.1.6",
29
+ "@bobfrankston/mailx-settings": "^0.1.7"
27
30
  }
28
31
  }
29
32
  }