@dxv-systems/helpdesk 0.1.0 → 0.3.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/dist/index.d.ts CHANGED
@@ -5,9 +5,8 @@ export { DEFAULT_WRITE_MODE, isPaused, writeModeFor } from "./controls";
5
5
  export { normalizeSubject, isForwardSubject } from "./subject";
6
6
  export { classifyNoise, type NoiseInput, type NoiseVerdict } from "./noise";
7
7
  export { matchThread, normalizeMessageId, type ThreadCandidate, type ThreadMatch } from "./threading";
8
- export { connectMailbox, ingestPage, isSyncFailing, syncMailbox, SYNC_FAILING_AFTER, type IngestResult, } from "./ingest";
9
- export { ensureDraft, completeDraft, regenerateDraft, editDraft, retryPlacement, sweepStuckDrafts, unignoreThread, type EnsureDraftOutcome, } from "./drafting";
10
- export { renderCommentHtml, replaceDraftComment, prependToDraftBody, hasQuotedHistory, findQuoteStart, } from "./draft-body";
8
+ export { connectMailbox, ingestPage, ingestSentPage, isSyncFailing, syncMailbox, SYNC_FAILING_AFTER, type IngestResult, type SyncFolder, } from "./ingest";
9
+ export { ensureDraft, completeDraft, regenerateDraft, editDraft, sweepStuckDrafts, unignoreThread, type EnsureDraftOutcome, } from "./drafting";
10
+ export { renderCommentHtml, prependToDraftBody, hasQuotedHistory, findQuoteStart, } from "./draft-body";
11
11
  export { approveAndSend, reconcileUnknownSends, resolveUnknownSend, markRepliedExternally, markWaiting, closeThread, dismissThread, classifySendError, type SendOutcome, } from "./sending";
12
12
  export { MAX_REPLY_ATTACHMENT_BYTES, MAX_REPLY_ATTACHMENTS, addReplyAttachment, pendingReplyAttachments, removeReplyAttachment, startReplyUpload, } from "./reply-attachments";
13
- export { evaluateAutoSend, DEFAULT_AUTO_SEND_POLICY, type AutoSendPolicy, type AutoSendContext, type AutoSendDecision, type OfficeHours, } from "./autosend";
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.DEFAULT_AUTO_SEND_POLICY = exports.evaluateAutoSend = exports.startReplyUpload = exports.removeReplyAttachment = exports.pendingReplyAttachments = exports.addReplyAttachment = exports.MAX_REPLY_ATTACHMENTS = exports.MAX_REPLY_ATTACHMENT_BYTES = exports.classifySendError = exports.dismissThread = exports.closeThread = exports.markWaiting = exports.markRepliedExternally = exports.resolveUnknownSend = exports.reconcileUnknownSends = exports.approveAndSend = exports.findQuoteStart = exports.hasQuotedHistory = exports.prependToDraftBody = exports.replaceDraftComment = exports.renderCommentHtml = exports.unignoreThread = exports.sweepStuckDrafts = exports.retryPlacement = exports.editDraft = exports.regenerateDraft = exports.completeDraft = exports.ensureDraft = exports.SYNC_FAILING_AFTER = exports.syncMailbox = exports.isSyncFailing = exports.ingestPage = exports.connectMailbox = exports.normalizeMessageId = exports.matchThread = exports.classifyNoise = exports.isForwardSubject = exports.normalizeSubject = exports.writeModeFor = exports.isPaused = exports.DEFAULT_WRITE_MODE = void 0;
17
+ exports.startReplyUpload = exports.removeReplyAttachment = exports.pendingReplyAttachments = exports.addReplyAttachment = exports.MAX_REPLY_ATTACHMENTS = exports.MAX_REPLY_ATTACHMENT_BYTES = exports.classifySendError = exports.dismissThread = exports.closeThread = exports.markWaiting = exports.markRepliedExternally = exports.resolveUnknownSend = exports.reconcileUnknownSends = exports.approveAndSend = exports.findQuoteStart = exports.hasQuotedHistory = exports.prependToDraftBody = exports.renderCommentHtml = exports.unignoreThread = exports.sweepStuckDrafts = exports.editDraft = exports.regenerateDraft = exports.completeDraft = exports.ensureDraft = exports.SYNC_FAILING_AFTER = exports.syncMailbox = exports.isSyncFailing = exports.ingestSentPage = exports.ingestPage = exports.connectMailbox = exports.normalizeMessageId = exports.matchThread = exports.classifyNoise = exports.isForwardSubject = exports.normalizeSubject = exports.writeModeFor = exports.isPaused = exports.DEFAULT_WRITE_MODE = void 0;
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./errors"), exports);
20
20
  __exportStar(require("./states"), exports);
