@dxv-systems/helpdesk 0.0.0-stage → 0.1.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.
Files changed (50) hide show
  1. package/README.md +279 -3
  2. package/dist/autosend.d.ts +43 -0
  3. package/dist/autosend.js +83 -0
  4. package/dist/bounce.d.ts +7 -0
  5. package/dist/bounce.js +41 -0
  6. package/dist/cleanup.d.ts +7 -0
  7. package/dist/cleanup.js +39 -0
  8. package/dist/controls.d.ts +13 -0
  9. package/dist/controls.js +87 -0
  10. package/dist/draft-body.d.ts +5 -0
  11. package/dist/draft-body.js +52 -0
  12. package/dist/drafting.d.ts +19 -0
  13. package/dist/drafting.js +311 -0
  14. package/dist/errors.d.ts +63 -0
  15. package/dist/errors.js +139 -0
  16. package/dist/inbox/handlers.d.ts +36 -0
  17. package/dist/inbox/handlers.js +535 -0
  18. package/dist/inbox/messages.d.ts +35 -0
  19. package/dist/inbox/messages.js +57 -0
  20. package/dist/inbox/plain-text.d.ts +1 -0
  21. package/dist/inbox/plain-text.js +58 -0
  22. package/dist/inbox/types.d.ts +108 -0
  23. package/dist/inbox/types.js +2 -0
  24. package/dist/index.d.ts +13 -0
  25. package/dist/index.js +71 -0
  26. package/dist/ingest.d.ts +25 -0
  27. package/dist/ingest.js +335 -0
  28. package/dist/internal.d.ts +8 -0
  29. package/dist/internal.js +44 -0
  30. package/dist/noise.d.ts +17 -0
  31. package/dist/noise.js +77 -0
  32. package/dist/react.d.ts +57 -0
  33. package/dist/react.js +431 -0
  34. package/dist/reply-attachments.d.ts +28 -0
  35. package/dist/reply-attachments.js +119 -0
  36. package/dist/sending.d.ts +33 -0
  37. package/dist/sending.js +291 -0
  38. package/dist/server.d.ts +6 -0
  39. package/dist/server.js +10 -0
  40. package/dist/states.d.ts +13 -0
  41. package/dist/states.js +63 -0
  42. package/dist/subject.d.ts +2 -0
  43. package/dist/subject.js +30 -0
  44. package/dist/testing/index.d.ts +99 -0
  45. package/dist/testing/index.js +489 -0
  46. package/dist/threading.d.ts +18 -0
  47. package/dist/threading.js +66 -0
  48. package/dist/types.d.ts +345 -0
  49. package/dist/types.js +2 -0
  50. package/package.json +73 -6
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.htmlToPlainText = htmlToPlainText;
4
+ const NAMED_ENTITIES = {
5
+ nbsp: " ",
6
+ amp: "&",
7
+ lt: "<",
8
+ gt: ">",
9
+ quot: '"',
10
+ apos: "'",
11
+ euro: "€",
12
+ hellip: "…",
13
+ ndash: "–",
14
+ mdash: "—",
15
+ lsquo: "‘",
16
+ rsquo: "’",
17
+ ldquo: "“",
18
+ rdquo: "”",
19
+ copy: "©",
20
+ reg: "®",
21
+ eacute: "é",
22
+ euml: "ë",
23
+ iuml: "ï",
24
+ ouml: "ö",
25
+ uuml: "ü",
26
+ };
27
+ function codePoint(n) {
28
+ if (!Number.isFinite(n) || n <= 0 || n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff))
29
+ return "";
30
+ return String.fromCodePoint(n);
31
+ }
32
+ function decodeEntities(text) {
33
+ return text.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (whole, ref) => {
34
+ if (ref[0] === "#") {
35
+ const hex = ref[1] === "x" || ref[1] === "X";
36
+ return codePoint(parseInt(ref.slice(hex ? 2 : 1), hex ? 16 : 10));
37
+ }
38
+ return NAMED_ENTITIES[ref.toLowerCase()] ?? whole;
39
+ });
40
+ }
41
+ const BLOCK_END = /<\/(p|div|li|tr|h[1-6]|blockquote|pre|table|section|article)\s*>/gi;
42
+ const LINE_BREAK = /<(br|hr)\b[^>]*>/gi;
43
+ function htmlToPlainText(html) {
44
+ const text = html
45
+ .replace(/<!--[\s\S]*?-->/g, "")
46
+ .replace(/<(head|style|script|title|noscript|template)\b[\s\S]*?<\/\1\s*>/gi, "")
47
+ .replace(LINE_BREAK, "\n")
48
+ .replace(BLOCK_END, "\n")
49
+ .replace(/<li\b[^>]*>/gi, "• ")
50
+ .replace(/<[^>]*>/g, "")
51
+ .replace(/<[^>]*$/, "");
52
+ return decodeEntities(text)
53
+ .replace(/[^\S\r\n]/g, " ")
54
+ .replace(/\r\n?/g, "\n")
55
+ .replace(/ +\n/g, "\n")
56
+ .replace(/\n{3,}/g, "\n\n")
57
+ .trim();
58
+ }
@@ -0,0 +1,108 @@
1
+ import type { MailboxWriteMode, ReviewReason, SendStatus, TicketState } from "../types";
2
+ export interface HelpdeskStaff {
3
+ id: string;
4
+ name: string;
5
+ email: string;
6
+ }
7
+ export interface InboxPerson {
8
+ id: string;
9
+ name: string;
10
+ }
11
+ export interface InboxMailboxView {
12
+ address: string;
13
+ writeMode: MailboxWriteMode;
14
+ }
15
+ export interface InboxPilotView {
16
+ paused: boolean;
17
+ mailboxes: InboxMailboxView[];
18
+ }
19
+ export interface InboxThreadSummary {
20
+ id: string;
21
+ mailbox: string;
22
+ state: TicketState;
23
+ subject: string;
24
+ customerAddress: string;
25
+ lastActivityAt: string;
26
+ reviewReason: ReviewReason | null;
27
+ assignee: InboxPerson | null;
28
+ }
29
+ export interface InboxSyncIssue {
30
+ mailbox: string;
31
+ failingSince: string;
32
+ }
33
+ export interface InboxThreadList {
34
+ viewer: InboxPerson;
35
+ pilot: InboxPilotView;
36
+ syncIssues: InboxSyncIssue[];
37
+ threads: InboxThreadSummary[];
38
+ nextCursor: string | null;
39
+ }
40
+ export interface InboxMessageView {
41
+ id: string;
42
+ fromAddress: string;
43
+ subject: string;
44
+ receivedAt: string;
45
+ noise: boolean;
46
+ removed: boolean;
47
+ }
48
+ export interface InboxMessageBody {
49
+ id: string;
50
+ text: string;
51
+ attachments: InboxAttachmentView[];
52
+ }
53
+ export interface InboxReplyAttachmentView {
54
+ id: string;
55
+ name: string;
56
+ size: number;
57
+ sent: boolean;
58
+ }
59
+ export interface InboxUploadTarget {
60
+ uploadUrl: string;
61
+ uploadToken: string;
62
+ contentType: string;
63
+ }
64
+ export interface InboxAttachmentView {
65
+ id: string;
66
+ name: string;
67
+ size: number;
68
+ kind: "file" | "item" | "reference";
69
+ href: string | null;
70
+ unavailable: "item" | "reference" | "too_large" | null;
71
+ }
72
+ export interface InboxDraftView {
73
+ generation: number;
74
+ status: "requested" | "started" | "succeeded" | "failed";
75
+ text: string | null;
76
+ confidence: number | null;
77
+ category: string | null;
78
+ stale: boolean;
79
+ inOutlook: boolean;
80
+ }
81
+ export type InboxAction = "edit" | "regenerate" | "send" | "retry_placement" | "replied_externally" | "mark_waiting" | "close" | "unignore" | "dismiss" | "resolve_send" | "attach";
82
+ export interface InboxThreadDetail {
83
+ viewer: InboxPerson;
84
+ thread: InboxThreadSummary & {
85
+ relatedThreadId: string | null;
86
+ dismissedBy: string | null;
87
+ participants: string[];
88
+ createdAt: string;
89
+ };
90
+ pilot: {
91
+ paused: boolean;
92
+ writeMode: MailboxWriteMode;
93
+ };
94
+ messages: InboxMessageView[];
95
+ draft: InboxDraftView | null;
96
+ send: {
97
+ status: SendStatus;
98
+ at: string;
99
+ } | null;
100
+ replyAttachments: InboxReplyAttachmentView[];
101
+ actions: InboxAction[];
102
+ }
103
+ export type InboxSendOutcome = "sent" | "already_sent" | "in_progress" | "unknown_outcome";
104
+ export interface InboxActionResult {
105
+ detail: InboxThreadDetail;
106
+ message: string;
107
+ outcome?: InboxSendOutcome;
108
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ export * from "./types";
2
+ export * from "./errors";
3
+ export * from "./states";
4
+ export { DEFAULT_WRITE_MODE, isPaused, writeModeFor } from "./controls";
5
+ export { normalizeSubject, isForwardSubject } from "./subject";
6
+ export { classifyNoise, type NoiseInput, type NoiseVerdict } from "./noise";
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";
11
+ export { approveAndSend, reconcileUnknownSends, resolveUnknownSend, markRepliedExternally, markWaiting, closeThread, dismissThread, classifySendError, type SendOutcome, } from "./sending";
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 ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
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;
18
+ __exportStar(require("./types"), exports);
19
+ __exportStar(require("./errors"), exports);
20
+ __exportStar(require("./states"), exports);
21
+ var controls_1 = require("./controls");
22
+ Object.defineProperty(exports, "DEFAULT_WRITE_MODE", { enumerable: true, get: function () { return controls_1.DEFAULT_WRITE_MODE; } });
23
+ Object.defineProperty(exports, "isPaused", { enumerable: true, get: function () { return controls_1.isPaused; } });
24
+ Object.defineProperty(exports, "writeModeFor", { enumerable: true, get: function () { return controls_1.writeModeFor; } });
25
+ var subject_1 = require("./subject");
26
+ Object.defineProperty(exports, "normalizeSubject", { enumerable: true, get: function () { return subject_1.normalizeSubject; } });
27
+ Object.defineProperty(exports, "isForwardSubject", { enumerable: true, get: function () { return subject_1.isForwardSubject; } });
28
+ var noise_1 = require("./noise");
29
+ Object.defineProperty(exports, "classifyNoise", { enumerable: true, get: function () { return noise_1.classifyNoise; } });
30
+ var threading_1 = require("./threading");
31
+ Object.defineProperty(exports, "matchThread", { enumerable: true, get: function () { return threading_1.matchThread; } });
32
+ Object.defineProperty(exports, "normalizeMessageId", { enumerable: true, get: function () { return threading_1.normalizeMessageId; } });
33
+ var ingest_1 = require("./ingest");
34
+ Object.defineProperty(exports, "connectMailbox", { enumerable: true, get: function () { return ingest_1.connectMailbox; } });
35
+ Object.defineProperty(exports, "ingestPage", { enumerable: true, get: function () { return ingest_1.ingestPage; } });
36
+ Object.defineProperty(exports, "isSyncFailing", { enumerable: true, get: function () { return ingest_1.isSyncFailing; } });
37
+ Object.defineProperty(exports, "syncMailbox", { enumerable: true, get: function () { return ingest_1.syncMailbox; } });
38
+ Object.defineProperty(exports, "SYNC_FAILING_AFTER", { enumerable: true, get: function () { return ingest_1.SYNC_FAILING_AFTER; } });
39
+ var drafting_1 = require("./drafting");
40
+ Object.defineProperty(exports, "ensureDraft", { enumerable: true, get: function () { return drafting_1.ensureDraft; } });
41
+ Object.defineProperty(exports, "completeDraft", { enumerable: true, get: function () { return drafting_1.completeDraft; } });
42
+ Object.defineProperty(exports, "regenerateDraft", { enumerable: true, get: function () { return drafting_1.regenerateDraft; } });
43
+ 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
+ Object.defineProperty(exports, "sweepStuckDrafts", { enumerable: true, get: function () { return drafting_1.sweepStuckDrafts; } });
46
+ Object.defineProperty(exports, "unignoreThread", { enumerable: true, get: function () { return drafting_1.unignoreThread; } });
47
+ var draft_body_1 = require("./draft-body");
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
+ Object.defineProperty(exports, "prependToDraftBody", { enumerable: true, get: function () { return draft_body_1.prependToDraftBody; } });
51
+ Object.defineProperty(exports, "hasQuotedHistory", { enumerable: true, get: function () { return draft_body_1.hasQuotedHistory; } });
52
+ Object.defineProperty(exports, "findQuoteStart", { enumerable: true, get: function () { return draft_body_1.findQuoteStart; } });
53
+ var sending_1 = require("./sending");
54
+ Object.defineProperty(exports, "approveAndSend", { enumerable: true, get: function () { return sending_1.approveAndSend; } });
55
+ Object.defineProperty(exports, "reconcileUnknownSends", { enumerable: true, get: function () { return sending_1.reconcileUnknownSends; } });
56
+ Object.defineProperty(exports, "resolveUnknownSend", { enumerable: true, get: function () { return sending_1.resolveUnknownSend; } });
57
+ Object.defineProperty(exports, "markRepliedExternally", { enumerable: true, get: function () { return sending_1.markRepliedExternally; } });
58
+ Object.defineProperty(exports, "markWaiting", { enumerable: true, get: function () { return sending_1.markWaiting; } });
59
+ Object.defineProperty(exports, "closeThread", { enumerable: true, get: function () { return sending_1.closeThread; } });
60
+ Object.defineProperty(exports, "dismissThread", { enumerable: true, get: function () { return sending_1.dismissThread; } });
61
+ Object.defineProperty(exports, "classifySendError", { enumerable: true, get: function () { return sending_1.classifySendError; } });
62
+ var reply_attachments_1 = require("./reply-attachments");
63
+ Object.defineProperty(exports, "MAX_REPLY_ATTACHMENT_BYTES", { enumerable: true, get: function () { return reply_attachments_1.MAX_REPLY_ATTACHMENT_BYTES; } });
64
+ Object.defineProperty(exports, "MAX_REPLY_ATTACHMENTS", { enumerable: true, get: function () { return reply_attachments_1.MAX_REPLY_ATTACHMENTS; } });
65
+ Object.defineProperty(exports, "addReplyAttachment", { enumerable: true, get: function () { return reply_attachments_1.addReplyAttachment; } });
66
+ Object.defineProperty(exports, "pendingReplyAttachments", { enumerable: true, get: function () { return reply_attachments_1.pendingReplyAttachments; } });
67
+ Object.defineProperty(exports, "removeReplyAttachment", { enumerable: true, get: function () { return reply_attachments_1.removeReplyAttachment; } });
68
+ 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; } });
@@ -0,0 +1,25 @@
1
+ import type { HelpdeskDeps, Paused, SyncFailing, SyncState } from "./types";
2
+ export interface IngestResult {
3
+ fetched: number;
4
+ created: number;
5
+ appended: number;
6
+ ignored: number;
7
+ duplicates: number;
8
+ unchanged: number;
9
+ removed: number;
10
+ skippedDrafts: number;
11
+ skippedOwn: number;
12
+ skippedPredates: number;
13
+ gone: number;
14
+ draftsStarted: number;
15
+ deliveryFailures: number;
16
+ restarted: boolean;
17
+ hasMore: boolean;
18
+ }
19
+ export declare function connectMailbox(deps: HelpdeskDeps, mailbox: string): Promise<SyncState>;
20
+ export declare function ingestPage(deps: HelpdeskDeps, mailbox: string): Promise<IngestResult | Paused>;
21
+ export declare function syncMailbox(deps: HelpdeskDeps, mailbox: string, options?: {
22
+ maxPages?: number;
23
+ }): Promise<IngestResult[] | Paused | SyncFailing>;
24
+ export declare const SYNC_FAILING_AFTER = 3;
25
+ export declare function isSyncFailing(value: unknown): value is SyncFailing;
package/dist/ingest.js ADDED
@@ -0,0 +1,335 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SYNC_FAILING_AFTER = void 0;
4
+ exports.connectMailbox = connectMailbox;
5
+ exports.ingestPage = ingestPage;
6
+ exports.syncMailbox = syncMailbox;
7
+ exports.isSyncFailing = isSyncFailing;
8
+ const controls_1 = require("./controls");
9
+ const drafting_1 = require("./drafting");
10
+ const errors_1 = require("./errors");
11
+ const internal_1 = require("./internal");
12
+ const bounce_1 = require("./bounce");
13
+ const noise_1 = require("./noise");
14
+ const states_1 = require("./states");
15
+ const subject_1 = require("./subject");
16
+ const threading_1 = require("./threading");
17
+ const DEFAULT_RESTART_OVERLAP_MS = 48 * 60 * 60 * 1000;
18
+ const MAX_ATTEMPTS = 3;
19
+ async function connectMailbox(deps, mailbox) {
20
+ (0, controls_1.assertInScope)(deps.config, mailbox);
21
+ const existing = await deps.store.getSyncState(mailbox);
22
+ if (existing)
23
+ return existing;
24
+ const state = { link: null, connectedAt: (0, internal_1.nowIso)(deps), lastReceivedAt: null };
25
+ await deps.store.saveSyncState(mailbox, state);
26
+ return state;
27
+ }
28
+ function headerValue(msg, name) {
29
+ return msg.internetMessageHeaders?.find((h) => h.name.toLowerCase() === name)?.value;
30
+ }
31
+ function threadingHeaders(msg) {
32
+ const inReplyTo = msg.inReplyTo ?? headerValue(msg, "in-reply-to") ?? null;
33
+ const raw = headerValue(msg, "references");
34
+ const references = msg.references ?? (raw ? (raw.match(/<[^>]+>/g) ?? []) : []);
35
+ return { inReplyTo, references };
36
+ }
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();
42
+ }
43
+ async function fetchPage(deps, gateway, state) {
44
+ const pageSize = deps.config.pageSize;
45
+ const first = (s) => gateway.delta(s.link
46
+ ? { link: s.link, ...(pageSize ? { pageSize } : {}) }
47
+ : {
48
+ folderId: "inbox",
49
+ since: restartFrom(s, deps.config.restartOverlapMs ?? DEFAULT_RESTART_OVERLAP_MS),
50
+ ...(pageSize ? { pageSize } : {}),
51
+ });
52
+ try {
53
+ return { page: await first(state), restarted: false };
54
+ }
55
+ catch (err) {
56
+ if (!state.link || !(0, errors_1.isDeltaLinkExpired)(err))
57
+ throw err;
58
+ return { page: await first({ ...state, link: null }), restarted: true };
59
+ }
60
+ }
61
+ function emptyResult() {
62
+ return {
63
+ fetched: 0,
64
+ created: 0,
65
+ appended: 0,
66
+ ignored: 0,
67
+ duplicates: 0,
68
+ unchanged: 0,
69
+ removed: 0,
70
+ skippedDrafts: 0,
71
+ skippedOwn: 0,
72
+ skippedPredates: 0,
73
+ gone: 0,
74
+ draftsStarted: 0,
75
+ deliveryFailures: 0,
76
+ restarted: false,
77
+ hasMore: false,
78
+ };
79
+ }
80
+ async function reconcileExisting(deps, existing, result) {
81
+ if (existing.removedAt)
82
+ return;
83
+ const thread = await deps.store.getThread(existing.threadId);
84
+ if (thread && (thread.state === "new" || thread.state === "drafted")) {
85
+ if ((await (0, drafting_1.startDraftFor)(deps, thread.id, existing)) === "started")
86
+ result.draftsStarted++;
87
+ }
88
+ }
89
+ async function processNew(deps, gateway, mailbox, item, state, result) {
90
+ const connectedAt = Date.parse(state.connectedAt);
91
+ if (item.receivedDateTime && Date.parse(item.receivedDateTime) < connectedAt) {
92
+ result.skippedPredates++;
93
+ return;
94
+ }
95
+ const msg = await gateway.getMessage(item.id, {
96
+ includeHeaders: true,
97
+ bodyType: "text",
98
+ });
99
+ if (!msg) {
100
+ result.gone++;
101
+ return;
102
+ }
103
+ const receivedAt = msg.receivedDateTime ?? item.receivedDateTime ?? (0, internal_1.nowIso)(deps);
104
+ const from = (msg.from?.address ?? item.from?.address ?? "").trim().toLowerCase();
105
+ const subject = msg.subject ?? item.subject ?? "";
106
+ const headers = threadingHeaders(msg);
107
+ const noise = (0, noise_1.classifyNoise)({
108
+ from,
109
+ subject,
110
+ headers: msg.internetMessageHeaders,
111
+ replyTo: msg.replyTo.map((a) => a.address),
112
+ ownAddresses: deps.config.ownAddresses,
113
+ connectedMailboxes: deps.config.connectedMailboxes,
114
+ });
115
+ const isNoise = noise.action === "ignore";
116
+ const customer = noise.customerAddress ?? from;
117
+ const people = [...new Set([from, customer].filter(Boolean))];
118
+ const base = {
119
+ id: item.id,
120
+ mailbox,
121
+ internetMessageId: (0, threading_1.normalizeMessageId)(msg.internetMessageId ?? item.internetMessageId),
122
+ conversationId: msg.conversationId ?? item.conversationId ?? null,
123
+ subject,
124
+ fromAddress: from,
125
+ receivedAt,
126
+ noise: { action: noise.action, reason: noise.reason },
127
+ suppressAutoResponse: noise.suppressAutoResponse === true,
128
+ removedAt: null,
129
+ };
130
+ if (noise.reason === "bounce") {
131
+ const refs = [...new Set([...(0, bounce_1.bouncedReplyRefs)(msg.body?.content ?? ""), ...headerRefs(headers)])];
132
+ if (await recordBounce(deps, mailbox, base, refs, result))
133
+ return;
134
+ }
135
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
136
+ const match = await (0, threading_1.matchThread)(deps.store, {
137
+ mailbox,
138
+ conversationId: base.conversationId,
139
+ inReplyTo: headers.inReplyTo,
140
+ references: headers.references,
141
+ subject,
142
+ from,
143
+ receivedAt,
144
+ }, { subjectWindowMs: deps.config.subjectWindowMs });
145
+ let outcome;
146
+ let threadId;
147
+ const existing = match.thread;
148
+ const reply = existing && !isNoise ? (0, states_1.onCustomerReply)(existing.state) : null;
149
+ if (existing && (isNoise || reply?.kind === "append")) {
150
+ threadId = existing.id;
151
+ const patch = {};
152
+ if (!isNoise && reply?.kind === "append") {
153
+ patch.state = reply.next;
154
+ patch.participants = [...new Set([...existing.participants, ...people])];
155
+ patch.latestInboundId = item.id;
156
+ patch.lastActivityAt = receivedAt;
157
+ if (existing.state === "review")
158
+ patch.reviewReason = "stale_draft";
159
+ if (reply.next === "new")
160
+ patch.reviewReason = null;
161
+ }
162
+ outcome = await deps.store.appendMessage({ ...base, threadId }, { threadId, expectedState: existing.state, patch });
163
+ }
164
+ else {
165
+ threadId = (0, internal_1.newId)(deps);
166
+ const thread = {
167
+ id: threadId,
168
+ mailbox,
169
+ state: isNoise ? "ignored" : "new",
170
+ subjectKey: (0, subject_1.normalizeSubject)(subject),
171
+ subject: subject || "(no subject)",
172
+ customerAddress: customer,
173
+ participants: people,
174
+ createdAt: (0, internal_1.nowIso)(deps),
175
+ lastActivityAt: receivedAt,
176
+ latestInboundId: isNoise ? null : item.id,
177
+ reviewReason: null,
178
+ autoReplyCount: 0,
179
+ relatedThreadId: existing?.id ?? null,
180
+ dismissedBy: null,
181
+ };
182
+ outcome = await deps.store.createThread(thread, { ...base, threadId });
183
+ }
184
+ if (outcome === "stale")
185
+ continue;
186
+ if (outcome === "duplicate") {
187
+ result.duplicates++;
188
+ const stored = await deps.store.getMessage(mailbox, item.id);
189
+ if (stored)
190
+ await reconcileExisting(deps, stored, result);
191
+ return;
192
+ }
193
+ if (outcome === "created")
194
+ result.created++;
195
+ else
196
+ result.appended++;
197
+ if (isNoise) {
198
+ result.ignored++;
199
+ return;
200
+ }
201
+ const stored = { ...base, threadId };
202
+ if ((await (0, drafting_1.startDraftFor)(deps, threadId, stored)) === "started")
203
+ result.draftsStarted++;
204
+ return;
205
+ }
206
+ throw new errors_1.StaleThreadError(item.id);
207
+ }
208
+ function headerRefs(headers) {
209
+ return [headers.inReplyTo, ...headers.references]
210
+ .map(threading_1.normalizeMessageId)
211
+ .filter((x) => x !== null);
212
+ }
213
+ async function recordBounce(deps, mailbox, message, refs, result) {
214
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
215
+ const thread = await (0, bounce_1.matchBounce)(deps.store, { mailbox, refs, conversationId: message.conversationId });
216
+ if (!thread)
217
+ return false;
218
+ const failed = (0, states_1.canTransition)(thread.state, "bounce");
219
+ const patch = failed
220
+ ? { state: (0, states_1.transition)(thread.state, "bounce"), reviewReason: "delivery_failed", lastActivityAt: message.receivedAt }
221
+ : {};
222
+ const outcome = await deps.store.appendMessage({ ...message, threadId: thread.id }, { threadId: thread.id, expectedState: thread.state, patch });
223
+ if (outcome === "stale")
224
+ continue;
225
+ if (outcome === "duplicate") {
226
+ result.duplicates++;
227
+ return true;
228
+ }
229
+ result.appended++;
230
+ result.ignored++;
231
+ if (failed) {
232
+ result.deliveryFailures++;
233
+ await (0, internal_1.notify)(deps, { type: "review_needed", threadId: thread.id, mailbox, subject: thread.subject, reason: "delivery_failed" });
234
+ }
235
+ return true;
236
+ }
237
+ throw new errors_1.StaleThreadError(message.id);
238
+ }
239
+ async function processItem(deps, gateway, mailbox, item, state, result) {
240
+ if (item.removed) {
241
+ const stored = await deps.store.getMessage(mailbox, item.id);
242
+ if (stored)
243
+ await deps.store.markMessageRemoved(mailbox, item.id, (0, internal_1.nowIso)(deps));
244
+ result.removed++;
245
+ return;
246
+ }
247
+ if (item.isDraft) {
248
+ result.skippedDrafts++;
249
+ return;
250
+ }
251
+ if (item.from?.address.trim().toLowerCase() === mailbox.trim().toLowerCase()) {
252
+ result.skippedOwn++;
253
+ return;
254
+ }
255
+ const stored = await deps.store.getMessage(mailbox, item.id);
256
+ if (stored) {
257
+ result.unchanged++;
258
+ await reconcileExisting(deps, stored, result);
259
+ return;
260
+ }
261
+ await processNew(deps, gateway, mailbox, item, state, result);
262
+ }
263
+ async function ingestPage(deps, mailbox) {
264
+ const controls = await (0, controls_1.loadControls)(deps);
265
+ if (controls.paused)
266
+ return (0, controls_1.paused)();
267
+ const gateway = (0, controls_1.mailboxAccess)(deps, controls, mailbox).gateway();
268
+ const state = await deps.store.getSyncState(mailbox);
269
+ if (!state)
270
+ throw new errors_1.MailboxNotConnectedError(mailbox);
271
+ const { page, restarted } = await fetchPage(deps, gateway, state);
272
+ const link = page.nextLink ?? page.deltaLink;
273
+ if (!link)
274
+ throw new errors_1.InvalidDeltaPageError();
275
+ const result = emptyResult();
276
+ result.restarted = restarted;
277
+ result.fetched = page.items.length;
278
+ const stamp = (i) => i.receivedDateTime ?? "";
279
+ const live = page.items.filter((i) => !i.removed).sort((a, b) => stamp(a).localeCompare(stamp(b)));
280
+ const gone = page.items.filter((i) => i.removed);
281
+ let newest = state.lastReceivedAt;
282
+ for (const item of [...live, ...gone]) {
283
+ await processItem(deps, gateway, mailbox, item, state, result);
284
+ if (item.receivedDateTime && (!newest || item.receivedDateTime > newest)) {
285
+ newest = item.receivedDateTime;
286
+ }
287
+ }
288
+ await deps.store.saveSyncState(mailbox, { ...state, link, lastReceivedAt: newest });
289
+ result.hasMore = page.nextLink !== null;
290
+ return result;
291
+ }
292
+ async function syncMailbox(deps, mailbox, options = {}) {
293
+ const results = [];
294
+ try {
295
+ for (let i = 0; i < (options.maxPages ?? 20); i++) {
296
+ const r = await ingestPage(deps, mailbox);
297
+ if ((0, controls_1.isPaused)(r))
298
+ return results.length ? results : r;
299
+ results.push(r);
300
+ if (!r.hasMore)
301
+ break;
302
+ }
303
+ }
304
+ catch (err) {
305
+ if (!(0, errors_1.isTransientMailError)(err))
306
+ throw err;
307
+ return recordSyncFailure(deps, mailbox, err);
308
+ }
309
+ await clearSyncFailure(deps, mailbox);
310
+ return results;
311
+ }
312
+ exports.SYNC_FAILING_AFTER = 3;
313
+ async function recordSyncFailure(deps, mailbox, err) {
314
+ const state = await deps.store.getSyncState(mailbox);
315
+ const failingSince = state?.failingSince ?? (0, internal_1.nowIso)(deps);
316
+ const failures = (state?.failures ?? 0) + 1;
317
+ const error = (0, internal_1.errorText)(err).slice(0, 300);
318
+ if (state)
319
+ await deps.store.saveSyncState(mailbox, { ...state, failingSince, failures, lastError: error });
320
+ if (failures === exports.SYNC_FAILING_AFTER)
321
+ await (0, internal_1.notify)(deps, { type: "sync_failing", mailbox, since: failingSince });
322
+ return { status: "failing", failures, failingSince, error };
323
+ }
324
+ async function clearSyncFailure(deps, mailbox) {
325
+ const state = await deps.store.getSyncState(mailbox);
326
+ if (!state || !state.failures)
327
+ return;
328
+ await deps.store.saveSyncState(mailbox, { ...state, failingSince: null, failures: 0, lastError: null });
329
+ if (state.failures >= exports.SYNC_FAILING_AFTER && state.failingSince) {
330
+ await (0, internal_1.notify)(deps, { type: "sync_recovered", mailbox, since: state.failingSince });
331
+ }
332
+ }
333
+ function isSyncFailing(value) {
334
+ return typeof value === "object" && value !== null && value.status === "failing";
335
+ }
@@ -0,0 +1,8 @@
1
+ import { type TicketEvent } from "./states";
2
+ import type { HelpdeskDeps, HelpdeskNotification, Thread, ThreadPatch } from "./types";
3
+ export declare const newId: (deps: HelpdeskDeps) => string;
4
+ export declare const nowIso: (deps: HelpdeskDeps) => string;
5
+ export declare function errorText(err: unknown): string;
6
+ export declare function notify(deps: HelpdeskDeps, event: HelpdeskNotification): Promise<void>;
7
+ export declare function loadThread(deps: HelpdeskDeps, threadId: string): Promise<Thread>;
8
+ export declare function applyEvents(deps: HelpdeskDeps, thread: Thread, events: TicketEvent[], patch?: ThreadPatch): Promise<Thread>;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nowIso = exports.newId = void 0;
4
+ exports.errorText = errorText;
5
+ exports.notify = notify;
6
+ exports.loadThread = loadThread;
7
+ exports.applyEvents = applyEvents;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const errors_1 = require("./errors");
10
+ const states_1 = require("./states");
11
+ const newId = (deps) => (deps.newId ?? node_crypto_1.randomUUID)();
12
+ exports.newId = newId;
13
+ const nowIso = (deps) => deps.clock.now().toISOString();
14
+ exports.nowIso = nowIso;
15
+ function errorText(err) {
16
+ return err instanceof Error ? err.message : String(err);
17
+ }
18
+ async function notify(deps, event) {
19
+ try {
20
+ await deps.notifier?.notify(event);
21
+ }
22
+ catch {
23
+ }
24
+ }
25
+ async function loadThread(deps, threadId) {
26
+ const thread = await deps.store.getThread(threadId);
27
+ if (!thread)
28
+ throw new errors_1.ThreadNotFoundError(threadId);
29
+ return thread;
30
+ }
31
+ async function applyEvents(deps, thread, events, patch = {}) {
32
+ let state = thread.state;
33
+ for (const event of events)
34
+ state = (0, states_1.transition)(state, event);
35
+ const full = { ...patch, state, lastActivityAt: patch.lastActivityAt ?? (0, exports.nowIso)(deps) };
36
+ const ok = await deps.store.updateThread({
37
+ threadId: thread.id,
38
+ expectedState: thread.state,
39
+ patch: full,
40
+ });
41
+ if (!ok)
42
+ throw new errors_1.StaleThreadError(thread.id);
43
+ return { ...thread, ...full, state };
44
+ }