@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/README.md +299 -279
- package/dist/bounce.d.ts +6 -0
- package/dist/bounce.js +38 -0
- package/dist/controls.js +1 -6
- package/dist/draft-body.d.ts +0 -1
- package/dist/draft-body.js +0 -14
- package/dist/drafting.d.ts +1 -2
- package/dist/drafting.js +12 -80
- package/dist/errors.d.ts +0 -3
- package/dist/errors.js +1 -7
- package/dist/inbox/handlers.js +27 -36
- package/dist/inbox/messages.d.ts +4 -5
- package/dist/inbox/messages.js +23 -14
- package/dist/inbox/types.d.ts +12 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -6
- package/dist/ingest.d.ts +4 -0
- package/dist/ingest.js +144 -22
- package/dist/react.d.ts +42 -2
- package/dist/react.js +249 -94
- package/dist/sending.d.ts +2 -1
- package/dist/sending.js +35 -58
- package/dist/states.d.ts +1 -1
- package/dist/states.js +1 -2
- package/dist/testing/index.d.ts +4 -2
- package/dist/testing/index.js +19 -29
- package/dist/types.d.ts +6 -8
- package/package.json +73 -73
- package/dist/autosend.d.ts +0 -43
- package/dist/autosend.js +0 -83
- package/dist/cleanup.d.ts +0 -7
- package/dist/cleanup.js +0 -39
package/dist/bounce.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { HelpdeskStore, Thread } from "./types";
|
|
2
2
|
export declare function bouncedReplyRefs(bodyText: string): string[];
|
|
3
|
+
export interface BounceReport {
|
|
4
|
+
recipient: string | null;
|
|
5
|
+
statusCode: string | null;
|
|
6
|
+
diagnostic: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function readBounceReport(text: string): BounceReport;
|
|
3
9
|
export declare function matchBounce(store: HelpdeskStore, input: {
|
|
4
10
|
mailbox: string;
|
|
5
11
|
refs: string[];
|
package/dist/bounce.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.bouncedReplyRefs = bouncedReplyRefs;
|
|
4
|
+
exports.readBounceReport = readBounceReport;
|
|
4
5
|
exports.matchBounce = matchBounce;
|
|
5
6
|
const threading_1 = require("./threading");
|
|
6
7
|
const QUOTED_HEADER = /^(?:in-reply-to|references)[ \t]*:[ \t]*(.*(?:\r?\n[ \t]+.*)*)/gim;
|
|
@@ -15,6 +16,43 @@ function bouncedReplyRefs(bodyText) {
|
|
|
15
16
|
}
|
|
16
17
|
return [...ids];
|
|
17
18
|
}
|
|
19
|
+
const MAX_DIAGNOSTIC_CHARS = 2000;
|
|
20
|
+
const ADDRESS = String.raw `<?([^\s<>;:()"',]+@[^\s<>;:()"',]+)>?`;
|
|
21
|
+
const RECIPIENT_PATTERNS = [
|
|
22
|
+
new RegExp(String.raw `^(?:final|original)-recipient:[ \t]*(?:rfc822;)?[ \t]*${ADDRESS}[ \t]*$`, "im"),
|
|
23
|
+
new RegExp(String.raw `^recipient:[ \t]*${ADDRESS}[ \t]*$`, "im"),
|
|
24
|
+
new RegExp(String.raw `(?:your message to|je bericht aan|uw bericht aan)[ \t]+${ADDRESS}(?=[\s.,]|$)`, "i"),
|
|
25
|
+
new RegExp(String.raw `(?:delivery has failed to these recipients or groups|bezorging is mislukt voor deze ontvangers of groepen):\s*${ADDRESS}(?=[\s.,]|$)`, "i"),
|
|
26
|
+
];
|
|
27
|
+
const CODE_PATTERNS = [/\b[245]\d\d[ -]#?([245]\.\d{1,3}\.\d{1,3})\b/, /(?:status:|#)[ \t]*([245]\.\d{1,3}\.\d{1,3})\b/i, /\b([45]\.\d{1,3}\.\d{1,3})\b/];
|
|
28
|
+
const DIAGNOSTIC_START = /(?:diagnostic information for administrators|diagnostische gegevens voor beheerders)\s*:/i;
|
|
29
|
+
const DIAGNOSTIC_END = /(?:original message headers|oorspronkelijke berichtkoppen)\s*:/i;
|
|
30
|
+
function readBounceReport(text) {
|
|
31
|
+
const body = text.replace(/\r\n?/g, "\n");
|
|
32
|
+
let recipient = null;
|
|
33
|
+
for (const re of RECIPIENT_PATTERNS) {
|
|
34
|
+
const found = re.exec(body)?.[1];
|
|
35
|
+
if (found) {
|
|
36
|
+
recipient = found.replace(/[.]+$/, "").toLowerCase();
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let statusCode = null;
|
|
41
|
+
for (const re of CODE_PATTERNS) {
|
|
42
|
+
const found = re.exec(body)?.[1];
|
|
43
|
+
if (found) {
|
|
44
|
+
statusCode = found;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const start = DIAGNOSTIC_START.exec(body);
|
|
49
|
+
const rest = start ? body.slice(start.index + start[0].length) : body;
|
|
50
|
+
const end = DIAGNOSTIC_END.exec(rest);
|
|
51
|
+
let diagnostic = (end ? rest.slice(0, end.index) : rest).replace(/\n{3,}/g, "\n\n").trim();
|
|
52
|
+
if (diagnostic.length > MAX_DIAGNOSTIC_CHARS)
|
|
53
|
+
diagnostic = `${diagnostic.slice(0, MAX_DIAGNOSTIC_CHARS).trimEnd()}…`;
|
|
54
|
+
return { recipient, statusCode, diagnostic };
|
|
55
|
+
}
|
|
18
56
|
const awaitsDelivery = (t) => t.state === "sent" || t.state === "waiting";
|
|
19
57
|
async function matchBounce(store, input) {
|
|
20
58
|
const { mailbox, refs, conversationId } = input;
|
package/dist/controls.js
CHANGED
|
@@ -10,8 +10,7 @@ const errors_1 = require("./errors");
|
|
|
10
10
|
exports.DEFAULT_WRITE_MODE = "read_only";
|
|
11
11
|
const ALLOWED_WRITES = {
|
|
12
12
|
read_only: [],
|
|
13
|
-
|
|
14
|
-
drafts_in_outlook: ["createReply", "updateDraftBody", "addAttachment", "sendDraft", "deleteDraft"],
|
|
13
|
+
approve_and_send: ["createReply", "updateDraftBody", "addAttachment", "sendDraft"],
|
|
15
14
|
};
|
|
16
15
|
const norm = (address) => address.trim().toLowerCase();
|
|
17
16
|
const paused = () => ({ status: "paused" });
|
|
@@ -79,9 +78,5 @@ function guardedGateway(deps, mailbox, mode) {
|
|
|
79
78
|
allow("sendDraft");
|
|
80
79
|
return gw.sendDraft(id);
|
|
81
80
|
},
|
|
82
|
-
async deleteDraft(id) {
|
|
83
|
-
allow("deleteDraft");
|
|
84
|
-
return gw.deleteDraft(id);
|
|
85
|
-
},
|
|
86
81
|
};
|
|
87
82
|
}
|
package/dist/draft-body.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export declare function renderCommentHtml(text: string): string;
|
|
2
2
|
export declare function findQuoteStart(html: string): number;
|
|
3
3
|
export declare function hasQuotedHistory(html: string): boolean;
|
|
4
|
-
export declare function replaceDraftComment(placedBodyHtml: string, newComment: string): string;
|
|
5
4
|
export declare function prependToDraftBody(bodyHtml: string, text: string): string;
|
package/dist/draft-body.js
CHANGED
|
@@ -3,9 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.renderCommentHtml = renderCommentHtml;
|
|
4
4
|
exports.findQuoteStart = findQuoteStart;
|
|
5
5
|
exports.hasQuotedHistory = hasQuotedHistory;
|
|
6
|
-
exports.replaceDraftComment = replaceDraftComment;
|
|
7
6
|
exports.prependToDraftBody = prependToDraftBody;
|
|
8
|
-
const errors_1 = require("./errors");
|
|
9
7
|
const QUOTE_MARKERS = [
|
|
10
8
|
/<div[^>]*\bid=["']appendonsend["']/i,
|
|
11
9
|
/<div[^>]*\bid=["']divRplyFwdMsg["']/i,
|
|
@@ -33,18 +31,6 @@ function findQuoteStart(html) {
|
|
|
33
31
|
function hasQuotedHistory(html) {
|
|
34
32
|
return findQuoteStart(html) >= 0;
|
|
35
33
|
}
|
|
36
|
-
function replaceDraftComment(placedBodyHtml, newComment) {
|
|
37
|
-
const quoteAt = findQuoteStart(placedBodyHtml);
|
|
38
|
-
if (quoteAt < 0)
|
|
39
|
-
throw new errors_1.QuoteNotFoundError();
|
|
40
|
-
const bodyTag = /<body\b[^>]*>/i.exec(placedBodyHtml);
|
|
41
|
-
const commentAt = bodyTag ? bodyTag.index + bodyTag[0].length : 0;
|
|
42
|
-
if (commentAt > quoteAt)
|
|
43
|
-
throw new errors_1.QuoteNotFoundError();
|
|
44
|
-
return (placedBodyHtml.slice(0, commentAt) +
|
|
45
|
-
renderCommentHtml(newComment) +
|
|
46
|
-
placedBodyHtml.slice(quoteAt));
|
|
47
|
-
}
|
|
48
34
|
function prependToDraftBody(bodyHtml, text) {
|
|
49
35
|
const bodyTag = /<body\b[^>]*>/i.exec(bodyHtml);
|
|
50
36
|
const at = bodyTag ? bodyTag.index + bodyTag[0].length : 0;
|
package/dist/drafting.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DraftResult, DraftRun, HelpdeskDeps, Paused, StoredMessage, Thread
|
|
1
|
+
import type { DraftResult, DraftRun, HelpdeskDeps, Paused, StoredMessage, Thread } from "./types";
|
|
2
2
|
export declare function currentReplyRun(deps: HelpdeskDeps, thread: Thread, run: DraftRun | null): DraftRun | null;
|
|
3
3
|
export type EnsureDraftOutcome = "started" | "exists" | "busy" | "not_needed" | "no_drafter" | "paused";
|
|
4
4
|
export declare function ensureDraft(deps: HelpdeskDeps, threadId: string, message: StoredMessage): Promise<EnsureDraftOutcome>;
|
|
@@ -14,6 +14,5 @@ export declare function editDraft(deps: HelpdeskDeps, input: {
|
|
|
14
14
|
threadId: string;
|
|
15
15
|
text: string;
|
|
16
16
|
}): Promise<DraftRun | Paused>;
|
|
17
|
-
export declare function retryPlacement(deps: HelpdeskDeps, threadId: string): Promise<DraftRun | Paused | WriteBlocked>;
|
|
18
17
|
export declare function sweepStuckDrafts(deps: HelpdeskDeps, mailbox: string): Promise<number | Paused>;
|
|
19
18
|
export declare function unignoreThread(deps: HelpdeskDeps, threadId: string): Promise<Thread | Paused>;
|
package/dist/drafting.js
CHANGED
|
@@ -6,12 +6,9 @@ exports.startDraftFor = startDraftFor;
|
|
|
6
6
|
exports.completeDraft = completeDraft;
|
|
7
7
|
exports.regenerateDraft = regenerateDraft;
|
|
8
8
|
exports.editDraft = editDraft;
|
|
9
|
-
exports.retryPlacement = retryPlacement;
|
|
10
9
|
exports.sweepStuckDrafts = sweepStuckDrafts;
|
|
11
10
|
exports.unignoreThread = unignoreThread;
|
|
12
|
-
const cleanup_1 = require("./cleanup");
|
|
13
11
|
const controls_1 = require("./controls");
|
|
14
|
-
const draft_body_1 = require("./draft-body");
|
|
15
12
|
const errors_1 = require("./errors");
|
|
16
13
|
const internal_1 = require("./internal");
|
|
17
14
|
const states_1 = require("./states");
|
|
@@ -29,8 +26,6 @@ function currentReplyRun(deps, thread, run) {
|
|
|
29
26
|
function reviewReasonFor(deps, thread, run) {
|
|
30
27
|
if (run.status === "failed")
|
|
31
28
|
return "drafter_failed";
|
|
32
|
-
if (run.placementError)
|
|
33
|
-
return "placement_failed";
|
|
34
29
|
if (thread.latestInboundId !== run.inboundMessageId)
|
|
35
30
|
return "stale_draft";
|
|
36
31
|
if ((run.confidence ?? 0) < (deps.config.minConfidence ?? DEFAULT_MIN_CONFIDENCE)) {
|
|
@@ -88,8 +83,6 @@ function newRun(deps, thread, messageId, generation) {
|
|
|
88
83
|
placedDraftId: null,
|
|
89
84
|
placedBodyHtml: null,
|
|
90
85
|
commentText: null,
|
|
91
|
-
placementError: null,
|
|
92
|
-
draftCleanupError: null,
|
|
93
86
|
};
|
|
94
87
|
}
|
|
95
88
|
async function ensureDraft(deps, threadId, message) {
|
|
@@ -132,31 +125,10 @@ async function startDraftFor(deps, threadId, message) {
|
|
|
132
125
|
await startRun(deps, run, message);
|
|
133
126
|
return "started";
|
|
134
127
|
}
|
|
135
|
-
async function place(deps, access, run, text) {
|
|
136
|
-
let placed;
|
|
137
|
-
try {
|
|
138
|
-
const draft = await access.gateway().createReply(run.inboundMessageId, { comment: text });
|
|
139
|
-
placed = await deps.store.updateDraftRun(run.idempotencyKey, {
|
|
140
|
-
placedDraftId: draft.id,
|
|
141
|
-
placedBodyHtml: draft.body.content,
|
|
142
|
-
placementError: null,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
catch (err) {
|
|
146
|
-
return deps.store.updateDraftRun(run.idempotencyKey, { placementError: (0, internal_1.errorText)(err) });
|
|
147
|
-
}
|
|
148
|
-
const previous = run.generation > 1
|
|
149
|
-
? await deps.store.getDraftRunGeneration(run.mailbox, run.inboundMessageId, run.generation - 1)
|
|
150
|
-
: null;
|
|
151
|
-
if (previous)
|
|
152
|
-
await (0, cleanup_1.discardPlacedDraft)(deps, previous, access, run.threadId);
|
|
153
|
-
return placed;
|
|
154
|
-
}
|
|
155
128
|
async function completeDraft(deps, input) {
|
|
156
|
-
|
|
157
|
-
if (controls.paused)
|
|
129
|
+
if ((await (0, controls_1.loadControls)(deps)).paused)
|
|
158
130
|
return (0, controls_1.paused)();
|
|
159
|
-
|
|
131
|
+
(0, controls_1.assertInScope)(deps.config, input.mailbox);
|
|
160
132
|
const run = await deps.store.getLatestDraftRun(input.mailbox, input.inboundMessageId);
|
|
161
133
|
if (!run)
|
|
162
134
|
throw new errors_1.DraftNotReadyError(input.inboundMessageId, "no draft run recorded");
|
|
@@ -169,7 +141,7 @@ async function completeDraft(deps, input) {
|
|
|
169
141
|
await failRun(deps, run, result.error);
|
|
170
142
|
return deps.store.updateDraftRun(run.idempotencyKey, {});
|
|
171
143
|
}
|
|
172
|
-
|
|
144
|
+
const current = await deps.store.updateDraftRun(run.idempotencyKey, {
|
|
173
145
|
status: "succeeded",
|
|
174
146
|
text: result.text,
|
|
175
147
|
commentText: result.text,
|
|
@@ -177,8 +149,6 @@ async function completeDraft(deps, input) {
|
|
|
177
149
|
reason: result.reason,
|
|
178
150
|
category: result.category ?? null,
|
|
179
151
|
});
|
|
180
|
-
if (access.mode === "drafts_in_outlook")
|
|
181
|
-
current = await place(deps, access, current, result.text);
|
|
182
152
|
const thread = await (0, internal_1.loadThread)(deps, run.threadId);
|
|
183
153
|
if (thread.state === "new" || thread.state === "drafted")
|
|
184
154
|
await moveToReview(deps, thread, current);
|
|
@@ -203,7 +173,7 @@ async function regenerateDraft(deps, threadId) {
|
|
|
203
173
|
await startRun(deps, run, message);
|
|
204
174
|
return run;
|
|
205
175
|
}
|
|
206
|
-
async function writeStaffReply(deps,
|
|
176
|
+
async function writeStaffReply(deps, thread, text) {
|
|
207
177
|
const inboundId = thread.latestInboundId;
|
|
208
178
|
if (!inboundId)
|
|
209
179
|
throw new errors_1.DraftNotReadyError(thread.id, "no inbound message");
|
|
@@ -217,65 +187,27 @@ async function writeStaffReply(deps, access, thread, text) {
|
|
|
217
187
|
});
|
|
218
188
|
if (run.status !== "succeeded")
|
|
219
189
|
throw new errors_1.DraftNotReadyError(thread.id, "a draft is being written");
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
current = await place(deps, access, current, text);
|
|
223
|
-
const reviewReason = current.placementError ? "placement_failed" : "staff_edited";
|
|
224
|
-
await (0, internal_1.applyEvents)(deps, thread, ["redraft", "needs_review"], { reviewReason });
|
|
190
|
+
const current = await deps.store.updateDraftRun(run.idempotencyKey, { commentText: text });
|
|
191
|
+
await (0, internal_1.applyEvents)(deps, thread, ["redraft", "needs_review"], { reviewReason: "staff_edited" });
|
|
225
192
|
return current;
|
|
226
193
|
}
|
|
227
194
|
async function editDraft(deps, input) {
|
|
228
|
-
|
|
229
|
-
if (controls.paused)
|
|
195
|
+
if ((await (0, controls_1.loadControls)(deps)).paused)
|
|
230
196
|
return (0, controls_1.paused)();
|
|
231
197
|
const thread = await (0, internal_1.loadThread)(deps, input.threadId);
|
|
232
|
-
|
|
198
|
+
(0, controls_1.assertInScope)(deps.config, thread.mailbox);
|
|
233
199
|
if (input.text.trim() === "")
|
|
234
200
|
throw new errors_1.EmptyReplyError(thread.id);
|
|
235
201
|
const run = currentReplyRun(deps, thread, await deps.store.getLatestDraftRunForThread(thread.id));
|
|
236
202
|
if (!run)
|
|
237
|
-
return writeStaffReply(deps,
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
throw new errors_1.DraftNotReadyError(thread.id, "no draft text to edit");
|
|
241
|
-
(0, states_1.transition)(thread.state, "redraft");
|
|
242
|
-
const updated = await deps.store.updateDraftRun(run.idempotencyKey, { commentText: input.text });
|
|
243
|
-
await (0, internal_1.applyEvents)(deps, thread, ["redraft", "needs_review"], { reviewReason: "staff_edited" });
|
|
244
|
-
return updated;
|
|
245
|
-
}
|
|
246
|
-
if (!run?.placedDraftId || run.placedBodyHtml === null) {
|
|
247
|
-
throw new errors_1.DraftNotReadyError(thread.id, "no placed draft to edit");
|
|
248
|
-
}
|
|
203
|
+
return writeStaffReply(deps, thread, input.text);
|
|
204
|
+
if (run.status !== "succeeded")
|
|
205
|
+
throw new errors_1.DraftNotReadyError(thread.id, "no draft text to edit");
|
|
249
206
|
(0, states_1.transition)(thread.state, "redraft");
|
|
250
|
-
const
|
|
251
|
-
await access.gateway().updateDraftBody(run.placedDraftId, { contentType: "html", content: body });
|
|
252
|
-
const updated = await deps.store.updateDraftRun(run.idempotencyKey, {
|
|
253
|
-
placedBodyHtml: body,
|
|
254
|
-
commentText: input.text,
|
|
255
|
-
});
|
|
207
|
+
const updated = await deps.store.updateDraftRun(run.idempotencyKey, { commentText: input.text });
|
|
256
208
|
await (0, internal_1.applyEvents)(deps, thread, ["redraft", "needs_review"], { reviewReason: "staff_edited" });
|
|
257
209
|
return updated;
|
|
258
210
|
}
|
|
259
|
-
async function retryPlacement(deps, threadId) {
|
|
260
|
-
const controls = await (0, controls_1.loadControls)(deps);
|
|
261
|
-
if (controls.paused)
|
|
262
|
-
return (0, controls_1.paused)();
|
|
263
|
-
const thread = await (0, internal_1.loadThread)(deps, threadId);
|
|
264
|
-
const access = (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox);
|
|
265
|
-
if (access.mode !== "drafts_in_outlook")
|
|
266
|
-
return { status: "write_blocked", mode: access.mode };
|
|
267
|
-
const run = await deps.store.getLatestDraftRunForThread(thread.id);
|
|
268
|
-
const text = run?.status === "succeeded" ? (run.commentText ?? run.text) : null;
|
|
269
|
-
if (!run || text === null)
|
|
270
|
-
throw new errors_1.DraftNotReadyError(thread.id, "no stored draft text to place");
|
|
271
|
-
if (run.placedDraftId)
|
|
272
|
-
return run;
|
|
273
|
-
const placed = await place(deps, access, run, text);
|
|
274
|
-
if (!placed.placementError && thread.state === "review") {
|
|
275
|
-
await (0, internal_1.applyEvents)(deps, thread, ["redraft", "needs_review"], { reviewReason: "policy" });
|
|
276
|
-
}
|
|
277
|
-
return placed;
|
|
278
|
-
}
|
|
279
211
|
async function sweepStuckDrafts(deps, mailbox) {
|
|
280
212
|
if ((await (0, controls_1.loadControls)(deps)).paused)
|
|
281
213
|
return (0, controls_1.paused)();
|
package/dist/errors.d.ts
CHANGED
|
@@ -29,9 +29,6 @@ export declare class EmptyReplyError extends HelpdeskError {
|
|
|
29
29
|
export declare class StaleDraftError extends HelpdeskError {
|
|
30
30
|
constructor(threadId: string);
|
|
31
31
|
}
|
|
32
|
-
export declare class QuoteNotFoundError extends HelpdeskError {
|
|
33
|
-
constructor();
|
|
34
|
-
}
|
|
35
32
|
export declare class MailboxNotInScopeError extends HelpdeskError {
|
|
36
33
|
readonly mailbox: string;
|
|
37
34
|
constructor(mailbox: string);
|
package/dist/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ThreadNotFoundError = exports.ReplyAttachmentNotFoundError = exports.AttachmentLimitError = exports.AttachmentsUnavailableError = exports.SendPendingError = exports.MailboxWriteBlockedError = exports.MailboxNotInScopeError = exports.
|
|
3
|
+
exports.ThreadNotFoundError = exports.ReplyAttachmentNotFoundError = exports.AttachmentLimitError = exports.AttachmentsUnavailableError = exports.SendPendingError = exports.MailboxWriteBlockedError = exports.MailboxNotInScopeError = exports.StaleDraftError = exports.EmptyReplyError = exports.DraftNotReadyError = exports.DeltaLinkExpiredError = exports.InvalidDeltaPageError = exports.MailboxNotConnectedError = exports.StaleThreadError = exports.InvalidTransitionError = exports.HelpdeskError = void 0;
|
|
4
4
|
exports.isDeltaLinkExpired = isDeltaLinkExpired;
|
|
5
5
|
exports.isTransientMailError = isTransientMailError;
|
|
6
6
|
class HelpdeskError extends Error {
|
|
@@ -61,12 +61,6 @@ class StaleDraftError extends HelpdeskError {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
exports.StaleDraftError = StaleDraftError;
|
|
64
|
-
class QuoteNotFoundError extends HelpdeskError {
|
|
65
|
-
constructor() {
|
|
66
|
-
super("quote_not_found", "Cannot find the quoted history in the draft body; refusing to edit");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
exports.QuoteNotFoundError = QuoteNotFoundError;
|
|
70
64
|
class MailboxNotInScopeError extends HelpdeskError {
|
|
71
65
|
constructor(mailbox) {
|
|
72
66
|
super("mailbox_not_in_scope", `Mailbox ${mailbox} is not one of this helpdesk's mailboxes`);
|
package/dist/inbox/handlers.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MAX_ATTACHMENT_DOWNLOAD_BYTES = void 0;
|
|
4
4
|
exports.contentDisposition = contentDisposition;
|
|
5
5
|
exports.createHelpdeskHandlers = createHelpdeskHandlers;
|
|
6
|
-
const
|
|
6
|
+
const bounce_1 = require("../bounce");
|
|
7
7
|
const controls_1 = require("../controls");
|
|
8
8
|
const drafting_1 = require("../drafting");
|
|
9
9
|
const errors_1 = require("../errors");
|
|
@@ -50,7 +50,6 @@ const DOMAIN_ERRORS = {
|
|
|
50
50
|
draft_not_ready: [409, "draft_not_ready"],
|
|
51
51
|
invalid_transition: [409, "invalid_transition"],
|
|
52
52
|
stale_thread: [409, "stale_thread"],
|
|
53
|
-
quote_not_found: [422, "quote_not_found"],
|
|
54
53
|
send_in_progress: [409, "send_in_progress"],
|
|
55
54
|
send_outcome_unknown: [409, "send_outcome_unknown"],
|
|
56
55
|
empty_reply: [422, "empty_reply"],
|
|
@@ -87,25 +86,27 @@ function summary(thread, assignee) {
|
|
|
87
86
|
assignee,
|
|
88
87
|
};
|
|
89
88
|
}
|
|
89
|
+
function bounceView(text) {
|
|
90
|
+
const report = (0, bounce_1.readBounceReport)(text);
|
|
91
|
+
return { ...report, reason: (0, messages_1.bounceReasonText)(report.statusCode, report.recipient) };
|
|
92
|
+
}
|
|
90
93
|
async function latestIntent(deps, run) {
|
|
91
94
|
if (!run)
|
|
92
95
|
return null;
|
|
93
|
-
|
|
94
|
-
const found = await Promise.all([perRun, perDraft].map((id) => (id ? deps.store.getSendIntent(id) : null)));
|
|
95
|
-
const intents = found.filter((i) => i !== null);
|
|
96
|
-
return intents.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0] ?? null;
|
|
96
|
+
return deps.store.getSendIntent((0, sending_1.sendIntentId)(run));
|
|
97
97
|
}
|
|
98
98
|
function draftView(thread, run) {
|
|
99
99
|
if (!run)
|
|
100
100
|
return null;
|
|
101
|
+
const text = run.status === "succeeded" ? (run.commentText ?? run.text) : null;
|
|
101
102
|
return {
|
|
102
103
|
generation: run.generation,
|
|
103
104
|
status: run.status,
|
|
104
|
-
text
|
|
105
|
+
text,
|
|
105
106
|
confidence: run.confidence,
|
|
106
107
|
category: run.category,
|
|
107
108
|
stale: thread.latestInboundId !== run.inboundMessageId,
|
|
108
|
-
|
|
109
|
+
fromDrafter: text !== null && run.text !== null && text === run.text,
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
function allowedActions(thread, run, intent, paused, mode, hasDrafter, canAttach = false) {
|
|
@@ -113,22 +114,16 @@ function allowedActions(thread, run, intent, paused, mode, hasDrafter, canAttach
|
|
|
113
114
|
const actions = [];
|
|
114
115
|
const hasText = run?.status === "succeeded";
|
|
115
116
|
const fresh = run === null && thread.latestInboundId !== null;
|
|
116
|
-
const placed = Boolean(run?.placedDraftId) && run?.placedBodyHtml !== null;
|
|
117
117
|
const pending = intent !== null && ["sending", "unknown", "sent"].includes(intent.status);
|
|
118
118
|
const sendUnderWay = intent !== null && ["sending", "unknown"].includes(intent.status);
|
|
119
119
|
const bounced = s === "review" && thread.reviewReason === "delivery_failed";
|
|
120
120
|
if (!paused) {
|
|
121
|
-
if (!bounced && (0, states_1.canTransition)(s, "redraft") && (
|
|
121
|
+
if (!bounced && (0, states_1.canTransition)(s, "redraft") && (hasText || fresh))
|
|
122
122
|
actions.push("edit");
|
|
123
|
-
}
|
|
124
123
|
if (!bounced && (0, states_1.canTransition)(s, "redraft") && thread.latestInboundId && hasDrafter)
|
|
125
124
|
actions.push("regenerate");
|
|
126
|
-
|
|
127
|
-
if (!bounced && (0, states_1.canTransition)(s, "approve") && (sendable || (fresh && mode !== "read_only")) && !pending)
|
|
125
|
+
if (!bounced && (0, states_1.canTransition)(s, "approve") && mode !== "read_only" && (hasText || fresh) && !pending)
|
|
128
126
|
actions.push("send");
|
|
129
|
-
if (!bounced && (0, states_1.canTransition)(s, "approve") && mode === "drafts_in_outlook" && hasText && !run?.placedDraftId) {
|
|
130
|
-
actions.push("retry_placement");
|
|
131
|
-
}
|
|
132
127
|
if ((0, states_1.canTransition)(s, "approve"))
|
|
133
128
|
actions.push("replied_externally");
|
|
134
129
|
if ((0, states_1.canTransition)(s, "unignore"))
|
|
@@ -209,10 +204,14 @@ function createHelpdeskHandlers(config) {
|
|
|
209
204
|
subject: m.subject,
|
|
210
205
|
receivedAt: m.receivedAt,
|
|
211
206
|
noise: m.noise.action === "ignore",
|
|
207
|
+
bounce: m.direction === "inbound" && m.noise.reason === "bounce",
|
|
212
208
|
removed: m.removedAt !== null,
|
|
209
|
+
outbound: m.direction === "outbound",
|
|
213
210
|
})),
|
|
214
211
|
draft: draftView(thread, run ?? (latestRun?.status === "failed" ? latestRun : null)),
|
|
215
|
-
send: intent
|
|
212
|
+
send: intent
|
|
213
|
+
? { status: intent.status, at: intent.updatedAt, messageId: intent.status === "sent" ? intent.draftId : null }
|
|
214
|
+
: null,
|
|
216
215
|
replyAttachments: await replyAttachmentViews(thread.id, intent),
|
|
217
216
|
actions: allowedActions(thread, run, intent, controls.paused, mode, Boolean(deps.drafter), canAttach(thread.mailbox)),
|
|
218
217
|
};
|
|
@@ -290,10 +289,10 @@ function createHelpdeskHandlers(config) {
|
|
|
290
289
|
throw new RouteError(423, "paused");
|
|
291
290
|
const thread = await loadThread(threadId);
|
|
292
291
|
const messages = await inbox.listMessages(thread.id);
|
|
293
|
-
|
|
292
|
+
const stored = messages.find((m) => m.id === messageId && m.threadId === thread.id);
|
|
293
|
+
if (!stored)
|
|
294
294
|
throw new RouteError(404, "message_not_found");
|
|
295
|
-
}
|
|
296
|
-
return (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox).gateway();
|
|
295
|
+
return { gateway: (0, controls_1.mailboxAccess)(deps, controls, thread.mailbox).gateway(), stored };
|
|
297
296
|
}
|
|
298
297
|
function attachmentView(threadId, messageId, a) {
|
|
299
298
|
const unavailable = a.kind !== "file" ? a.kind : a.size > exports.MAX_ATTACHMENT_DOWNLOAD_BYTES ? "too_large" : null;
|
|
@@ -301,19 +300,20 @@ function createHelpdeskHandlers(config) {
|
|
|
301
300
|
return { id: a.id, name: a.name, size: a.size, kind: a.kind, href: unavailable ? null : `${basePath}/threads/${path}`, unavailable };
|
|
302
301
|
}
|
|
303
302
|
async function messageBody({ threadId, messageId }) {
|
|
304
|
-
const gateway = await ticketMessage(threadId, messageId);
|
|
303
|
+
const { gateway, stored } = await ticketMessage(threadId, messageId);
|
|
305
304
|
const message = await gateway.getMessage(messageId, { bodyType: "text" });
|
|
306
305
|
if (!message)
|
|
307
306
|
throw new RouteError(404, "message_unavailable");
|
|
308
307
|
const { contentType, content } = message.body;
|
|
309
308
|
const text = contentType === "html" ? (0, plain_text_1.htmlToPlainText)(content) : content.replace(/\r\n?/g, "\n").trim();
|
|
310
|
-
const
|
|
309
|
+
const isBounce = stored.direction === "inbound" && stored.noise.reason === "bounce";
|
|
310
|
+
const listed = !isBounce && gateway.listAttachments && message.hasAttachments !== false ? await gateway.listAttachments(messageId) : [];
|
|
311
311
|
const attachments = listed.filter((a) => !a.isInline).map((a) => attachmentView(threadId, messageId, a));
|
|
312
|
-
const body = { id: messageId, text, attachments };
|
|
312
|
+
const body = { id: messageId, text, attachments, bounce: isBounce ? bounceView(text) : null };
|
|
313
313
|
return json(200, body);
|
|
314
314
|
}
|
|
315
315
|
async function attachment({ threadId, messageId, attachmentId }) {
|
|
316
|
-
const gateway = await ticketMessage(threadId, messageId);
|
|
316
|
+
const { gateway } = await ticketMessage(threadId, messageId);
|
|
317
317
|
if (!gateway.listAttachments || !gateway.downloadAttachment)
|
|
318
318
|
throw new RouteError(404, "attachment_not_found");
|
|
319
319
|
const found = (await gateway.listAttachments(messageId)).find((a) => a.id === attachmentId);
|
|
@@ -343,11 +343,11 @@ function createHelpdeskHandlers(config) {
|
|
|
343
343
|
if (text === null || text.trim() === "" || text.length > MAX_DRAFT_CHARS)
|
|
344
344
|
throw new RouteError(400, "invalid_input");
|
|
345
345
|
const outcome = await (0, drafting_1.editDraft)(deps, { threadId: ctx.threadId, text });
|
|
346
|
-
return refused(outcome) ?? result(ctx, "
|
|
346
|
+
return refused(outcome) ?? result(ctx, "Opgeslagen.");
|
|
347
347
|
}
|
|
348
348
|
async function regenerate(ctx) {
|
|
349
349
|
const outcome = await (0, drafting_1.regenerateDraft)(deps, ctx.threadId);
|
|
350
|
-
return refused(outcome) ?? result(ctx, "Er wordt een nieuw
|
|
350
|
+
return refused(outcome) ?? result(ctx, "Er wordt een nieuw voorstel geschreven.");
|
|
351
351
|
}
|
|
352
352
|
async function send(ctx) {
|
|
353
353
|
const body = await readJson(ctx.request);
|
|
@@ -363,14 +363,6 @@ function createHelpdeskHandlers(config) {
|
|
|
363
363
|
const status = outcome.status;
|
|
364
364
|
return result(ctx, SEND_MESSAGES[status], status, status === "in_progress" ? 202 : 200);
|
|
365
365
|
}
|
|
366
|
-
async function retry(ctx) {
|
|
367
|
-
const outcome = await (0, drafting_1.retryPlacement)(deps, ctx.threadId);
|
|
368
|
-
const blocked = refused(outcome);
|
|
369
|
-
if (blocked)
|
|
370
|
-
return blocked;
|
|
371
|
-
const placed = outcome.placementError === null;
|
|
372
|
-
return result(ctx, placed ? "Het concept staat nu in Outlook." : "Het concept kon nog steeds niet in Outlook worden gezet.");
|
|
373
|
-
}
|
|
374
366
|
async function assign(ctx) {
|
|
375
367
|
await loadThread(ctx.threadId);
|
|
376
368
|
await inbox.setAssignee(ctx.threadId, person(ctx.staff));
|
|
@@ -393,7 +385,7 @@ function createHelpdeskHandlers(config) {
|
|
|
393
385
|
}
|
|
394
386
|
async function unignore(ctx) {
|
|
395
387
|
const outcome = await (0, drafting_1.unignoreThread)(deps, ctx.threadId);
|
|
396
|
-
const next = deps.drafter ? "Er wordt een
|
|
388
|
+
const next = deps.drafter ? "Er wordt een voorstel geschreven." : "Schrijf zelf het antwoord.";
|
|
397
389
|
return refused(outcome) ?? result(ctx, `Ticket teruggezet. ${next}`);
|
|
398
390
|
}
|
|
399
391
|
async function repliedExternally(ctx) {
|
|
@@ -454,7 +446,6 @@ function createHelpdeskHandlers(config) {
|
|
|
454
446
|
draft: { PUT: saveDraft },
|
|
455
447
|
regenerate: { POST: regenerate },
|
|
456
448
|
send: { POST: send },
|
|
457
|
-
"retry-placement": { POST: retry },
|
|
458
449
|
assign: { POST: assign, DELETE: unassign },
|
|
459
450
|
waiting: { POST: waiting },
|
|
460
451
|
close: { POST: close },
|
package/dist/inbox/messages.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReviewReason, TicketState } from "../types";
|
|
2
2
|
export declare const HELPDESK_BASE_PATH = "/api/helpdesk";
|
|
3
3
|
export declare const INBOX_ERROR_MESSAGES: {
|
|
4
4
|
readonly not_production: "De helpdesk werkt alleen in de productieomgeving van de app.";
|
|
@@ -18,11 +18,10 @@ export declare const INBOX_ERROR_MESSAGES: {
|
|
|
18
18
|
readonly paused: "De helpdesk staat op pauze. Er wordt niets opgehaald of verstuurd tot de pauze voorbij is.";
|
|
19
19
|
readonly write_blocked: "Deze mailbox staat op alleen lezen. Antwoorden kan pas als de mailbox daarvoor is vrijgegeven.";
|
|
20
20
|
readonly mailbox_not_in_scope: "Deze mailbox hoort niet bij de helpdesk.";
|
|
21
|
-
readonly stale_draft: "Er is een nieuwere mail van de klant binnengekomen na dit
|
|
22
|
-
readonly draft_not_ready: "Er is nog geen
|
|
21
|
+
readonly stale_draft: "Er is een nieuwere mail van de klant binnengekomen na dit antwoord. Lees die eerst, of verstuur toch.";
|
|
22
|
+
readonly draft_not_ready: "Er is nog geen antwoord dat verstuurd of aangepast kan worden.";
|
|
23
23
|
readonly invalid_transition: "Dit kan niet in de huidige status van het ticket. Vernieuw de pagina.";
|
|
24
24
|
readonly stale_thread: "Het ticket is net gewijzigd. Vernieuw de pagina en probeer het opnieuw.";
|
|
25
|
-
readonly quote_not_found: "De eerdere mail in het concept is niet terug te vinden, daarom is de wijziging niet opgeslagen. Pas het concept aan in Outlook.";
|
|
26
25
|
readonly empty_reply: "Het antwoord is leeg. Schrijf eerst een antwoord; alleen de eerdere mail wordt nooit verstuurd.";
|
|
27
26
|
readonly send_in_progress: "Er wordt net een antwoord verstuurd. Vernieuw zo de pagina en kijk of het nog nodig is.";
|
|
28
27
|
readonly send_outcome_unknown: "Het is niet zeker of het vorige antwoord is verstuurd. Geef eerst aan wat je in Outlook bij Verzonden items ziet.";
|
|
@@ -32,4 +31,4 @@ export declare const INBOX_ERROR_MESSAGES: {
|
|
|
32
31
|
export type InboxErrorCode = keyof typeof INBOX_ERROR_MESSAGES;
|
|
33
32
|
export declare const STATE_LABELS: Record<TicketState, string>;
|
|
34
33
|
export declare const REVIEW_REASON_LABELS: Record<ReviewReason, string>;
|
|
35
|
-
export declare
|
|
34
|
+
export declare function bounceReasonText(statusCode: string | null, recipient: string | null): string;
|
package/dist/inbox/messages.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.REVIEW_REASON_LABELS = exports.STATE_LABELS = exports.INBOX_ERROR_MESSAGES = exports.HELPDESK_BASE_PATH = void 0;
|
|
4
|
+
exports.bounceReasonText = bounceReasonText;
|
|
4
5
|
exports.HELPDESK_BASE_PATH = "/api/helpdesk";
|
|
5
6
|
exports.INBOX_ERROR_MESSAGES = {
|
|
6
7
|
not_production: "De helpdesk werkt alleen in de productieomgeving van de app.",
|
|
@@ -20,11 +21,10 @@ exports.INBOX_ERROR_MESSAGES = {
|
|
|
20
21
|
paused: "De helpdesk staat op pauze. Er wordt niets opgehaald of verstuurd tot de pauze voorbij is.",
|
|
21
22
|
write_blocked: "Deze mailbox staat op alleen lezen. Antwoorden kan pas als de mailbox daarvoor is vrijgegeven.",
|
|
22
23
|
mailbox_not_in_scope: "Deze mailbox hoort niet bij de helpdesk.",
|
|
23
|
-
stale_draft: "Er is een nieuwere mail van de klant binnengekomen na dit
|
|
24
|
-
draft_not_ready: "Er is nog geen
|
|
24
|
+
stale_draft: "Er is een nieuwere mail van de klant binnengekomen na dit antwoord. Lees die eerst, of verstuur toch.",
|
|
25
|
+
draft_not_ready: "Er is nog geen antwoord dat verstuurd of aangepast kan worden.",
|
|
25
26
|
invalid_transition: "Dit kan niet in de huidige status van het ticket. Vernieuw de pagina.",
|
|
26
27
|
stale_thread: "Het ticket is net gewijzigd. Vernieuw de pagina en probeer het opnieuw.",
|
|
27
|
-
quote_not_found: "De eerdere mail in het concept is niet terug te vinden, daarom is de wijziging niet opgeslagen. Pas het concept aan in Outlook.",
|
|
28
28
|
empty_reply: "Het antwoord is leeg. Schrijf eerst een antwoord; alleen de eerdere mail wordt nooit verstuurd.",
|
|
29
29
|
send_in_progress: "Er wordt net een antwoord verstuurd. Vernieuw zo de pagina en kijk of het nog nodig is.",
|
|
30
30
|
send_outcome_unknown: "Het is niet zeker of het vorige antwoord is verstuurd. Geef eerst aan wat je in Outlook bij Verzonden items ziet.",
|
|
@@ -34,24 +34,33 @@ exports.INBOX_ERROR_MESSAGES = {
|
|
|
34
34
|
exports.STATE_LABELS = {
|
|
35
35
|
new: "Nieuw",
|
|
36
36
|
ignored: "Genegeerd",
|
|
37
|
-
drafted: "
|
|
37
|
+
drafted: "Voorstel wordt geschreven",
|
|
38
38
|
review: "Te beoordelen",
|
|
39
39
|
sent: "Verstuurd",
|
|
40
40
|
waiting: "Wacht op klant",
|
|
41
41
|
closed: "Gesloten",
|
|
42
42
|
};
|
|
43
43
|
exports.REVIEW_REASON_LABELS = {
|
|
44
|
-
low_confidence: "
|
|
45
|
-
policy: "
|
|
46
|
-
stale_draft: "Nieuwe mail na
|
|
47
|
-
drafter_failed: "
|
|
48
|
-
placement_failed: "Niet in Outlook gezet",
|
|
44
|
+
low_confidence: "Voorstel twijfelachtig",
|
|
45
|
+
policy: "Voorstel klaar",
|
|
46
|
+
stale_draft: "Nieuwe mail na voorstel",
|
|
47
|
+
drafter_failed: "Voorstel mislukt",
|
|
49
48
|
staff_edited: "Aangepast",
|
|
50
49
|
no_draft: "Zelf antwoorden",
|
|
51
50
|
delivery_failed: "Niet afgeleverd",
|
|
52
51
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
const BOUNCE_REASONS = {
|
|
53
|
+
"5.1.1": "Dit adres bestaat niet.",
|
|
54
|
+
"5.1.10": "Dit adres bestaat niet.",
|
|
55
|
+
"5.2.2": "De mailbox van de ontvanger is vol.",
|
|
56
|
+
"5.7.708": "Microsoft blokkeert mail vanaf deze mailbox naar adressen buiten jullie organisatie.",
|
|
57
|
+
"5.7.750": "Microsoft blokkeert mail vanaf deze mailbox naar adressen buiten jullie organisatie.",
|
|
58
|
+
"5.7.1": "De ontvangende server weigert mail van deze afzender.",
|
|
57
59
|
};
|
|
60
|
+
function bounceReasonText(statusCode, recipient) {
|
|
61
|
+
const reason = statusCode ? BOUNCE_REASONS[statusCode] : undefined;
|
|
62
|
+
if (!recipient)
|
|
63
|
+
return reason ?? "De ontvangende server weigerde het bericht.";
|
|
64
|
+
const prefix = `De mailserver van ${recipient} weigerde het bericht.`;
|
|
65
|
+
return reason ? `${prefix} ${reason}` : prefix;
|
|
66
|
+
}
|
package/dist/inbox/types.d.ts
CHANGED
|
@@ -43,12 +43,21 @@ export interface InboxMessageView {
|
|
|
43
43
|
subject: string;
|
|
44
44
|
receivedAt: string;
|
|
45
45
|
noise: boolean;
|
|
46
|
+
bounce: boolean;
|
|
46
47
|
removed: boolean;
|
|
48
|
+
outbound: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface InboxBounceView {
|
|
51
|
+
recipient: string | null;
|
|
52
|
+
statusCode: string | null;
|
|
53
|
+
reason: string;
|
|
54
|
+
diagnostic: string;
|
|
47
55
|
}
|
|
48
56
|
export interface InboxMessageBody {
|
|
49
57
|
id: string;
|
|
50
58
|
text: string;
|
|
51
59
|
attachments: InboxAttachmentView[];
|
|
60
|
+
bounce: InboxBounceView | null;
|
|
52
61
|
}
|
|
53
62
|
export interface InboxReplyAttachmentView {
|
|
54
63
|
id: string;
|
|
@@ -76,9 +85,9 @@ export interface InboxDraftView {
|
|
|
76
85
|
confidence: number | null;
|
|
77
86
|
category: string | null;
|
|
78
87
|
stale: boolean;
|
|
79
|
-
|
|
88
|
+
fromDrafter: boolean;
|
|
80
89
|
}
|
|
81
|
-
export type InboxAction = "edit" | "regenerate" | "send" | "
|
|
90
|
+
export type InboxAction = "edit" | "regenerate" | "send" | "replied_externally" | "mark_waiting" | "close" | "unignore" | "dismiss" | "resolve_send" | "attach";
|
|
82
91
|
export interface InboxThreadDetail {
|
|
83
92
|
viewer: InboxPerson;
|
|
84
93
|
thread: InboxThreadSummary & {
|
|
@@ -96,6 +105,7 @@ export interface InboxThreadDetail {
|
|
|
96
105
|
send: {
|
|
97
106
|
status: SendStatus;
|
|
98
107
|
at: string;
|
|
108
|
+
messageId: string | null;
|
|
99
109
|
} | null;
|
|
100
110
|
replyAttachments: InboxReplyAttachmentView[];
|
|
101
111
|
actions: InboxAction[];
|