@dxv-systems/helpdesk 0.1.0 → 0.2.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/sending.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.classifySendError = classifySendError;
4
+ exports.sendIntentId = sendIntentId;
4
5
  exports.approveAndSend = approveAndSend;
5
6
  exports.reconcileUnknownSends = reconcileUnknownSends;
6
7
  exports.resolveUnknownSend = resolveUnknownSend;
@@ -8,7 +9,6 @@ exports.markRepliedExternally = markRepliedExternally;
8
9
  exports.dismissThread = dismissThread;
9
10
  exports.markWaiting = markWaiting;
10
11
  exports.closeThread = closeThread;
11
- const cleanup_1 = require("./cleanup");
12
12
  const controls_1 = require("./controls");
13
13
  const draft_body_1 = require("./draft-body");
14
14
  const errors_1 = require("./errors");
@@ -22,6 +22,9 @@ function classifySendError(err) {
22
22
  return typeof status === "number" && NOT_SENT_STATUSES.has(status) ? "not_sent" : "unknown";
23
23
  }
24
24
  const SEND_IN_FLIGHT_MS = 2 * 60 * 1000;
25
+ function sendIntentId(run) {
26
+ return `send:${run.mailbox}:${run.idempotencyKey}`;
27
+ }
25
28
  async function markSent(deps, thread, intent) {
26
29
  const fresh = await (0, internal_1.loadThread)(deps, thread.id);
27
30
  const sent = await deps.store.transitionSendIntent(intent.id, ["sending", "unknown"], {
@@ -58,7 +61,7 @@ async function sendingElsewhereOrLost(deps, thread, intent) {
58
61
  }
59
62
  return surfaceUnknown(deps, thread, intent);
60
63
  }
61
- async function prepareAppDraft(deps, access, run, intent) {
64
+ async function prepareDraft(deps, access, run, intent) {
62
65
  const gateway = access.gateway();
63
66
  let current = run;
64
67
  if (!intent.draftId || run.placedDraftId !== intent.draftId || run.placedBodyHtml === null) {
@@ -66,7 +69,6 @@ async function prepareAppDraft(deps, access, run, intent) {
66
69
  current = await deps.store.updateDraftRun(run.idempotencyKey, {
67
70
  placedDraftId: draft.id,
68
71
  placedBodyHtml: draft.body.content,
69
- placementError: null,
70
72
  });
71
73
  await deps.store.transitionSendIntent(intent.id, ["sending"], { draftId: draft.id });
72
74
  }
@@ -84,21 +86,17 @@ async function approveAndSend(deps, input) {
84
86
  const access = (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox);
85
87
  if (access.mode === "read_only")
86
88
  return { status: "write_blocked", mode: access.mode };
87
- const inApp = access.mode === "drafts_in_app";
88
89
  const run = (0, drafting_1.currentReplyRun)(deps, thread, await deps.store.getLatestDraftRunForThread(thread.id));
89
- if (!run || (inApp ? run.status !== "succeeded" : !run.placedDraftId)) {
90
- throw new errors_1.DraftNotReadyError(thread.id, inApp ? "no approved draft text" : "no placed draft");
91
- }
92
- const ids = (0, cleanup_1.sendIntentIds)(run);
93
- for (const id of [ids.perRun, ids.perDraft]) {
94
- const prior = id ? await deps.store.getSendIntent(id) : null;
95
- if (prior?.status === "sent")
96
- return { status: "already_sent" };
97
- if (prior?.status === "unknown")
98
- return surfaceUnknown(deps, thread, prior);
99
- if (prior?.status === "sending")
100
- return sendingElsewhereOrLost(deps, thread, prior);
101
- }
90
+ if (!run || run.status !== "succeeded")
91
+ throw new errors_1.DraftNotReadyError(thread.id, "no reply text to approve");
92
+ const intentId = sendIntentId(run);
93
+ const prior = await deps.store.getSendIntent(intentId);
94
+ if (prior?.status === "sent")
95
+ return { status: "already_sent" };
96
+ if (prior?.status === "unknown")
97
+ return surfaceUnknown(deps, thread, prior);
98
+ if (prior?.status === "sending")
99
+ return sendingElsewhereOrLost(deps, thread, prior);
102
100
  (0, states_1.transition)(thread.state, "approve");
103
101
  if ((run.commentText ?? run.text ?? "").trim() === "")
104
102
  throw new errors_1.EmptyReplyError(thread.id);
@@ -107,10 +105,10 @@ async function approveAndSend(deps, input) {
107
105
  }
108
106
  const now = (0, internal_1.nowIso)(deps);
109
107
  const { intent } = await deps.store.createSendIntentIfAbsent({
110
- id: inApp ? ids.perRun : ids.perDraft,
108
+ id: intentId,
111
109
  mailbox: thread.mailbox,
112
110
  threadId: thread.id,
113
- draftId: inApp ? null : run.placedDraftId,
111
+ draftId: null,
114
112
  approvedBy: input.approvedBy,
115
113
  status: "intended",
116
114
  createdAt: now,
@@ -134,10 +132,9 @@ async function approveAndSend(deps, input) {
134
132
  await failNotSent(deps, intent.id, new Error("ticket changed before sending"));
135
133
  throw new errors_1.StaleThreadError(thread.id);
136
134
  }
137
- let draftId = run.placedDraftId;
135
+ let draftId;
138
136
  try {
139
- if (inApp)
140
- draftId = await prepareAppDraft(deps, access, run, claimed);
137
+ draftId = await prepareDraft(deps, access, run, claimed);
141
138
  await (0, reply_attachments_1.placeReplyAttachments)(deps, access, thread.id, draftId);
142
139
  }
143
140
  catch (err) {
@@ -202,14 +199,8 @@ async function resolveUnknownSend(deps, input) {
202
199
  const run = await deps.store.getLatestDraftRunForThread(thread.id);
203
200
  if (!run)
204
201
  throw new errors_1.DraftNotReadyError(thread.id, "no draft run");
205
- const ids = (0, cleanup_1.sendIntentIds)(run);
206
- let current = null;
207
- for (const id of [ids.perRun, ids.perDraft]) {
208
- const found = id ? await deps.store.getSendIntent(id) : null;
209
- if (found?.status === "unknown")
210
- current = found;
211
- }
212
- if (!current)
202
+ const current = await deps.store.getSendIntent(sendIntentId(run));
203
+ if (current?.status !== "unknown")
213
204
  throw new errors_1.DraftNotReadyError(thread.id, "no unknown send to resolve");
214
205
  const id = current.id;
215
206
  if (input.outcome === "not_sent") {
@@ -229,56 +220,42 @@ async function markRepliedExternally(deps, threadId) {
229
220
  if (controls.paused)
230
221
  return (0, controls_1.paused)();
231
222
  const thread = await (0, internal_1.loadThread)(deps, threadId);
232
- const access = (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox);
223
+ (0, controls_1.assertInScope)(deps.config, thread.mailbox);
233
224
  const updated = await (0, internal_1.applyEvents)(deps, thread, ["approve"], { reviewReason: null });
234
225
  await (0, reply_attachments_1.discardPendingReplyAttachments)(deps, thread.id);
235
- const run = await deps.store.getLatestDraftRunForThread(thread.id).catch(() => null);
236
- if (run)
237
- await (0, cleanup_1.discardPlacedDraft)(deps, run, access, thread.id);
238
226
  return updated;
239
227
  }
240
228
  async function dismissThread(deps, input) {
241
- const controls = await (0, controls_1.loadControls)(deps);
242
- if (controls.paused)
229
+ if ((await (0, controls_1.loadControls)(deps)).paused)
243
230
  return (0, controls_1.paused)();
244
231
  const thread = await (0, internal_1.loadThread)(deps, input.threadId);
245
- const access = (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox);
232
+ (0, controls_1.assertInScope)(deps.config, thread.mailbox);
246
233
  (0, states_1.transition)(thread.state, "dismiss");
247
234
  const run = await deps.store.getLatestDraftRunForThread(thread.id);
248
- for (const id of run ? Object.values((0, cleanup_1.sendIntentIds)(run)) : []) {
249
- const intent = id ? await deps.store.getSendIntent(id) : null;
250
- if (intent?.status === "sending") {
251
- const seen = await sendingElsewhereOrLost(deps, thread, intent);
252
- throw new errors_1.SendPendingError(thread.id, seen.status === "unknown_outcome" ? "unknown" : "sending");
253
- }
254
- if (intent?.status === "unknown")
255
- throw new errors_1.SendPendingError(thread.id, "unknown");
235
+ const intent = run ? await deps.store.getSendIntent(sendIntentId(run)) : null;
236
+ if (intent?.status === "sending") {
237
+ const seen = await sendingElsewhereOrLost(deps, thread, intent);
238
+ throw new errors_1.SendPendingError(thread.id, seen.status === "unknown_outcome" ? "unknown" : "sending");
256
239
  }
240
+ if (intent?.status === "unknown")
241
+ throw new errors_1.SendPendingError(thread.id, "unknown");
257
242
  const updated = await (0, internal_1.applyEvents)(deps, thread, ["dismiss"], {
258
243
  reviewReason: null,
259
244
  dismissedBy: input.dismissedBy,
260
245
  });
261
- await stopDrafting(deps, access, thread.id);
246
+ await stopDrafting(deps, thread.id);
262
247
  await (0, reply_attachments_1.discardPendingReplyAttachments)(deps, thread.id);
263
248
  return updated;
264
249
  }
265
- async function stopDrafting(deps, access, threadId) {
250
+ async function stopDrafting(deps, threadId) {
266
251
  try {
267
- let latest = await deps.store.getLatestDraftRunForThread(threadId);
268
- if (!latest)
269
- return;
270
- if (latest.status === "requested" || latest.status === "started") {
271
- latest = await deps.store.updateDraftRun(latest.idempotencyKey, {
252
+ const latest = await deps.store.getLatestDraftRunForThread(threadId);
253
+ if (latest?.status === "requested" || latest?.status === "started") {
254
+ await deps.store.updateDraftRun(latest.idempotencyKey, {
272
255
  status: "failed",
273
256
  error: "stopped: staff marked the ticket as needing no reply",
274
257
  });
275
258
  }
276
- const previous = latest.generation > 1
277
- ? await deps.store.getDraftRunGeneration(latest.mailbox, latest.inboundMessageId, latest.generation - 1)
278
- : null;
279
- for (const run of previous ? [latest, previous] : [latest]) {
280
- await (0, cleanup_1.discardPlacedDraft)(deps, run, access, threadId);
281
- }
282
259
  }
283
260
  catch {
284
261
  }
package/dist/states.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { TicketState } from "./types";
2
2
  export declare const TICKET_STATES: readonly TicketState[];
3
- export type TicketEvent = "ignore" | "draft" | "needs_review" | "auto_send" | "approve" | "redraft" | "settle" | "customer_reply" | "close" | "unignore" | "dismiss" | "bounce";
3
+ export type TicketEvent = "ignore" | "draft" | "needs_review" | "approve" | "redraft" | "settle" | "customer_reply" | "close" | "unignore" | "dismiss" | "bounce";
4
4
  export declare const TICKET_EVENTS: readonly TicketEvent[];
5
5
  export declare function transition(from: TicketState, event: TicketEvent): TicketState;
6
6
  export declare function canTransition(from: TicketState, event: TicketEvent): boolean;
package/dist/states.js CHANGED
@@ -18,7 +18,6 @@ exports.TICKET_EVENTS = [
18
18
  "ignore",
19
19
  "draft",
20
20
  "needs_review",
21
- "auto_send",
22
21
  "approve",
23
22
  "redraft",
24
23
  "settle",
@@ -31,7 +30,7 @@ exports.TICKET_EVENTS = [
31
30
  const TABLE = {
32
31
  new: { ignore: "ignored", draft: "drafted", dismiss: "ignored" },
33
32
  ignored: { unignore: "new" },
34
- drafted: { needs_review: "review", auto_send: "sent", dismiss: "ignored" },
33
+ drafted: { needs_review: "review", dismiss: "ignored" },
35
34
  review: { approve: "sent", redraft: "drafted", dismiss: "ignored" },
36
35
  sent: { settle: "waiting", bounce: "review" },
37
36
  waiting: { customer_reply: "new", close: "closed", bounce: "review" },
@@ -53,14 +53,12 @@ export interface FakeMailbox extends MailboxGateway {
53
53
  sendBehaviour: SendBehaviour;
54
54
  sent: string[];
55
55
  drafts: Map<string, Message>;
56
- deleteCalls: string[];
57
56
  draftAttachments: Map<string, Array<{
58
57
  name: string;
59
58
  contentType: string;
60
59
  size: number;
61
60
  }>>;
62
61
  addAttachmentError: Error | null;
63
- deleteError: Error | null;
64
62
  outage: Error | null;
65
63
  getMessageCalls: number;
66
64
  deltaCalls: DeltaOptions[];
@@ -132,11 +132,6 @@ function createMemoryStore() {
132
132
  draftRuns.push(clone(run));
133
133
  return { created: true, run: clone(run) };
134
134
  },
135
- async getDraftRunGeneration(mailbox, inboundMessageId, generation) {
136
- await tick();
137
- const r = draftRuns.find((x) => x.mailbox === mailbox && x.inboundMessageId === inboundMessageId && x.generation === generation);
138
- return r ? clone(r) : null;
139
- },
140
135
  async getLatestDraftRun(mailbox, inboundMessageId) {
141
136
  await tick();
142
137
  const runs = draftRuns.filter((r) => r.mailbox === mailbox && r.inboundMessageId === inboundMessageId);
@@ -186,7 +181,7 @@ function createMemoryStore() {
186
181
  },
187
182
  };
188
183
  }
189
- const WRITE_METHODS = ["createReply", "updateDraftBody", "sendDraft", "deleteDraft"];
184
+ const WRITE_METHODS = ["createReply", "updateDraftBody", "sendDraft"];
190
185
  function createFakeMailbox(address) {
191
186
  const mails = new Map();
192
187
  const log = [];
@@ -210,10 +205,8 @@ function createFakeMailbox(address) {
210
205
  sent: [],
211
206
  drafts,
212
207
  getMessageCalls: 0,
213
- deleteCalls: [],
214
208
  draftAttachments: new Map(),
215
209
  addAttachmentError: null,
216
- deleteError: null,
217
210
  outage: null,
218
211
  deltaCalls: [],
219
212
  calls: [],
@@ -339,16 +332,6 @@ function createFakeMailbox(address) {
339
332
  throw new Error(`no draft ${draftId}`);
340
333
  d.body = clone(body);
341
334
  },
342
- async deleteDraft(draftId) {
343
- await tick();
344
- box.calls.push(`deleteDraft:${draftId}`);
345
- box.deleteCalls.push(draftId);
346
- if (box.deleteError)
347
- throw box.deleteError;
348
- if (box.sent.includes(draftId))
349
- return { status: "not_draft" };
350
- return { status: drafts.delete(draftId) ? "deleted" : "gone" };
351
- },
352
335
  async addAttachment(draftId, attachment) {
353
336
  await tick();
354
337
  box.calls.push(`addAttachment:${draftId}:${attachment.name}`);
package/dist/types.d.ts CHANGED
@@ -78,9 +78,6 @@ export interface MailboxGateway {
78
78
  contentType: string;
79
79
  content: Uint8Array;
80
80
  }): Promise<unknown>;
81
- deleteDraft(draftId: string): Promise<{
82
- status: "deleted" | "gone" | "not_draft";
83
- }>;
84
81
  }
85
82
  export interface Clock {
86
83
  now(): Date;
@@ -140,7 +137,7 @@ export interface ReplyAttachmentsDeps {
140
137
  files: ReplyFiles;
141
138
  }
142
139
  export type TicketState = "new" | "ignored" | "drafted" | "review" | "sent" | "waiting" | "closed";
143
- export type ReviewReason = "low_confidence" | "policy" | "stale_draft" | "drafter_failed" | "placement_failed" | "staff_edited" | "no_draft" | "delivery_failed";
140
+ export type ReviewReason = "low_confidence" | "policy" | "stale_draft" | "drafter_failed" | "staff_edited" | "no_draft" | "delivery_failed";
144
141
  export interface Thread {
145
142
  id: string;
146
143
  mailbox: string;
@@ -212,8 +209,6 @@ export interface DraftRun {
212
209
  placedDraftId: string | null;
213
210
  placedBodyHtml: string | null;
214
211
  commentText: string | null;
215
- placementError: string | null;
216
- draftCleanupError: string | null;
217
212
  }
218
213
  export type SendStatus = "intended" | "sending" | "sent" | "failed" | "unknown";
219
214
  export interface SendIntent {
@@ -249,7 +244,6 @@ export interface HelpdeskStore {
249
244
  run: DraftRun;
250
245
  }>;
251
246
  getLatestDraftRun(mailbox: string, inboundMessageId: string): Promise<DraftRun | null>;
252
- getDraftRunGeneration(mailbox: string, inboundMessageId: string, generation: number): Promise<DraftRun | null>;
253
247
  getLatestDraftRunForThread(threadId: string): Promise<DraftRun | null>;
254
248
  listInFlightDraftRuns(mailbox: string, olderThan: string): Promise<DraftRun[]>;
255
249
  updateDraftRun(key: string, patch: Partial<Omit<DraftRun, "idempotencyKey">>): Promise<DraftRun>;
@@ -319,7 +313,7 @@ export interface HelpdeskConfig {
319
313
  minConfidence?: number;
320
314
  pageSize?: number;
321
315
  }
322
- export type MailboxWriteMode = "read_only" | "drafts_in_app" | "drafts_in_outlook";
316
+ export type MailboxWriteMode = "read_only" | "approve_and_send";
323
317
  export interface HelpdeskControls {
324
318
  paused: boolean;
325
319
  writeMode?: MailboxWriteMode;
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
- {
2
- "name": "@dxv-systems/helpdesk",
3
- "version": "0.1.0",
4
- "description": "Mailbox helpdesk domain logic for DXV apps — idempotent delta ingestion, conservative threading, ticket state machine, noise detection, draft placement that keeps the quoted history, retry-safe sending and an auto-send policy evaluator. Pure logic behind interfaces, plus the staff inbox as a route handler (./server) and a React component (./react). Zero runtime dependencies.",
5
- "keywords": [
6
- "dxv",
7
- "helpdesk",
8
- "mail",
9
- "microsoft-graph",
10
- "threading",
11
- "tickets"
12
- ],
13
- "engines": {
14
- "node": ">=20.0.0"
15
- },
16
- "exports": {
17
- ".": {
18
- "types": "./dist/index.d.ts",
19
- "default": "./dist/index.js"
20
- },
21
- "./testing": {
22
- "types": "./dist/testing/index.d.ts",
23
- "default": "./dist/testing/index.js"
24
- },
25
- "./server": {
26
- "types": "./dist/server.d.ts",
27
- "default": "./dist/server.js"
28
- },
29
- "./react": {
30
- "types": "./dist/react.d.ts",
31
- "default": "./dist/react.js"
32
- }
33
- },
34
- "main": "./dist/index.js",
35
- "types": "./dist/index.d.ts",
36
- "files": [
37
- "dist"
38
- ],
39
- "sideEffects": false,
40
- "scripts": {
41
- "build": "tsc",
42
- "test": "vitest run",
43
- "test:watch": "vitest",
44
- "prepublishOnly": "npm run build"
45
- },
46
- "peerDependencies": {
47
- "react": ">=18"
48
- },
49
- "peerDependenciesMeta": {
50
- "react": {
51
- "optional": true
52
- }
53
- },
54
- "devDependencies": {
55
- "@testing-library/react": "^16.0.1",
56
- "@types/node": "^20.19.0",
57
- "@types/react": "^18.3.12",
58
- "jsdom": "^25.0.1",
59
- "react": "^18.3.1",
60
- "react-dom": "^18.3.1",
61
- "typescript": "^5.6.0",
62
- "vitest": "^4.1.11"
63
- },
64
- "repository": {
65
- "type": "git",
66
- "url": "git+https://github.com/dxv-systems/dxv-packages.git",
67
- "directory": "packages/helpdesk"
68
- },
69
- "publishConfig": {
70
- "access": "public"
71
- },
72
- "license": "MIT"
73
- }
1
+ {
2
+ "name": "@dxv-systems/helpdesk",
3
+ "version": "0.2.0",
4
+ "description": "Mailbox helpdesk domain logic for DXV apps — idempotent delta ingestion, conservative threading, ticket state machine, noise detection, and replies that a person approves in the helpdesk before they are sent from the mailbox (quoted history kept, retry-safe, never sent twice). Pure logic behind interfaces, plus the staff inbox as a route handler (./server) and a React component (./react). Zero runtime dependencies.",
5
+ "keywords": [
6
+ "dxv",
7
+ "helpdesk",
8
+ "mail",
9
+ "microsoft-graph",
10
+ "threading",
11
+ "tickets"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./testing": {
22
+ "types": "./dist/testing/index.d.ts",
23
+ "default": "./dist/testing/index.js"
24
+ },
25
+ "./server": {
26
+ "types": "./dist/server.d.ts",
27
+ "default": "./dist/server.js"
28
+ },
29
+ "./react": {
30
+ "types": "./dist/react.d.ts",
31
+ "default": "./dist/react.js"
32
+ }
33
+ },
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "sideEffects": false,
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "prepublishOnly": "npm run build"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "react": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@testing-library/react": "^16.0.1",
56
+ "@types/node": "^20.19.0",
57
+ "@types/react": "^18.3.12",
58
+ "jsdom": "^25.0.1",
59
+ "react": "^18.3.1",
60
+ "react-dom": "^18.3.1",
61
+ "typescript": "^5.6.0",
62
+ "vitest": "^4.1.11"
63
+ },
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/dxv-systems/dxv-packages.git",
67
+ "directory": "packages/helpdesk"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ },
72
+ "license": "MIT"
73
+ }
@@ -1,43 +0,0 @@
1
- import type { MailboxWriteMode, TicketState } from "./types";
2
- export interface OfficeHours {
3
- timeZone: string;
4
- windows: Array<{
5
- days: number[];
6
- start: string;
7
- end: string;
8
- }>;
9
- }
10
- export interface AutoSendPolicy {
11
- enabled: boolean;
12
- categories?: string[];
13
- senders?: string[];
14
- minConfidence: number;
15
- officeHours?: OfficeHours;
16
- maxAutoRepliesPerThread: number;
17
- internalDomains?: string[];
18
- }
19
- export declare const DEFAULT_AUTO_SEND_POLICY: AutoSendPolicy;
20
- export interface AutoSendContext {
21
- paused: boolean;
22
- writeMode: MailboxWriteMode;
23
- threadState: TicketState;
24
- inboundIgnored: boolean;
25
- sender: string;
26
- category?: string | null;
27
- confidence: number;
28
- now: Date;
29
- autoRepliesInThread: number;
30
- suppressAutoResponse: boolean;
31
- draftIsStale: boolean;
32
- ownAddresses: string[];
33
- connectedMailboxes: string[];
34
- }
35
- export type AutoSendDecision = {
36
- allow: true;
37
- reason: "all_rules_passed";
38
- } | {
39
- allow: false;
40
- reason: "paused" | "write_mode" | "disabled" | "ignored_mail" | "internal_sender" | "auto_response_suppressed" | "wrong_state" | "stale_draft" | "no_scope_configured" | "sender_not_allowed" | "category_not_allowed" | "low_confidence" | "outside_office_hours" | "max_auto_replies";
41
- detail?: string;
42
- };
43
- export declare function evaluateAutoSend(policy: AutoSendPolicy, ctx: AutoSendContext): AutoSendDecision;
package/dist/autosend.js DELETED
@@ -1,83 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_AUTO_SEND_POLICY = void 0;
4
- exports.evaluateAutoSend = evaluateAutoSend;
5
- exports.DEFAULT_AUTO_SEND_POLICY = {
6
- enabled: false,
7
- minConfidence: 0.9,
8
- maxAutoRepliesPerThread: 1,
9
- };
10
- const WEEKDAYS = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
11
- function minutes(hhmm) {
12
- const [h, m] = hhmm.split(":");
13
- return Number(h) * 60 + Number(m ?? 0);
14
- }
15
- function withinOfficeHours(hours, now) {
16
- const parts = new Intl.DateTimeFormat("en-US", {
17
- timeZone: hours.timeZone,
18
- weekday: "short",
19
- hour: "2-digit",
20
- minute: "2-digit",
21
- hourCycle: "h23",
22
- }).formatToParts(now);
23
- const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
24
- const day = WEEKDAYS[get("weekday")];
25
- const at = Number(get("hour")) * 60 + Number(get("minute"));
26
- return hours.windows.some((w) => day !== undefined && w.days.includes(day) && at >= minutes(w.start) && at < minutes(w.end));
27
- }
28
- function senderMatches(sender, rules) {
29
- const domain = sender.split("@")[1] ?? "";
30
- return rules.some((r) => {
31
- const rule = r.trim().toLowerCase();
32
- return rule.startsWith("@") ? domain === rule.slice(1) : sender === rule;
33
- });
34
- }
35
- function evaluateAutoSend(policy, ctx) {
36
- const deny = (reason, detail) => ({
37
- allow: false,
38
- reason,
39
- ...(detail ? { detail } : {}),
40
- });
41
- const sender = ctx.sender.trim().toLowerCase();
42
- const domain = sender.split("@")[1] ?? "";
43
- if (ctx.paused)
44
- return deny("paused");
45
- if (ctx.writeMode !== "drafts_in_outlook")
46
- return deny("write_mode", ctx.writeMode);
47
- if (!policy.enabled)
48
- return deny("disabled");
49
- if (ctx.inboundIgnored || ctx.threadState === "ignored")
50
- return deny("ignored_mail");
51
- const internalAddresses = [...ctx.ownAddresses, ...ctx.connectedMailboxes].map((a) => a.toLowerCase());
52
- const internalDomains = (policy.internalDomains ?? []).map((d) => d.toLowerCase());
53
- if (internalAddresses.includes(sender) || internalDomains.includes(domain)) {
54
- return deny("internal_sender");
55
- }
56
- if (ctx.suppressAutoResponse)
57
- return deny("auto_response_suppressed");
58
- if (ctx.threadState !== "drafted")
59
- return deny("wrong_state", ctx.threadState);
60
- if (ctx.draftIsStale)
61
- return deny("stale_draft");
62
- const senders = policy.senders ?? [];
63
- const categories = policy.categories ?? [];
64
- if (senders.length === 0 && categories.length === 0)
65
- return deny("no_scope_configured");
66
- const senderOk = senders.length > 0 && senderMatches(sender, senders);
67
- const category = (ctx.category ?? "").toLowerCase();
68
- const categoryOk = categories.length > 0 && categories.some((c) => c.toLowerCase() === category);
69
- if (!senderOk && !categoryOk) {
70
- return senders.length > 0 && categories.length === 0
71
- ? deny("sender_not_allowed", sender)
72
- : deny("category_not_allowed", ctx.category ?? "none");
73
- }
74
- if (ctx.confidence < policy.minConfidence)
75
- return deny("low_confidence", String(ctx.confidence));
76
- if (policy.officeHours && !withinOfficeHours(policy.officeHours, ctx.now)) {
77
- return deny("outside_office_hours");
78
- }
79
- if (ctx.autoRepliesInThread >= policy.maxAutoRepliesPerThread) {
80
- return deny("max_auto_replies", String(ctx.autoRepliesInThread));
81
- }
82
- return { allow: true, reason: "all_rules_passed" };
83
- }
package/dist/cleanup.d.ts DELETED
@@ -1,7 +0,0 @@
1
- import type { MailboxAccess } from "./controls";
2
- import type { DraftRun, HelpdeskDeps } from "./types";
3
- export declare function sendIntentIds(run: DraftRun): {
4
- perRun: string;
5
- perDraft: string | null;
6
- };
7
- export declare function discardPlacedDraft(deps: HelpdeskDeps, run: DraftRun, access: MailboxAccess, threadId: string): Promise<void>;
package/dist/cleanup.js DELETED
@@ -1,39 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sendIntentIds = sendIntentIds;
4
- exports.discardPlacedDraft = discardPlacedDraft;
5
- const internal_1 = require("./internal");
6
- const KEEP_FOR_INTENT = ["intended", "sending", "unknown", "sent"];
7
- function sendIntentIds(run) {
8
- return {
9
- perRun: `send:${run.mailbox}:${run.idempotencyKey}`,
10
- perDraft: run.placedDraftId ? `send:${run.mailbox}:${run.placedDraftId}` : null,
11
- };
12
- }
13
- async function discardPlacedDraft(deps, run, access, threadId) {
14
- try {
15
- const draftId = run.placedDraftId;
16
- if (access.mode !== "drafts_in_outlook")
17
- return;
18
- if (!draftId || run.threadId !== threadId || run.mailbox !== access.mailbox)
19
- return;
20
- const { perRun, perDraft } = sendIntentIds(run);
21
- for (const id of [perRun, perDraft]) {
22
- const intent = id ? await deps.store.getSendIntent(id) : null;
23
- if (intent && KEEP_FOR_INTENT.includes(intent.status))
24
- return;
25
- }
26
- try {
27
- await access.gateway().deleteDraft(draftId);
28
- await deps.store.updateDraftRun(run.idempotencyKey, {
29
- placedDraftId: null,
30
- draftCleanupError: null,
31
- });
32
- }
33
- catch (err) {
34
- await deps.store.updateDraftRun(run.idempotencyKey, { draftCleanupError: (0, internal_1.errorText)(err) });
35
- }
36
- }
37
- catch {
38
- }
39
- }