@remit/mailbox-service 0.0.123 → 1.0.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/body-sync.d.ts +7 -5
- package/dist/body-sync.js +56 -45
- package/dist/outbox-queue.d.ts +12 -1
- package/dist/outbox-queue.js +32 -0
- package/dist/test-helpers/test-store.d.ts +2 -0
- package/package.json +1 -1
package/dist/body-sync.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type IAddressRepository, type ICalendarStore, type ICalendarSuggestionRepository, type IEnvelopeRepository, type IFilterRepository, type IMailboxSpecialUseRepository, type IMessageRepository, type IThreadMessageRepository } from "@remit/data-ports";
|
|
1
|
+
import { type IAccountRepository, type IAddressRepository, type ICalendarStore, type ICalendarSuggestionRepository, type IEnvelopeRepository, type IFilterRepository, type IMailboxSpecialUseRepository, type IMessageRepository, type IThreadMessageRepository } from "@remit/data-ports";
|
|
2
2
|
import { type ParsedBody, type StorageService } from "@remit/storage-service";
|
|
3
3
|
import { type ParsedMail } from "mailparser";
|
|
4
4
|
import { type FilterConfig } from "./filters/pipeline.js";
|
|
@@ -116,6 +116,8 @@ export interface CalendarSuggestionConfig {
|
|
|
116
116
|
calendarSuggestionService: ICalendarSuggestionRepository;
|
|
117
117
|
calendarStore: ICalendarStore;
|
|
118
118
|
filterService: Pick<IFilterRepository, "listByAccountAndState">;
|
|
119
|
+
/** The account config's addresses: a guest's REPLY lands only on an event the reader organizes. */
|
|
120
|
+
accountService: Pick<IAccountRepository, "listAllByAccountConfig">;
|
|
119
121
|
}
|
|
120
122
|
export declare class BodySyncService {
|
|
121
123
|
private messageService;
|
|
@@ -236,10 +238,10 @@ export declare class BodySyncService {
|
|
|
236
238
|
* `syncBodies(..., force: true)`) reach this with `bodyStorageKey` already
|
|
237
239
|
* set, and re-offering there would resurrect a card the user dismissed.
|
|
238
240
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
241
|
+
* Nothing here is caught. A failure is a fault in this code or its store,
|
|
242
|
+
* so it fails the body-sync job loudly and the job takes the queue's own
|
|
243
|
+
* dead-letter path. A part the sender's client wrote badly is not a failure:
|
|
244
|
+
* it is read as `Unreadable` and offered nothing.
|
|
243
245
|
*/
|
|
244
246
|
private deriveCalendarSuggestions;
|
|
245
247
|
private deriveSenderUnsubscribed;
|
package/dist/body-sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inspect } from "node:util";
|
|
2
|
-
import { provisionDefaultCalendar, recordCalendarSuggestion, } from "@remit/calendar-service";
|
|
2
|
+
import { provisionDefaultCalendar, recordCalendarReply, recordCalendarSuggestion, } from "@remit/calendar-service";
|
|
3
3
|
import { isSenderMuted, } from "@remit/data-ports";
|
|
4
4
|
import { MessageGoneFromServerError, MessagePlacementUnsettledError, NotFoundError, } from "@remit/data-ports/errors";
|
|
5
5
|
import { deriveAddressId, deriveBodyPartId } from "@remit/data-ports/id";
|
|
@@ -822,10 +822,10 @@ export class BodySyncService {
|
|
|
822
822
|
* `syncBodies(..., force: true)`) reach this with `bodyStorageKey` already
|
|
823
823
|
* set, and re-offering there would resurrect a card the user dismissed.
|
|
824
824
|
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
825
|
+
* Nothing here is caught. A failure is a fault in this code or its store,
|
|
826
|
+
* so it fails the body-sync job loudly and the job takes the queue's own
|
|
827
|
+
* dead-letter path. A part the sender's client wrote badly is not a failure:
|
|
828
|
+
* it is read as `Unreadable` and offered nothing.
|
|
829
829
|
*/
|
|
830
830
|
async deriveCalendarSuggestions(lookups, messageId, accountConfigId, parsed, storedBodyKey) {
|
|
831
831
|
if (!this.calendarConfig)
|
|
@@ -835,49 +835,60 @@ export class BodySyncService {
|
|
|
835
835
|
const parts = calendarParts(parsed);
|
|
836
836
|
if (parts.length === 0)
|
|
837
837
|
return;
|
|
838
|
-
const { calendarStore, calendarSuggestionService } = this.calendarConfig;
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
838
|
+
const { calendarStore, calendarSuggestionService, accountService } = this.calendarConfig;
|
|
839
|
+
// A guest answering the reader's own invitation moves their answer on the
|
|
840
|
+
// organizer's copy, whoever the reader muted. Local only: nothing goes
|
|
841
|
+
// back to the guest. A reply proposes nothing to add, so it is never
|
|
842
|
+
// offered as a card.
|
|
843
|
+
const selfAddresses = (await accountService.listAllByAccountConfig(accountConfigId)).map((account) => account.email);
|
|
844
|
+
const offered = [];
|
|
845
|
+
for (const part of parts) {
|
|
846
|
+
const replied = await recordCalendarReply(calendarStore, {
|
|
847
|
+
accountConfigId,
|
|
848
|
+
icalData: part.icalData,
|
|
849
|
+
selfAddresses,
|
|
850
|
+
});
|
|
851
|
+
if (replied === "NotAReply")
|
|
852
|
+
offered.push(part);
|
|
853
|
+
}
|
|
854
|
+
if (offered.length === 0)
|
|
855
|
+
return;
|
|
856
|
+
// `dismiss{muteSender:true}` wrote a standing rule naming this
|
|
857
|
+
// sender. Honouring it here is what makes that button do anything:
|
|
858
|
+
// the index-time filter pipeline skips a rule with no label and no
|
|
859
|
+
// move, so this is the only reader such a rule has.
|
|
860
|
+
const sender = extractPrimaryFromEmail(parsed);
|
|
861
|
+
if (sender) {
|
|
862
|
+
const active = await lookups.activeFilters();
|
|
863
|
+
if (isSenderMuted(active, sender)) {
|
|
864
|
+
this.log.debug?.({ messageId, sender }, "Sender is muted; offering no calendar suggestion");
|
|
865
|
+
return;
|
|
852
866
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
867
|
+
}
|
|
868
|
+
// Provisioned rather than looked up, through the one function every
|
|
869
|
+
// caller provisions with. A missing collection has no timezone, and
|
|
870
|
+
// a floating DTSTART read in UTC puts the meeting hours from where
|
|
871
|
+
// the organizer meant it. Idempotent: the id is derived, so a second
|
|
872
|
+
// first use returns the collection the first one made.
|
|
873
|
+
const collection = await provisionDefaultCalendar(calendarStore, accountConfigId);
|
|
874
|
+
for (const part of offered) {
|
|
875
|
+
const recorded = await recordCalendarSuggestion(calendarSuggestionService, {
|
|
876
|
+
accountConfigId,
|
|
877
|
+
messageId,
|
|
878
|
+
bodyPartId: deriveBodyPartId(messageId, part.partPath),
|
|
879
|
+
source: part.source,
|
|
880
|
+
icalData: part.icalData,
|
|
881
|
+
timezone: collection.timezone,
|
|
882
|
+
});
|
|
883
|
+
if (!recorded.ok) {
|
|
884
|
+
this.log.info({
|
|
862
885
|
messageId,
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
});
|
|
868
|
-
if (!recorded.ok) {
|
|
869
|
-
this.log.info({
|
|
870
|
-
messageId,
|
|
871
|
-
partPath: part.partPath,
|
|
872
|
-
code: recorded.error.code,
|
|
873
|
-
reason: recorded.error.message,
|
|
874
|
-
}, "Message carries iCalendar this cannot read; no suggestion offered");
|
|
875
|
-
}
|
|
886
|
+
partPath: part.partPath,
|
|
887
|
+
code: recorded.error.code,
|
|
888
|
+
reason: recorded.error.message,
|
|
889
|
+
}, "Message carries iCalendar this cannot read; no suggestion offered");
|
|
876
890
|
}
|
|
877
|
-
}
|
|
878
|
-
.catch((error) => {
|
|
879
|
-
this.log.error?.({ messageId, accountConfigId, error: inspect(error) }, "Calendar suggestion derivation failed; body is stored without a card");
|
|
880
|
-
});
|
|
891
|
+
}
|
|
881
892
|
}
|
|
882
893
|
async deriveSenderUnsubscribed(accountConfigId, fromEmail) {
|
|
883
894
|
try {
|
package/dist/outbox-queue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IAccountRepository, IOutboxMessageRepository, OutboxMessageItem, QueueProducer } from "@remit/data-ports";
|
|
1
|
+
import type { CreateOutboxSchedulingPartInput, IAccountRepository, IOutboxMessageRepository, OutboxMessageItem, QueueProducer } from "@remit/data-ports";
|
|
2
2
|
import type { OutboxAttachmentService } from "./outbox-attachment.js";
|
|
3
3
|
export interface SendMessageEvent {
|
|
4
4
|
type: "SEND_MESSAGE";
|
|
@@ -54,6 +54,17 @@ export declare class OutboxQueueService {
|
|
|
54
54
|
private refuseWithoutMail;
|
|
55
55
|
send: (accountConfigId: string, outboxMessageId: string) => Promise<OutboxMessageItem>;
|
|
56
56
|
createAndSend: (input: CreateDraftInput) => Promise<OutboxMessageItem>;
|
|
57
|
+
/**
|
|
58
|
+
* Queues one calendar scheduling message — an invitation, a cancellation or
|
|
59
|
+
* a reply — with its `text/calendar` part.
|
|
60
|
+
*
|
|
61
|
+
* The row is the pending record and lands first. An enqueue that fails
|
|
62
|
+
* settles it `failed` and answers with that row rather than throwing: the
|
|
63
|
+
* calendar write it follows is already committed, and the person needs the
|
|
64
|
+
* failed send in front of them, with `send` as the retry, not an error that
|
|
65
|
+
* hides which guests were reached.
|
|
66
|
+
*/
|
|
67
|
+
createAndSendScheduled: (input: CreateDraftInput, schedulingPart: CreateOutboxSchedulingPartInput) => Promise<OutboxMessageItem>;
|
|
57
68
|
deleteDraft: (accountConfigId: string, outboxMessageId: string) => Promise<void>;
|
|
58
69
|
/**
|
|
59
70
|
* Take a row back out of `queued` after the enqueue reported a failure.
|
package/dist/outbox-queue.js
CHANGED
|
@@ -221,6 +221,38 @@ export class OutboxQueueService {
|
|
|
221
221
|
this.log.info({ outboxMessageId: outbox.outboxMessageId, accountId: input.accountId }, "Created and queued outbox message for sending");
|
|
222
222
|
return outbox;
|
|
223
223
|
};
|
|
224
|
+
/**
|
|
225
|
+
* Queues one calendar scheduling message — an invitation, a cancellation or
|
|
226
|
+
* a reply — with its `text/calendar` part.
|
|
227
|
+
*
|
|
228
|
+
* The row is the pending record and lands first. An enqueue that fails
|
|
229
|
+
* settles it `failed` and answers with that row rather than throwing: the
|
|
230
|
+
* calendar write it follows is already committed, and the person needs the
|
|
231
|
+
* failed send in front of them, with `send` as the retry, not an error that
|
|
232
|
+
* hides which guests were reached.
|
|
233
|
+
*/
|
|
234
|
+
createAndSendScheduled = async (input, schedulingPart) => {
|
|
235
|
+
const domain = extractDomain(input.fromAddress);
|
|
236
|
+
const outbox = await this.outboxMessageService.create({
|
|
237
|
+
accountId: input.accountId,
|
|
238
|
+
accountConfigId: input.accountConfigId,
|
|
239
|
+
fromAddress: input.fromAddress,
|
|
240
|
+
fromName: input.fromName,
|
|
241
|
+
toAddresses: input.toAddresses,
|
|
242
|
+
subject: input.subject,
|
|
243
|
+
textBody: input.textBody,
|
|
244
|
+
messageIdValue: generateMessageId(domain),
|
|
245
|
+
status: OutboxMessageStatus.queued,
|
|
246
|
+
}, schedulingPart);
|
|
247
|
+
const queued = await this.enqueueSend(input.accountId, outbox.outboxMessageId).then(() => true, async (error) => {
|
|
248
|
+
this.log.error({ outboxMessageId: outbox.outboxMessageId, error: String(error) }, "The queue refused a calendar scheduling message; settling it failed");
|
|
249
|
+
await this.settleUnqueued(input.accountConfigId, outbox.outboxMessageId, OutboxMessageStatus.failed);
|
|
250
|
+
return false;
|
|
251
|
+
});
|
|
252
|
+
if (queued)
|
|
253
|
+
return outbox;
|
|
254
|
+
return this.outboxMessageService.get(input.accountConfigId, outbox.outboxMessageId);
|
|
255
|
+
};
|
|
224
256
|
deleteDraft = async (accountConfigId, outboxMessageId) => {
|
|
225
257
|
const existing = await this.outboxMessageService.get(accountConfigId, outboxMessageId, "act");
|
|
226
258
|
// `unfiled` is discardable: the message was delivered but never landed in
|
|
@@ -16,6 +16,7 @@ export declare const openStore: () => Promise<{
|
|
|
16
16
|
message: import("@remit/drizzle-service").DrizzleMessageRepository;
|
|
17
17
|
messageFlag: import("@remit/drizzle-service").DrizzleMessageFlagRepository;
|
|
18
18
|
outboxMessage: import("@remit/drizzle-service").OutboxMessageRepo;
|
|
19
|
+
outboxSchedulingPart: import("@remit/drizzle-service").OutboxSchedulingPartRepo;
|
|
19
20
|
outboxAttachment: import("@remit/drizzle-service").OutboxAttachmentRepo;
|
|
20
21
|
threadMessage: import("@remit/drizzle-service").DrizzleThreadMessageRepository;
|
|
21
22
|
envelope: import("@remit/drizzle-service").DrizzleEnvelopeRepository;
|
|
@@ -34,6 +35,7 @@ export declare const openStore: () => Promise<{
|
|
|
34
35
|
calendarObject: import("@remit/drizzle-service").CalendarObjectRepo;
|
|
35
36
|
calendarEventIndex: import("@remit/drizzle-service").CalendarEventIndexRepo;
|
|
36
37
|
calendarFeedToken: import("@remit/drizzle-service").CalendarFeedTokenRepo;
|
|
38
|
+
calendarTask: import("@remit/drizzle-service").CalendarTaskRepo;
|
|
37
39
|
calendarSuggestion: import("@remit/drizzle-service").CalendarSuggestionRepo;
|
|
38
40
|
calendarStore: import("@remit/drizzle-service").DrizzleCalendarStore;
|
|
39
41
|
}>;
|