@zaclinley/dsh-mailharbor 0.12.0 → 0.12.1

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/lib/index.js CHANGED
@@ -170,8 +170,15 @@ function flagsOf(value) {
170
170
  function isSeen(flags) {
171
171
  return flags.some((flag) => flag.toLowerCase() === "\\seen");
172
172
  }
173
+ function isInlineImage(attachment) {
174
+ const type = String(attachment.contentType ?? "").toLowerCase();
175
+ if (!type.startsWith("image/")) return false;
176
+ const disposition = String(attachment.contentDisposition ?? "").toLowerCase();
177
+ const cid = String(attachment.cid ?? "");
178
+ return disposition === "inline" || cid !== "" && disposition !== "attachment";
179
+ }
173
180
  function attachmentList(parsed) {
174
- return (parsed.attachments ?? []).map((attachment, index) => ({
181
+ return (parsed.attachments ?? []).filter((attachment) => !isInlineImage(attachment)).map((attachment, index) => ({
175
182
  id: String(index),
176
183
  filename: String(attachment.filename ?? "attachment"),
177
184
  contentType: String(attachment.contentType ?? "application/octet-stream"),
@@ -992,6 +999,13 @@ function ensureMailSchema(file) {
992
999
  db.exec(FTS);
993
1000
  } catch {
994
1001
  }
1002
+ const columns = db.prepare("PRAGMA table_info(attachments)").all();
1003
+ if (!columns.some((column) => column.name === "content_id")) {
1004
+ db.exec("ALTER TABLE attachments ADD COLUMN content_id TEXT");
1005
+ }
1006
+ if (!columns.some((column) => column.name === "disposition")) {
1007
+ db.exec("ALTER TABLE attachments ADD COLUMN disposition TEXT");
1008
+ }
995
1009
  } finally {
996
1010
  db.close();
997
1011
  }
@@ -1019,6 +1033,27 @@ var MailDatabase = class {
1019
1033
  this.db.exec("PRAGMA busy_timeout = 3000");
1020
1034
  return this.db;
1021
1035
  }
1036
+ /** Whether the backup has added inline-image columns yet. */
1037
+ hasAttachmentColumns() {
1038
+ try {
1039
+ const columns = this.open().prepare("PRAGMA table_info(attachments)").all();
1040
+ return columns.some((column) => column.name === "content_id");
1041
+ } catch {
1042
+ return false;
1043
+ }
1044
+ }
1045
+ attachmentCountSql() {
1046
+ if (!this.hasAttachmentColumns()) {
1047
+ return "(SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count";
1048
+ }
1049
+ return `(SELECT COUNT(*) FROM attachments a
1050
+ WHERE a.email_id = e.id
1051
+ AND NOT (a.content_type LIKE 'image/%' AND (
1052
+ a.disposition = 'inline'
1053
+ OR (a.content_id IS NOT NULL AND a.content_id <> '' AND a.disposition IS NOT 'attachment')
1054
+ OR lower(a.filename) GLOB 'image[0-9]*'
1055
+ ))) AS attachment_count`;
1056
+ }
1022
1057
  close() {
1023
1058
  if (this.db === void 0) return;
1024
1059
  try {
@@ -1089,7 +1124,7 @@ var MailDatabase = class {
1089
1124
  const clause = where.length === 0 ? "" : `WHERE ${where.join(" AND ")}`;
1090
1125
  const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
1091
1126
  e.message_id, e.date, length(e.body_text) AS text_length,
1092
- (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
1127
+ ${this.attachmentCountSql()},
1093
1128
  substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`;
1094
1129
  const total = Number(
1095
1130
  db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0
@@ -1144,7 +1179,7 @@ var MailDatabase = class {
1144
1179
  const clause = `WHERE ${where.join(" AND ")}`;
1145
1180
  const columns = `SELECT e.id, e.mailbox, e.uid, e.subject, e.from_name, e.from_addr, e.to_addr, e.cc,
1146
1181
  e.message_id, e.date, length(e.body_text) AS text_length,
1147
- (SELECT COUNT(*) FROM attachments a WHERE a.email_id = e.id) AS attachment_count,
1182
+ ${this.attachmentCountSql()},
1148
1183
  substr(COALESCE(e.body_text, ''), 1, 240) AS snippet`;
1149
1184
  const total = Number(
1150
1185
  db.prepare(`SELECT COUNT(*) AS count FROM emails e ${clause}`).get(...params)?.count ?? 0
@@ -1160,7 +1195,20 @@ var MailDatabase = class {
1160
1195
  FROM emails WHERE id = ?`
1161
1196
  ).get(localId);
1162
1197
  if (row === void 0) return void 0;
1163
- const attachments = db.prepare("SELECT id, filename, content_type, size FROM attachments WHERE email_id = ? ORDER BY id").all(localId).map((item) => ({
1198
+ const cidText = String(row.body_html ?? "").toLowerCase();
1199
+ const attachmentColumns = this.hasAttachmentColumns() ? "id, filename, content_type, size, content_id, disposition" : "id, filename, content_type, size";
1200
+ const attachments = db.prepare(`SELECT ${attachmentColumns} FROM attachments WHERE email_id = ? ORDER BY id`).all(localId).filter((item) => {
1201
+ const type = String(item.content_type ?? "").toLowerCase();
1202
+ if (!type.startsWith("image/")) return true;
1203
+ const disposition = String(item.disposition ?? "").toLowerCase();
1204
+ const contentId = String(item.content_id ?? "").toLowerCase();
1205
+ if (disposition === "inline") return false;
1206
+ if (contentId !== "" && disposition !== "attachment") return false;
1207
+ const filename = String(item.filename ?? "").toLowerCase();
1208
+ if (filename !== "" && cidText.includes(`cid:${filename}`)) return false;
1209
+ if (/^image\d+\.[a-z0-9]+$/.test(filename)) return false;
1210
+ return true;
1211
+ }).map((item) => ({
1164
1212
  id: String(item.id),
1165
1213
  filename: String(item.filename ?? "attachment"),
1166
1214
  contentType: String(item.content_type ?? "application/octet-stream"),
@@ -1377,7 +1425,7 @@ var BackupEngine = class {
1377
1425
  const emailId = Number(inserted.lastInsertRowid);
1378
1426
  const attachments = parsed.attachments ?? [];
1379
1427
  const insertAttachment = db.prepare(
1380
- "INSERT INTO attachments (email_id, filename, content_type, size, hash) VALUES (?, ?, ?, ?, ?)"
1428
+ "INSERT INTO attachments (email_id, filename, content_type, size, hash, content_id, disposition) VALUES (?, ?, ?, ?, ?, ?, ?)"
1381
1429
  );
1382
1430
  for (const attachment of attachments) {
1383
1431
  const data = attachment.content;
@@ -1388,7 +1436,9 @@ var BackupEngine = class {
1388
1436
  String(attachment.filename ?? "attachment"),
1389
1437
  String(attachment.contentType ?? "application/octet-stream"),
1390
1438
  stored.size,
1391
- stored.hash
1439
+ stored.hash,
1440
+ attachment.cid === void 0 ? null : String(attachment.cid).replace(/^<|>$/g, ""),
1441
+ attachment.contentDisposition === void 0 ? null : String(attachment.contentDisposition)
1392
1442
  );
1393
1443
  }
1394
1444
  try {
@@ -2444,7 +2494,7 @@ ${conversation.trim()}
2444
2494
  }
2445
2495
 
2446
2496
  // src/host/version.ts
2447
- var VERSION = "0.12.0";
2497
+ var VERSION = "0.12.1";
2448
2498
 
2449
2499
  // src/host/api.ts
2450
2500
  init_smtp();