@@ -33,6 +33,7 @@ Object.defineProperty(exports, "normalizeMessageId", { enumerable: true, get: fu
33
33
  var ingest_1 = require("./ingest");
34
34
  Object.defineProperty(exports, "connectMailbox", { enumerable: true, get: function () { return ingest_1.connectMailbox; } });
35
35
  Object.defineProperty(exports, "ingestPage", { enumerable: true, get: function () { return ingest_1.ingestPage; } });
36
+ Object.defineProperty(exports, "ingestSentPage", { enumerable: true, get: function () { return ingest_1.ingestSentPage; } });
36
37
  Object.defineProperty(exports, "isSyncFailing", { enumerable: true, get: function () { return ingest_1.isSyncFailing; } });
37
38
  Object.defineProperty(exports, "syncMailbox", { enumerable: true, get: function () { return ingest_1.syncMailbox; } });
38
39
  Object.defineProperty(exports, "SYNC_FAILING_AFTER", { enumerable: true, get: function () { return ingest_1.SYNC_FAILING_AFTER; } });
@@ -41,12 +42,10 @@ Object.defineProperty(exports, "ensureDraft", { enumerable: true, get: function
41
42
  Object.defineProperty(exports, "completeDraft", { enumerable: true, get: function () { return drafting_1.completeDraft; } });
42
43
  Object.defineProperty(exports, "regenerateDraft", { enumerable: true, get: function () { return drafting_1.regenerateDraft; } });
43
44
  Object.defineProperty(exports, "editDraft", { enumerable: true, get: function () { return drafting_1.editDraft; } });
44
- Object.defineProperty(exports, "retryPlacement", { enumerable: true, get: function () { return drafting_1.retryPlacement; } });
45
45
  Object.defineProperty(exports, "sweepStuckDrafts", { enumerable: true, get: function () { return drafting_1.sweepStuckDrafts; } });
46
46
  Object.defineProperty(exports, "unignoreThread", { enumerable: true, get: function () { return drafting_1.unignoreThread; } });
47
47
  var draft_body_1 = require("./draft-body");
48
48
  Object.defineProperty(exports, "renderCommentHtml", { enumerable: true, get: function () { return draft_body_1.renderCommentHtml; } });
49
- Object.defineProperty(exports, "replaceDraftComment", { enumerable: true, get: function () { return draft_body_1.replaceDraftComment; } });
50
49
  Object.defineProperty(exports, "prependToDraftBody", { enumerable: true, get: function () { return draft_body_1.prependToDraftBody; } });
51
50
  Object.defineProperty(exports, "hasQuotedHistory", { enumerable: true, get: function () { return draft_body_1.hasQuotedHistory; } });
52
51
  Object.defineProperty(exports, "findQuoteStart", { enumerable: true, get: function () { return draft_body_1.findQuoteStart; } });
@@ -66,6 +65,3 @@ Object.defineProperty(exports, "addReplyAttachment", { enumerable: true, get: fu
66
65
  Object.defineProperty(exports, "pendingReplyAttachments", { enumerable: true, get: function () { return reply_attachments_1.pendingReplyAttachments; } });
67
66
  Object.defineProperty(exports, "removeReplyAttachment", { enumerable: true, get: function () { return reply_attachments_1.removeReplyAttachment; } });
68
67
  Object.defineProperty(exports, "startReplyUpload", { enumerable: true, get: function () { return reply_attachments_1.startReplyUpload; } });
69
- var autosend_1 = require("./autosend");
70
- Object.defineProperty(exports, "evaluateAutoSend", { enumerable: true, get: function () { return autosend_1.evaluateAutoSend; } });
71
- Object.defineProperty(exports, "DEFAULT_AUTO_SEND_POLICY", { enumerable: true, get: function () { return autosend_1.DEFAULT_AUTO_SEND_POLICY; } });
package/dist/ingest.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { HelpdeskDeps, Paused, SyncFailing, SyncState } from "./types";
2
+ export type SyncFolder = "inbox" | "sentitems";
2
3
  export interface IngestResult {
4
+ folder: SyncFolder;
3
5
  fetched: number;
4
6
  created: number;
5
7
  appended: number;
@@ -13,11 +15,13 @@ export interface IngestResult {
13
15
  gone: number;
14
16
  draftsStarted: number;
15
17
  deliveryFailures: number;
18
+ unmatched: number;
16
19
  restarted: boolean;
17
20
  hasMore: boolean;
18
21
  }
19
22
  export declare function connectMailbox(deps: HelpdeskDeps, mailbox: string): Promise<SyncState>;
20
23
  export declare function ingestPage(deps: HelpdeskDeps, mailbox: string): Promise<IngestResult | Paused>;
24
+ export declare function ingestSentPage(deps: HelpdeskDeps, mailbox: string): Promise<IngestResult | Paused>;
21
25
  export declare function syncMailbox(deps: HelpdeskDeps, mailbox: string, options?: {
22
26
  maxPages?: number;
23
27
  }): Promise<IngestResult[] | Paused | SyncFailing>;
package/dist/ingest.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SYNC_FAILING_AFTER = void 0;
4
4
  exports.connectMailbox = connectMailbox;
5
5
  exports.ingestPage = ingestPage;
6
+ exports.ingestSentPage = ingestSentPage;
6
7
  exports.syncMailbox = syncMailbox;
7
8
  exports.isSyncFailing = isSyncFailing;
8
9
  const controls_1 = require("./controls");
@@ -34,32 +35,44 @@ function threadingHeaders(msg) {
34
35
  const references = msg.references ?? (raw ? (raw.match(/<[^>]+>/g) ?? []) : []);
35
36
  return { inReplyTo, references };
36
37
  }
37
- function restartFrom(state, overlapMs) {
38
- if (!state.lastReceivedAt)
39
- return state.connectedAt;
40
- const back = Date.parse(state.lastReceivedAt) - overlapMs;
41
- return new Date(Math.max(Date.parse(state.connectedAt), back)).toISOString();
38
+ function cursorOf(state, folder) {
39
+ return folder === "inbox"
40
+ ? { link: state.link, lastReceivedAt: state.lastReceivedAt }
41
+ : { link: state.sentLink ?? null, lastReceivedAt: state.sentLastReceivedAt ?? null };
42
42
  }
43
- async function fetchPage(deps, gateway, state) {
43
+ function withCursor(state, folder, cursor) {
44
+ return folder === "inbox"
45
+ ? { ...state, link: cursor.link, lastReceivedAt: cursor.lastReceivedAt }
46
+ : { ...state, sentLink: cursor.link, sentLastReceivedAt: cursor.lastReceivedAt };
47
+ }
48
+ function restartFrom(connectedAt, lastReceivedAt, overlapMs) {
49
+ if (!lastReceivedAt)
50
+ return connectedAt;
51
+ const back = Date.parse(lastReceivedAt) - overlapMs;
52
+ return new Date(Math.max(Date.parse(connectedAt), back)).toISOString();
53
+ }
54
+ async function fetchPage(deps, gateway, state, folder) {
44
55
  const pageSize = deps.config.pageSize;
45
- const first = (s) => gateway.delta(s.link
46
- ? { link: s.link, ...(pageSize ? { pageSize } : {}) }
56
+ const cursor = cursorOf(state, folder);
57
+ const first = (link) => gateway.delta(link
58
+ ? { link, ...(pageSize ? { pageSize } : {}) }
47
59
  : {
48
- folderId: "inbox",
49
- since: restartFrom(s, deps.config.restartOverlapMs ?? DEFAULT_RESTART_OVERLAP_MS),
60
+ folderId: folder,
61
+ since: restartFrom(state.connectedAt, cursor.lastReceivedAt, deps.config.restartOverlapMs ?? DEFAULT_RESTART_OVERLAP_MS),
50
62
  ...(pageSize ? { pageSize } : {}),
51
63
  });
52
64
  try {
53
- return { page: await first(state), restarted: false };
65
+ return { page: await first(cursor.link), restarted: false };
54
66
  }
55
67
  catch (err) {
56
- if (!state.link || !(0, errors_1.isDeltaLinkExpired)(err))
68
+ if (!cursor.link || !(0, errors_1.isDeltaLinkExpired)(err))
57
69
  throw err;
58
- return { page: await first({ ...state, link: null }), restarted: true };
70
+ return { page: await first(null), restarted: true };
59
71
  }
60
72
  }
61
- function emptyResult() {
73
+ function emptyResult(folder) {
62
74
  return {
75
+ folder,
63
76
  fetched: 0,
64
77
  created: 0,
65
78
  appended: 0,
@@ -73,12 +86,13 @@ function emptyResult() {
73
86
  gone: 0,
74
87
  draftsStarted: 0,
75
88
  deliveryFailures: 0,
89
+ unmatched: 0,
76
90
  restarted: false,
77
91
  hasMore: false,
78
92
  };
79
93
  }
80
94
  async function reconcileExisting(deps, existing, result) {
81
- if (existing.removedAt)
95
+ if (existing.removedAt || existing.direction === "outbound")
82
96
  return;
83
97
  const thread = await deps.store.getThread(existing.threadId);
84
98
  if (thread && (thread.state === "new" || thread.state === "drafted")) {
@@ -126,6 +140,7 @@ async function processNew(deps, gateway, mailbox, item, state, result) {
126
140
  noise: { action: noise.action, reason: noise.reason },
127
141
  suppressAutoResponse: noise.suppressAutoResponse === true,
128
142
  removedAt: null,
143
+ direction: "inbound",
129
144
  };
130
145
  if (noise.reason === "bounce") {
131
146
  const refs = [...new Set([...(0, bounce_1.bouncedReplyRefs)(msg.body?.content ?? ""), ...headerRefs(headers)])];
@@ -260,7 +275,7 @@ async function processItem(deps, gateway, mailbox, item, state, result) {
260
275
  }
261
276
  await processNew(deps, gateway, mailbox, item, state, result);
262
277
  }
263
- async function ingestPage(deps, mailbox) {
278
+ async function ingestFolderPage(deps, mailbox, folder, process) {
264
279
  const controls = await (0, controls_1.loadControls)(deps);
265
280
  if (controls.paused)
266
281
  return (0, controls_1.paused)();
@@ -268,35 +283,142 @@ async function ingestPage(deps, mailbox) {
268
283
  const state = await deps.store.getSyncState(mailbox);
269
284
  if (!state)
270
285
  throw new errors_1.MailboxNotConnectedError(mailbox);
271
- const { page, restarted } = await fetchPage(deps, gateway, state);
286
+ const { page, restarted } = await fetchPage(deps, gateway, state, folder);
272
287
  const link = page.nextLink ?? page.deltaLink;
273
288
  if (!link)
274
289
  throw new errors_1.InvalidDeltaPageError();
275
- const result = emptyResult();
290
+ const result = emptyResult(folder);
276
291
  result.restarted = restarted;
277
292
  result.fetched = page.items.length;
278
293
  const stamp = (i) => i.receivedDateTime ?? "";
279
294
  const live = page.items.filter((i) => !i.removed).sort((a, b) => stamp(a).localeCompare(stamp(b)));
280
295
  const gone = page.items.filter((i) => i.removed);
281
- let newest = state.lastReceivedAt;
296
+ let newest = cursorOf(state, folder).lastReceivedAt;
282
297
  for (const item of [...live, ...gone]) {
283
- await processItem(deps, gateway, mailbox, item, state, result);
298
+ await process(deps, gateway, mailbox, item, state, result);
284
299
  if (item.receivedDateTime && (!newest || item.receivedDateTime > newest)) {
285
300
  newest = item.receivedDateTime;
286
301
  }
287
302
  }
288
- await deps.store.saveSyncState(mailbox, { ...state, link, lastReceivedAt: newest });
303
+ await deps.store.saveSyncState(mailbox, withCursor(state, folder, { link, lastReceivedAt: newest }));
289
304
  result.hasMore = page.nextLink !== null;
290
305
  return result;
291
306
  }
307
+ async function ingestPage(deps, mailbox) {
308
+ return ingestFolderPage(deps, mailbox, "inbox", processItem);
309
+ }
310
+ async function ingestSentPage(deps, mailbox) {
311
+ return ingestFolderPage(deps, mailbox, "sentitems", processSentItem);
312
+ }
313
+ async function processSentItem(deps, gateway, mailbox, item, state, result) {
314
+ if (item.removed) {
315
+ const stored = await deps.store.getMessage(mailbox, item.id);
316
+ if (stored)
317
+ await deps.store.markMessageRemoved(mailbox, item.id, (0, internal_1.nowIso)(deps));
318
+ result.removed++;
319
+ return;
320
+ }
321
+ if (item.isDraft) {
322
+ result.skippedDrafts++;
323
+ return;
324
+ }
325
+ if (await deps.store.getMessage(mailbox, item.id)) {
326
+ result.unchanged++;
327
+ return;
328
+ }
329
+ if (item.receivedDateTime && Date.parse(item.receivedDateTime) < Date.parse(state.connectedAt)) {
330
+ result.skippedPredates++;
331
+ return;
332
+ }
333
+ const msg = await gateway.getMessage(item.id, { includeHeaders: true, bodyType: "text" });
334
+ if (!msg) {
335
+ result.gone++;
336
+ return;
337
+ }
338
+ if (msg.isDraft === true) {
339
+ result.skippedDrafts++;
340
+ return;
341
+ }
342
+ const conversationId = msg.conversationId ?? item.conversationId ?? null;
343
+ const recipients = [...msg.toRecipients, ...msg.ccRecipients]
344
+ .map((a) => a.address.trim().toLowerCase())
345
+ .filter(Boolean);
346
+ const message = {
347
+ id: item.id,
348
+ mailbox,
349
+ internetMessageId: (0, threading_1.normalizeMessageId)(msg.internetMessageId ?? item.internetMessageId),
350
+ conversationId,
351
+ subject: msg.subject ?? item.subject ?? "",
352
+ fromAddress: mailbox.trim().toLowerCase(),
353
+ receivedAt: msg.sentDateTime ?? msg.receivedDateTime ?? item.receivedDateTime ?? (0, internal_1.nowIso)(deps),
354
+ noise: { action: "process", reason: "none" },
355
+ suppressAutoResponse: false,
356
+ removedAt: null,
357
+ direction: "outbound",
358
+ };
359
+ const headers = threadingHeaders(msg);
360
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
361
+ const thread = await matchSent(deps, { mailbox, headers, conversationId, recipients });
362
+ if (!thread) {
363
+ result.unmatched++;
364
+ return;
365
+ }
366
+ const outcome = await deps.store.appendMessage({ ...message, threadId: thread.id }, { threadId: thread.id, expectedState: thread.state, patch: {} });
367
+ if (outcome === "stale")
368
+ continue;
369
+ if (outcome === "duplicate")
370
+ result.duplicates++;
371
+ else
372
+ result.appended++;
373
+ return;
374
+ }
375
+ throw new errors_1.StaleThreadError(item.id);
376
+ }
377
+ async function matchSent(deps, input) {
378
+ const { mailbox, headers, conversationId, recipients } = input;
379
+ const refs = [...new Set(headerRefs(headers))];
380
+ const hits = refs.length ? await deps.store.findMessageThreads(mailbox, refs) : [];
381
+ const parent = (0, threading_1.normalizeMessageId)(headers.inReplyTo);
382
+ const direct = parent ? hits.find((h) => h.internetMessageId === parent) : undefined;
383
+ const headerIds = new Set(direct ? [direct.threadId] : hits.map((h) => h.threadId));
384
+ const candidates = new Map();
385
+ for (const id of headerIds) {
386
+ const t = await deps.store.getThread(id);
387
+ if (t && t.mailbox === mailbox)
388
+ candidates.set(t.id, t);
389
+ }
390
+ if (!direct && conversationId) {
391
+ for (const t of await deps.store.findThreadsByConversationId(mailbox, conversationId)) {
392
+ if (t.mailbox !== mailbox)
393
+ continue;
394
+ if (headerIds.has(t.id) || recipients.some((r) => t.participants.includes(r)))
395
+ candidates.set(t.id, t);
396
+ }
397
+ }
398
+ if (candidates.size !== 1)
399
+ return null;
400
+ return [...candidates.values()][0] ?? null;
401
+ }
292
402
  async function syncMailbox(deps, mailbox, options = {}) {
293
403
  const results = [];
404
+ const maxPages = options.maxPages ?? 20;
294
405
  try {
295
- for (let i = 0; i < (options.maxPages ?? 20); i++) {
406
+ let inboxDone = false;
407
+ for (let i = 0; i < maxPages; i++) {
296
408
  const r = await ingestPage(deps, mailbox);
297
409
  if ((0, controls_1.isPaused)(r))
298
410
  return results.length ? results : r;
299
411
  results.push(r);
412
+ if (!r.hasMore) {
413
+ inboxDone = true;
414
+ break;
415
+ }
416
+ }
417
+ for (let i = 0; inboxDone && i < maxPages; i++) {
418
+ const r = await ingestSentPage(deps, mailbox);
419
+ if ((0, controls_1.isPaused)(r))
420
+ return results;
421
+ results.push(r);
300
422
  if (!r.hasMore)
301
423
  break;
302
424
  }
package/dist/react.d.ts CHANGED
@@ -10,27 +10,67 @@ export interface HelpdeskInboxClassNames {
10
10
  title: string;
11
11
  filters: string;
12
12
  field: string;
13
+ fieldWide: string;
13
14
  label: string;
15
+ selectWrap: string;
14
16
  select: string;
17
+ selectIcon: string;
15
18
  list: string;
16
19
  listItem: string;
17
20
  listItemCurrent: string;
21
+ listItemTop: string;
18
22
  listItemTitle: string;
23
+ listItemTime: string;
19
24
  listItemMeta: string;
20
25
  card: string;
26
+ header: string;
27
+ headerTop: string;
28
+ headerActions: string;
21
29
  heading: string;
22
30
  subheading: string;
23
31
  text: string;
24
32
  muted: string;
25
33
  meta: string;
26
- badges: string;
27
34
  badge: string;
28
35
  badgeAttention: string;
29
36
  badgeDone: string;
30
- messages: string;
37
+ badgeError: string;
38
+ reasonNote: string;
39
+ toolButton: string;
40
+ assignedChip: string;
41
+ iconButton: string;
42
+ menu: string;
43
+ menuItem: string;
44
+ backLink: string;
45
+ conversation: string;
46
+ entry: string;
47
+ entryOurs: string;
48
+ avatar: string;
49
+ avatarOurs: string;
50
+ avatarEvent: string;
31
51
  message: string;
52
+ messageOurs: string;
53
+ messageEvent: string;
32
54
  messageHeader: string;
33
55
  messageBody: string;
56
+ emptyBody: string;
57
+ eventTitle: string;
58
+ details: string;
59
+ detailsPre: string;
60
+ fileChips: string;
61
+ fileChip: string;
62
+ fileChipName: string;
63
+ fileChipMeta: string;
64
+ sentNote: string;
65
+ composer: string;
66
+ composerHeader: string;
67
+ aiChip: string;
68
+ composerFooter: string;
69
+ replyChip: string;
70
+ chipRemove: string;
71
+ dashedButton: string;
72
+ srOnly: string;
73
+ icon: string;
34
74
  textarea: string;
35
75
  actions: string;
36
76
  primaryButton: string;