proton-mail-bridge-client 2.16.2 → 2.17.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 +149 -12
- package/dist/cli.d.ts +16 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +178 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-env.d.ts +2 -1
- package/dist/config-env.d.ts.map +1 -1
- package/dist/config-env.js +24 -0
- package/dist/config-env.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +180 -11
- package/dist/index.js.map +1 -1
- package/dist/services/account-manager.d.ts +7 -0
- package/dist/services/account-manager.d.ts.map +1 -1
- package/dist/services/account-manager.js +59 -0
- package/dist/services/account-manager.js.map +1 -1
- package/dist/services/audit-service.d.ts +20 -0
- package/dist/services/audit-service.d.ts.map +1 -1
- package/dist/services/audit-service.js +266 -9
- package/dist/services/audit-service.js.map +1 -1
- package/dist/services/autoresponder-service.d.ts +35 -6
- package/dist/services/autoresponder-service.d.ts.map +1 -1
- package/dist/services/autoresponder-service.js +223 -108
- package/dist/services/autoresponder-service.js.map +1 -1
- package/dist/services/brief-service.d.ts +7 -1
- package/dist/services/brief-service.d.ts.map +1 -1
- package/dist/services/brief-service.js +19 -5
- package/dist/services/brief-service.js.map +1 -1
- package/dist/services/category-service.d.ts +91 -0
- package/dist/services/category-service.d.ts.map +1 -0
- package/dist/services/category-service.js +320 -0
- package/dist/services/category-service.js.map +1 -0
- package/dist/services/commitment-service.d.ts.map +1 -1
- package/dist/services/commitment-service.js +9 -3
- package/dist/services/commitment-service.js.map +1 -1
- package/dist/services/contact-service.d.ts +60 -0
- package/dist/services/contact-service.d.ts.map +1 -0
- package/dist/services/contact-service.js +248 -0
- package/dist/services/contact-service.js.map +1 -0
- package/dist/services/delivery-queue-service.d.ts +4 -0
- package/dist/services/delivery-queue-service.d.ts.map +1 -1
- package/dist/services/delivery-queue-service.js +12 -2
- package/dist/services/delivery-queue-service.js.map +1 -1
- package/dist/services/draft-store-service.d.ts.map +1 -1
- package/dist/services/draft-store-service.js +3 -2
- package/dist/services/draft-store-service.js.map +1 -1
- package/dist/services/embedding-service.d.ts +68 -0
- package/dist/services/embedding-service.d.ts.map +1 -0
- package/dist/services/embedding-service.js +465 -0
- package/dist/services/embedding-service.js.map +1 -0
- package/dist/services/event-log-service.d.ts +67 -0
- package/dist/services/event-log-service.d.ts.map +1 -0
- package/dist/services/event-log-service.js +280 -0
- package/dist/services/event-log-service.js.map +1 -0
- package/dist/services/grant-service.d.ts +84 -0
- package/dist/services/grant-service.d.ts.map +1 -0
- package/dist/services/grant-service.js +214 -0
- package/dist/services/grant-service.js.map +1 -0
- package/dist/services/local-index-service.d.ts +18 -1
- package/dist/services/local-index-service.d.ts.map +1 -1
- package/dist/services/local-index-service.js +54 -4
- package/dist/services/local-index-service.js.map +1 -1
- package/dist/services/reply-reminder-service.d.ts +4 -0
- package/dist/services/reply-reminder-service.d.ts.map +1 -1
- package/dist/services/reply-reminder-service.js +22 -2
- package/dist/services/reply-reminder-service.js.map +1 -1
- package/dist/services/rules-service.d.ts +9 -1
- package/dist/services/rules-service.d.ts.map +1 -1
- package/dist/services/rules-service.js +8 -3
- package/dist/services/rules-service.js.map +1 -1
- package/dist/services/sieve-export.d.ts +19 -0
- package/dist/services/sieve-export.d.ts.map +1 -0
- package/dist/services/sieve-export.js +192 -0
- package/dist/services/sieve-export.js.map +1 -0
- package/dist/services/simple-imap-service.d.ts +13 -0
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +142 -18
- package/dist/services/simple-imap-service.js.map +1 -1
- package/dist/services/smtp-service.d.ts +1 -0
- package/dist/services/smtp-service.d.ts.map +1 -1
- package/dist/services/smtp-service.js +20 -0
- package/dist/services/smtp-service.js.map +1 -1
- package/dist/services/snooze-service.d.ts +4 -0
- package/dist/services/snooze-service.d.ts.map +1 -1
- package/dist/services/snooze-service.js +12 -3
- package/dist/services/snooze-service.js.map +1 -1
- package/dist/services/template-service.d.ts.map +1 -1
- package/dist/services/template-service.js +3 -2
- package/dist/services/template-service.js.map +1 -1
- package/dist/services/triage-plan-service.d.ts +10 -2
- package/dist/services/triage-plan-service.d.ts.map +1 -1
- package/dist/services/triage-plan-service.js +23 -3
- package/dist/services/triage-plan-service.js.map +1 -1
- package/dist/tools/attachments.js +1 -1
- package/dist/tools/attachments.js.map +1 -1
- package/dist/tools/brief.d.ts.map +1 -1
- package/dist/tools/brief.js +14 -4
- package/dist/tools/brief.js.map +1 -1
- package/dist/tools/categories.d.ts +4 -0
- package/dist/tools/categories.d.ts.map +1 -0
- package/dist/tools/categories.js +269 -0
- package/dist/tools/categories.js.map +1 -0
- package/dist/tools/commitments.d.ts.map +1 -1
- package/dist/tools/commitments.js +6 -2
- package/dist/tools/commitments.js.map +1 -1
- package/dist/tools/contacts.d.ts +6 -0
- package/dist/tools/contacts.d.ts.map +1 -0
- package/dist/tools/contacts.js +295 -0
- package/dist/tools/contacts.js.map +1 -0
- package/dist/tools/context.d.ts +4 -0
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +3 -0
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/definitions.d.ts +333 -5
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +169 -5
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/delegation.d.ts +44 -0
- package/dist/tools/delegation.d.ts.map +1 -0
- package/dist/tools/delegation.js +180 -0
- package/dist/tools/delegation.js.map +1 -0
- package/dist/tools/diagnostics.d.ts.map +1 -1
- package/dist/tools/diagnostics.js +6 -0
- package/dist/tools/diagnostics.js.map +1 -1
- package/dist/tools/drafts-support.d.ts +2 -1
- package/dist/tools/drafts-support.d.ts.map +1 -1
- package/dist/tools/drafts-support.js +14 -6
- package/dist/tools/drafts-support.js.map +1 -1
- package/dist/tools/drafts.d.ts.map +1 -1
- package/dist/tools/drafts.js +22 -1
- package/dist/tools/drafts.js.map +1 -1
- package/dist/tools/events.d.ts +47 -0
- package/dist/tools/events.d.ts.map +1 -0
- package/dist/tools/events.js +240 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/reading.d.ts.map +1 -1
- package/dist/tools/reading.js +125 -4
- package/dist/tools/reading.js.map +1 -1
- package/dist/tools/resources.d.ts +4 -0
- package/dist/tools/resources.d.ts.map +1 -1
- package/dist/tools/resources.js +9 -0
- package/dist/tools/resources.js.map +1 -1
- package/dist/tools/rules-runner.d.ts.map +1 -1
- package/dist/tools/rules-runner.js +1 -0
- package/dist/tools/rules-runner.js.map +1 -1
- package/dist/tools/rules.d.ts +9 -0
- package/dist/tools/rules.d.ts.map +1 -1
- package/dist/tools/rules.js +50 -19
- package/dist/tools/rules.js.map +1 -1
- package/dist/tools/sending.d.ts.map +1 -1
- package/dist/tools/sending.js +4 -0
- package/dist/tools/sending.js.map +1 -1
- package/dist/tools/style.d.ts +2 -0
- package/dist/tools/style.d.ts.map +1 -1
- package/dist/tools/style.js +3 -3
- package/dist/tools/style.js.map +1 -1
- package/dist/tools/templates-and-reminders.d.ts +3 -0
- package/dist/tools/templates-and-reminders.d.ts.map +1 -1
- package/dist/tools/templates-and-reminders.js +31 -17
- package/dist/tools/templates-and-reminders.js.map +1 -1
- package/dist/tools/triage.d.ts.map +1 -1
- package/dist/tools/triage.js +33 -11
- package/dist/tools/triage.js.map +1 -1
- package/dist/types/index.d.ts +23 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/account-identity.d.ts.map +1 -1
- package/dist/utils/account-identity.js +3 -2
- package/dist/utils/account-identity.js.map +1 -1
- package/dist/utils/approval.d.ts +133 -0
- package/dist/utils/approval.d.ts.map +1 -0
- package/dist/utils/approval.js +507 -0
- package/dist/utils/approval.js.map +1 -0
- package/dist/utils/atomic-write.d.ts +1 -1
- package/dist/utils/atomic-write.d.ts.map +1 -1
- package/dist/utils/atomic-write.js +10 -3
- package/dist/utils/atomic-write.js.map +1 -1
- package/dist/utils/commitment-phrases.d.ts.map +1 -1
- package/dist/utils/commitment-phrases.js +31 -18
- package/dist/utils/commitment-phrases.js.map +1 -1
- package/dist/utils/contact-text.d.ts +8 -0
- package/dist/utils/contact-text.d.ts.map +1 -0
- package/dist/utils/contact-text.js +24 -0
- package/dist/utils/contact-text.js.map +1 -0
- package/dist/utils/delegation.d.ts +17 -0
- package/dist/utils/delegation.d.ts.map +1 -0
- package/dist/utils/delegation.js +72 -0
- package/dist/utils/delegation.js.map +1 -0
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +5 -4
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/mail-security.d.ts +9 -1
- package/dist/utils/mail-security.d.ts.map +1 -1
- package/dist/utils/mail-security.js +444 -62
- package/dist/utils/mail-security.js.map +1 -1
- package/dist/utils/ocr.d.ts +24 -0
- package/dist/utils/ocr.d.ts.map +1 -0
- package/dist/utils/ocr.js +109 -0
- package/dist/utils/ocr.js.map +1 -0
- package/dist/utils/office-text-worker.d.ts +2 -0
- package/dist/utils/office-text-worker.d.ts.map +1 -0
- package/dist/utils/office-text-worker.js +17 -0
- package/dist/utils/office-text-worker.js.map +1 -0
- package/dist/utils/office-text.d.ts +32 -0
- package/dist/utils/office-text.d.ts.map +1 -0
- package/dist/utils/office-text.js +540 -0
- package/dist/utils/office-text.js.map +1 -0
- package/dist/utils/pdf-text-worker.js +2 -2
- package/dist/utils/pdf-text-worker.js.map +1 -1
- package/dist/utils/pdf-text.d.ts +10 -0
- package/dist/utils/pdf-text.d.ts.map +1 -1
- package/dist/utils/pdf-text.js +49 -9
- package/dist/utils/pdf-text.js.map +1 -1
- package/dist/utils/png.d.ts +2 -0
- package/dist/utils/png.d.ts.map +1 -0
- package/dist/utils/png.js +26 -0
- package/dist/utils/png.js.map +1 -0
- package/dist/utils/regular-file.d.ts +9 -0
- package/dist/utils/regular-file.d.ts.map +1 -0
- package/dist/utils/regular-file.js +34 -0
- package/dist/utils/regular-file.js.map +1 -0
- package/dist/utils/runtime-policy.d.ts.map +1 -1
- package/dist/utils/runtime-policy.js +2 -0
- package/dist/utils/runtime-policy.js.map +1 -1
- package/dist/utils/safe-write.d.ts +8 -0
- package/dist/utils/safe-write.d.ts.map +1 -0
- package/dist/utils/safe-write.js +46 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/dist/utils/server-state-paths.d.ts +12 -0
- package/dist/utils/server-state-paths.d.ts.map +1 -0
- package/dist/utils/server-state-paths.js +69 -0
- package/dist/utils/server-state-paths.js.map +1 -0
- package/dist/utils/style-profile.d.ts +18 -0
- package/dist/utils/style-profile.d.ts.map +1 -1
- package/dist/utils/style-profile.js +63 -14
- package/dist/utils/style-profile.js.map +1 -1
- package/dist/utils/vcard.d.ts +27 -0
- package/dist/utils/vcard.d.ts.map +1 -0
- package/dist/utils/vcard.js +141 -0
- package/dist/utils/vcard.js.map +1 -0
- package/dist/utils/zip-reader.d.ts +16 -0
- package/dist/utils/zip-reader.d.ts.map +1 -0
- package/dist/utils/zip-reader.js +108 -0
- package/dist/utils/zip-reader.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { EmailSummary, ProtonMailConfig } from "../types/index.js";
|
|
2
2
|
import { type Logger } from "../utils/logger.js";
|
|
3
|
+
import { type StyleProfile } from "../utils/style-profile.js";
|
|
3
4
|
export type AutoresponderAction = "reply" | "draft" | "skip";
|
|
4
5
|
export interface AutoresponderMatchRule {
|
|
5
6
|
name?: string;
|
|
6
|
-
/** Addresses,
|
|
7
|
+
/** Addresses, "*@example.com" / "@example.com" for a whole domain, or "tag:vip" for people with that contact tag (set_contact). Any of them matches. */
|
|
7
8
|
from?: string[];
|
|
8
9
|
/** The subject contains any of these (case-insensitive). */
|
|
9
10
|
subjectContains?: string[];
|
|
@@ -19,6 +20,12 @@ export interface AutoresponderRule {
|
|
|
19
20
|
/** Subject of the reply; {subject} is replaced by the subject of the message answered. */
|
|
20
21
|
subject: string;
|
|
21
22
|
body: string;
|
|
23
|
+
/** Subject and body by language code (en, pl, de), used when the message answered is in that language. */
|
|
24
|
+
subjects?: Record<string, string>;
|
|
25
|
+
bodies?: Record<string, string>;
|
|
26
|
+
/** Plain text for {returnDate} (a YYYY-MM-DD date is written out in the reply's language) and {backupContact}. */
|
|
27
|
+
returnDate?: string;
|
|
28
|
+
backupContact?: string;
|
|
22
29
|
/** Only mail that arrived inside this window is answered, and the outgoing note shows only inside it. */
|
|
23
30
|
startsAt?: string;
|
|
24
31
|
endsAt?: string;
|
|
@@ -72,7 +79,7 @@ export interface AutoresponderState {
|
|
|
72
79
|
log?: AutoresponderLogEntry[];
|
|
73
80
|
}
|
|
74
81
|
export declare const AUTORESPONDER_MAX_PER_DAY = 50;
|
|
75
|
-
export type AutoresponderSkipReason = "not-active" | "wrong-folder" | "before-activation" | "outside-window" | "not-human" | "suspicious" | "no-sender" | "own-address" | "no-reply-sender" | "not-addressed-to-us" | "already-handled" | "replied-recently" | "daily-limit" | "rule-skip" | "answered-already" | "no-text" | "cannot-send-or-draft";
|
|
82
|
+
export type AutoresponderSkipReason = "not-active" | "wrong-folder" | "before-activation" | "outside-window" | "not-human" | "suspicious" | "no-sender" | "own-address" | "no-reply-sender" | "not-addressed-to-us" | "already-handled" | "replied-recently" | "daily-limit" | "rule-skip" | "answered-already" | "no-text" | "cannot-send-or-draft" | "lookup-failed";
|
|
76
83
|
/** The weekday (0=Sunday) and minutes since midnight at `now` in `timezone`. */
|
|
77
84
|
export declare function localClock(now: number, timezone?: string): {
|
|
78
85
|
day: number;
|
|
@@ -87,9 +94,13 @@ export interface AutoresponderChecks {
|
|
|
87
94
|
}
|
|
88
95
|
/** Why a message must not get a reply, or undefined when it may. Pure: nothing is recorded here. */
|
|
89
96
|
export declare function skipReason(email: EmailSummary, state: AutoresponderState, ownAddresses: string[], now: number, checks?: AutoresponderChecks): AutoresponderSkipReason | undefined;
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Fills {subject}, {senderName}, {senderFirstName}, {senderAddress}, {startsAt}, {endsAt}, {returnDate}, {backupContact};
|
|
99
|
+
* other braces stay as written. Values are inserted as they are, never read as a template. `language` writes the dates
|
|
100
|
+
* in that language (default English).
|
|
101
|
+
*/
|
|
102
|
+
export declare function renderText(template: string, rule: AutoresponderRule, email: EmailSummary, language?: string): string;
|
|
103
|
+
export declare function replySubject(rule: AutoresponderRule, original: string, email?: EmailSummary, language?: string): string;
|
|
93
104
|
export interface AutoresponderReply {
|
|
94
105
|
to: string;
|
|
95
106
|
subject: string;
|
|
@@ -110,6 +121,10 @@ export interface AutoresponderDeps {
|
|
|
110
121
|
canDraft?: boolean;
|
|
111
122
|
isKnownContact?: (address: string) => Promise<boolean>;
|
|
112
123
|
hasAnsweredSince?: (address: string, sinceIso: string) => Promise<boolean>;
|
|
124
|
+
/** The contact tags of an address, for "tag:vip" in a rule's from. */
|
|
125
|
+
contactTags?: (address: string) => Promise<string[]>;
|
|
126
|
+
/** Your style profile, if one was made (get_style_profile); a draft then opens and closes the way you do. */
|
|
127
|
+
styleProfile?: () => Promise<StyleProfile | undefined>;
|
|
113
128
|
}
|
|
114
129
|
export interface AutoresponderDecision {
|
|
115
130
|
id: string;
|
|
@@ -119,6 +134,12 @@ export interface AutoresponderDecision {
|
|
|
119
134
|
reason?: AutoresponderSkipReason;
|
|
120
135
|
rule?: string;
|
|
121
136
|
reply?: AutoresponderReply;
|
|
137
|
+
/** The language the message answered is in, when it could be told. */
|
|
138
|
+
language?: string;
|
|
139
|
+
/** Where the text came from: the matching rule's own text, the text for the sender's language, or the default. */
|
|
140
|
+
textFrom?: "rule" | "language" | "default";
|
|
141
|
+
/** A draft given your greeting and closing from the style profile. */
|
|
142
|
+
styled?: boolean;
|
|
122
143
|
}
|
|
123
144
|
/** Judges one message. Pure apart from the lookups in `deps`; records nothing. */
|
|
124
145
|
export declare function decide(email: EmailSummary, state: AutoresponderState, ownAddresses: string[], now: number, deps: AutoresponderDeps, checks?: AutoresponderChecks): Promise<AutoresponderDecision>;
|
|
@@ -126,6 +147,12 @@ export interface AutoresponderPatch {
|
|
|
126
147
|
enabled?: boolean;
|
|
127
148
|
subject?: string;
|
|
128
149
|
body?: string;
|
|
150
|
+
/** Replace the whole map ({} or null clears it); an empty text removes that language. */
|
|
151
|
+
subjects?: unknown;
|
|
152
|
+
bodies?: unknown;
|
|
153
|
+
/** Empty string removes it. */
|
|
154
|
+
returnDate?: string;
|
|
155
|
+
backupContact?: string;
|
|
129
156
|
startsAt?: string;
|
|
130
157
|
endsAt?: string;
|
|
131
158
|
replyEveryDays?: number;
|
|
@@ -170,8 +197,10 @@ export declare class AutoresponderService {
|
|
|
170
197
|
};
|
|
171
198
|
recent: AutoresponderLogEntry[];
|
|
172
199
|
}>;
|
|
200
|
+
/** The rule as it would be after `set(patch)`, without storing anything (what a question to the person describes). */
|
|
201
|
+
previewSet(patch: AutoresponderPatch, now?: number): Promise<AutoresponderRule>;
|
|
173
202
|
/** Create or change the rule. Switching it on (from off) restarts "mail from before this moment is not answered". */
|
|
174
|
-
set(patch: AutoresponderPatch, now?: number): Promise<AutoresponderRule>;
|
|
203
|
+
set(patch: AutoresponderPatch, now?: number, verify?: (next: AutoresponderRule) => void): Promise<AutoresponderRule>;
|
|
175
204
|
/** What it would do with `emails`, judged as if the rule were on now. Records nothing and sends nothing. */
|
|
176
205
|
preview(emails: EmailSummary[], ownAddresses: string[], deps?: AutoresponderDeps, now?: number): Promise<AutoresponderDecision[]>;
|
|
177
206
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"autoresponder-service.d.ts","sourceRoot":"","sources":["../../src/services/autoresponder-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"autoresponder-service.d.ts","sourceRoot":"","sources":["../../src/services/autoresponder-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdir
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { readRegularFile } from "../utils/regular-file.js";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { writeFileAtomic } from "../utils/atomic-write.js";
|
|
4
5
|
import { isFileNotFound, setAsideCorruptStore } from "../utils/corrupt-store.js";
|
|
@@ -6,6 +7,7 @@ import { withFileLock } from "../utils/file-lock.js";
|
|
|
6
7
|
import { InvalidArgumentError } from "../utils/helpers.js";
|
|
7
8
|
import { logger } from "../utils/logger.js";
|
|
8
9
|
import { assessMail } from "../utils/mail-security.js";
|
|
10
|
+
import { detectLanguage, greetingAndClosing, hasClosing, hasGreeting, STYLE_LANGUAGES } from "../utils/style-profile.js";
|
|
9
11
|
export const AUTORESPONDER_MAX_PER_DAY = 50;
|
|
10
12
|
const MAX_DAILY_LIMIT = 200;
|
|
11
13
|
const MAX_RULES = 20;
|
|
@@ -105,10 +107,12 @@ export function skipReason(email, state, ownAddresses, now, checks = {}) {
|
|
|
105
107
|
return "daily-limit";
|
|
106
108
|
return undefined;
|
|
107
109
|
}
|
|
108
|
-
function matchesAddress(pattern, address) {
|
|
110
|
+
function matchesAddress(pattern, address, tags = []) {
|
|
109
111
|
const p = pattern.trim().toLowerCase();
|
|
110
112
|
if (!p)
|
|
111
113
|
return false;
|
|
114
|
+
if (p.startsWith("tag:"))
|
|
115
|
+
return tags.includes(p.slice(4).trim());
|
|
112
116
|
if (p.startsWith("*@") || p.startsWith("@"))
|
|
113
117
|
return address.endsWith(p.slice(p.indexOf("@")));
|
|
114
118
|
return address === p;
|
|
@@ -119,34 +123,44 @@ function firstName(display, address) {
|
|
|
119
123
|
return name.split(/\s+/)[0];
|
|
120
124
|
return address.split("@")[0].split(/[._+-]/)[0];
|
|
121
125
|
}
|
|
122
|
-
|
|
126
|
+
const DATE_LOCALES = { en: "en-GB", pl: "pl-PL", de: "de-DE" };
|
|
127
|
+
const PLAIN_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
128
|
+
function formatDate(iso, timezone, language) {
|
|
123
129
|
if (!iso)
|
|
124
130
|
return "";
|
|
125
131
|
try {
|
|
126
|
-
return new Intl.DateTimeFormat("en-GB", { timeZone: timezone, day: "numeric", month: "long", year: "numeric" }).format(new Date(iso));
|
|
132
|
+
return new Intl.DateTimeFormat(DATE_LOCALES[language ?? ""] ?? "en-GB", { timeZone: timezone, day: "numeric", month: "long", year: "numeric" }).format(new Date(iso));
|
|
127
133
|
}
|
|
128
134
|
catch {
|
|
129
135
|
return iso.slice(0, 10);
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
|
-
/**
|
|
133
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Fills {subject}, {senderName}, {senderFirstName}, {senderAddress}, {startsAt}, {endsAt}, {returnDate}, {backupContact};
|
|
140
|
+
* other braces stay as written. Values are inserted as they are, never read as a template. `language` writes the dates
|
|
141
|
+
* in that language (default English).
|
|
142
|
+
*/
|
|
143
|
+
export function renderText(template, rule, email, language) {
|
|
134
144
|
const address = email.from[0]?.address ?? "";
|
|
135
145
|
const display = email.from[0]?.name;
|
|
146
|
+
const returnDate = rule.returnDate ?? "";
|
|
136
147
|
const values = {
|
|
137
148
|
subject: email.subject.replace(/^(re|aw|odp)\s*:\s*/i, "").trim() || "(no subject)",
|
|
138
149
|
senderName: (display ?? "").replace(/["<>]/g, "").trim() || address,
|
|
139
150
|
senderFirstName: firstName(display, address),
|
|
140
151
|
senderAddress: address,
|
|
141
|
-
startsAt: formatDate(rule.startsAt, rule.timezone),
|
|
142
|
-
endsAt: formatDate(rule.endsAt, rule.timezone),
|
|
152
|
+
startsAt: formatDate(rule.startsAt, rule.timezone, language),
|
|
153
|
+
endsAt: formatDate(rule.endsAt, rule.timezone, language),
|
|
154
|
+
// A plain date names a day, not a moment, so it is not shifted by a time zone.
|
|
155
|
+
returnDate: PLAIN_DATE.test(returnDate) ? formatDate(`${returnDate}T00:00:00Z`, "UTC", language) : returnDate,
|
|
156
|
+
backupContact: rule.backupContact ?? "",
|
|
143
157
|
};
|
|
144
|
-
return template.replace(/\{(subject|senderName|senderFirstName|senderAddress|startsAt|endsAt)\}/g, (_, key) => values[key]);
|
|
158
|
+
return template.replace(/\{(subject|senderName|senderFirstName|senderAddress|startsAt|endsAt|returnDate|backupContact)\}/g, (_, key) => values[key]);
|
|
145
159
|
}
|
|
146
|
-
export function replySubject(rule, original, email) {
|
|
160
|
+
export function replySubject(rule, original, email, language) {
|
|
147
161
|
const template = rule.subject || DEFAULT_SUBJECT;
|
|
148
162
|
const stripped = original.replace(/^(re|aw|odp)\s*:\s*/i, "").trim() || "(no subject)";
|
|
149
|
-
const subject = email ? renderText(template, rule, { ...email, subject: original }) : template.replace(/\{subject\}/g, stripped);
|
|
163
|
+
const subject = email ? renderText(template, rule, { ...email, subject: original }, language) : template.replace(/\{subject\}/g, () => stripped);
|
|
150
164
|
return subject.replace(/[\r\n]+/g, " ").slice(0, 200);
|
|
151
165
|
}
|
|
152
166
|
/** Judges one message. Pure apart from the lookups in `deps`; records nothing. */
|
|
@@ -165,25 +179,57 @@ export async function decide(email, state, ownAddresses, now, deps, checks = {})
|
|
|
165
179
|
let subjectTemplate = rule.subject;
|
|
166
180
|
let bodyTemplate = rule.body;
|
|
167
181
|
let ruleName;
|
|
182
|
+
let ruleSubject = false;
|
|
183
|
+
let ruleBody = false;
|
|
168
184
|
const needsKnown = rule.rules?.some((candidate) => candidate.known) === true;
|
|
169
|
-
|
|
185
|
+
// A lookup that throws is not an answer: a rule that depends on it cannot be judged, so it must not be skipped over.
|
|
186
|
+
let knownFailed = false;
|
|
187
|
+
let tagsFailed = false;
|
|
188
|
+
const known = needsKnown && deps.isKnownContact ? await deps.isKnownContact(sender).catch(() => {
|
|
189
|
+
knownFailed = true;
|
|
190
|
+
return false;
|
|
191
|
+
}) : undefined;
|
|
192
|
+
const needsTags = rule.rules?.some((candidate) => candidate.from?.some((pattern) => pattern.trim().toLowerCase().startsWith("tag:"))) === true;
|
|
193
|
+
const tags = needsTags && deps.contactTags ? await deps.contactTags(sender).catch(() => {
|
|
194
|
+
tagsFailed = true;
|
|
195
|
+
return [];
|
|
196
|
+
}) : [];
|
|
170
197
|
for (const [index, candidate] of (rule.rules ?? []).entries()) {
|
|
171
|
-
|
|
172
|
-
|
|
198
|
+
let unsure = false;
|
|
199
|
+
if (candidate.from?.length && !candidate.from.some((pattern) => matchesAddress(pattern, sender, tags))) {
|
|
200
|
+
if (tagsFailed && candidate.from.some((pattern) => pattern.trim().toLowerCase().startsWith("tag:")))
|
|
201
|
+
unsure = true;
|
|
202
|
+
else
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
173
205
|
if (candidate.subjectContains?.length && !candidate.subjectContains.some((text) => email.subject.toLowerCase().includes(text.toLowerCase())))
|
|
174
206
|
continue;
|
|
175
|
-
if (candidate.known &&
|
|
207
|
+
if (candidate.known && knownFailed)
|
|
208
|
+
unsure = true;
|
|
209
|
+
else if (candidate.known && (known === undefined || (candidate.known === "yes") !== known))
|
|
176
210
|
continue;
|
|
211
|
+
// Whatever this rule says (skip, say) cannot be told, and the default reply is not what its author meant.
|
|
212
|
+
if (unsure)
|
|
213
|
+
return { ...base, outcome: "skip", reason: "lookup-failed", rule: candidate.name || `rule ${index + 1}` };
|
|
177
214
|
action = candidate.action;
|
|
178
215
|
ruleName = candidate.name || `rule ${index + 1}`;
|
|
179
216
|
if (candidate.subject !== undefined)
|
|
180
|
-
subjectTemplate = candidate.subject;
|
|
217
|
+
[subjectTemplate, ruleSubject] = [candidate.subject, true];
|
|
181
218
|
if (candidate.body !== undefined)
|
|
182
|
-
bodyTemplate = candidate.body;
|
|
219
|
+
[bodyTemplate, ruleBody] = [candidate.body, true];
|
|
183
220
|
break;
|
|
184
221
|
}
|
|
185
222
|
if (action === "skip")
|
|
186
223
|
return { ...base, outcome: "skip", reason: "rule-skip", rule: ruleName };
|
|
224
|
+
// The sender's language picks the text where the matching rule has none of its own.
|
|
225
|
+
const language = detectLanguage(`${email.subject}\n${email.preview ?? ""}`.slice(0, 2000));
|
|
226
|
+
let textFrom = ruleBody ? "rule" : "default";
|
|
227
|
+
let bodyLanguage;
|
|
228
|
+
let subjectLanguage;
|
|
229
|
+
if (language && !ruleBody && rule.bodies?.[language])
|
|
230
|
+
[bodyTemplate, bodyLanguage, textFrom] = [rule.bodies[language], language, "language"];
|
|
231
|
+
if (language && !ruleSubject && rule.subjects?.[language])
|
|
232
|
+
[subjectTemplate, subjectLanguage] = [rule.subjects[language], language];
|
|
187
233
|
if (!bodyTemplate?.trim())
|
|
188
234
|
return { ...base, outcome: "skip", reason: "no-text", rule: ruleName };
|
|
189
235
|
// What the server may do: a reply it may not send becomes a draft; with neither, nothing.
|
|
@@ -195,20 +241,38 @@ export async function decide(email, state, ownAddresses, now, deps, checks = {})
|
|
|
195
241
|
return { ...base, outcome: "skip", reason: "cannot-send-or-draft", rule: ruleName };
|
|
196
242
|
if (rule.skipIfAnswered !== false && deps.hasAnsweredSince) {
|
|
197
243
|
const since = email.internalDate ?? email.date ?? new Date(now).toISOString();
|
|
198
|
-
|
|
244
|
+
// A lookup that throws is not "no": the sender may already have been answered, so wait for the next sync.
|
|
245
|
+
const answered = await deps.hasAnsweredSince(sender, since).catch(() => undefined);
|
|
246
|
+
if (answered === undefined)
|
|
247
|
+
return { ...base, outcome: "skip", reason: "lookup-failed", rule: ruleName };
|
|
248
|
+
if (answered)
|
|
199
249
|
return { ...base, outcome: "skip", reason: "answered-already", rule: ruleName };
|
|
200
250
|
}
|
|
251
|
+
let body = renderText(bodyTemplate, rule, email, bodyLanguage);
|
|
252
|
+
let styled = false;
|
|
253
|
+
// Only a draft: it is read before it goes, while a sent reply carries exactly the text that was set.
|
|
254
|
+
const replyLanguage = bodyLanguage ?? detectLanguage(body);
|
|
255
|
+
if (action === "draft" && deps.styleProfile && replyLanguage) {
|
|
256
|
+
const profile = await deps.styleProfile().catch(() => undefined);
|
|
257
|
+
if (profile) {
|
|
258
|
+
const { greeting, closing } = greetingAndClosing(profile, sender, replyLanguage);
|
|
259
|
+
const open = greeting && !hasGreeting(body) ? `${greeting.replace("{name}", () => firstName(email.from[0]?.name, email.from[0]?.address ?? sender))}\n\n` : "";
|
|
260
|
+
const close = closing && !hasClosing(body) ? `\n\n${closing}` : "";
|
|
261
|
+
if (open || close)
|
|
262
|
+
[body, styled] = [`${open}${body}${close}`, true];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
201
265
|
const reply = {
|
|
202
266
|
to: sender,
|
|
203
|
-
subject: replySubject({ ...rule, subject: subjectTemplate }, email.subject, email),
|
|
204
|
-
body
|
|
267
|
+
subject: replySubject({ ...rule, subject: subjectTemplate }, email.subject, email, subjectLanguage),
|
|
268
|
+
body,
|
|
205
269
|
inReplyTo: email.messageId,
|
|
206
270
|
references: [...(email.references ?? []), ...(email.messageId ? [email.messageId] : [])],
|
|
207
271
|
headers: { "Auto-Submitted": "auto-replied", Precedence: "bulk", "X-Auto-Response-Suppress": "All" },
|
|
208
272
|
sourceEmailId: email.id,
|
|
209
273
|
rule: ruleName,
|
|
210
274
|
};
|
|
211
|
-
return { ...base, outcome: action, reply, rule: ruleName };
|
|
275
|
+
return { ...base, outcome: action, reply, rule: ruleName, language, textFrom, ...(styled ? { styled } : {}) };
|
|
212
276
|
}
|
|
213
277
|
function validateRules(rules) {
|
|
214
278
|
if (!Array.isArray(rules))
|
|
@@ -274,6 +338,136 @@ function validateOutsideHours(value) {
|
|
|
274
338
|
}
|
|
275
339
|
return { days: [...new Set(hours.days)].sort(), from: hours.from, to: hours.to };
|
|
276
340
|
}
|
|
341
|
+
// Only the languages the detector can tell apart: a text for any other could never be chosen.
|
|
342
|
+
function validateLanguageTexts(value, name, max) {
|
|
343
|
+
if (value === null)
|
|
344
|
+
return undefined;
|
|
345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
346
|
+
throw new InvalidArgumentError(`${name} must be an object keyed by language code (${STYLE_LANGUAGES.join(", ")}), or null to remove it.`);
|
|
347
|
+
}
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [key, text] of Object.entries(value)) {
|
|
350
|
+
const code = key.trim().toLowerCase();
|
|
351
|
+
if (!STYLE_LANGUAGES.includes(code))
|
|
352
|
+
throw new InvalidArgumentError(`${name}: "${key.slice(0, 20)}" is not a language that can be recognised; use ${STYLE_LANGUAGES.join(", ")}.`);
|
|
353
|
+
if (typeof text !== "string" || text.length > max)
|
|
354
|
+
throw new InvalidArgumentError(`${name}.${code} must be text up to ${max} characters.`);
|
|
355
|
+
if (text.trim())
|
|
356
|
+
out[code] = text.trim();
|
|
357
|
+
}
|
|
358
|
+
return Object.keys(out).length ? out : undefined;
|
|
359
|
+
}
|
|
360
|
+
function validateLine(value, name, max) {
|
|
361
|
+
if (value.length > max || /[\r\n]/.test(value))
|
|
362
|
+
throw new InvalidArgumentError(`${name} must be one line of up to ${max} characters.`);
|
|
363
|
+
return value.trim() || undefined;
|
|
364
|
+
}
|
|
365
|
+
/** What the rule becomes when `patch` is applied to `before`. Pure: nothing is stored. */
|
|
366
|
+
function mergePatch(before, patch, now) {
|
|
367
|
+
const next = {
|
|
368
|
+
enabled: before?.enabled ?? false,
|
|
369
|
+
subject: before?.subject ?? DEFAULT_SUBJECT,
|
|
370
|
+
body: before?.body ?? "",
|
|
371
|
+
subjects: before?.subjects,
|
|
372
|
+
bodies: before?.bodies,
|
|
373
|
+
returnDate: before?.returnDate,
|
|
374
|
+
backupContact: before?.backupContact,
|
|
375
|
+
startsAt: before?.startsAt,
|
|
376
|
+
endsAt: before?.endsAt,
|
|
377
|
+
replyEveryDays: before?.replyEveryDays ?? 7,
|
|
378
|
+
outgoingNote: before?.outgoingNote,
|
|
379
|
+
updatedAt: new Date(now).toISOString(),
|
|
380
|
+
activatedAt: before?.activatedAt,
|
|
381
|
+
mode: before?.mode,
|
|
382
|
+
timezone: before?.timezone,
|
|
383
|
+
outsideHours: before?.outsideHours,
|
|
384
|
+
dailyLimit: before?.dailyLimit,
|
|
385
|
+
skipIfAnswered: before?.skipIfAnswered,
|
|
386
|
+
rules: before?.rules,
|
|
387
|
+
};
|
|
388
|
+
if (patch.enabled !== undefined)
|
|
389
|
+
next.enabled = patch.enabled;
|
|
390
|
+
if (patch.subject !== undefined) {
|
|
391
|
+
if (patch.subject.length > 200)
|
|
392
|
+
throw new InvalidArgumentError("subject is limited to 200 characters.");
|
|
393
|
+
next.subject = patch.subject.trim() || DEFAULT_SUBJECT;
|
|
394
|
+
}
|
|
395
|
+
if (patch.body !== undefined) {
|
|
396
|
+
if (patch.body.length > 4000)
|
|
397
|
+
throw new InvalidArgumentError("body is limited to 4000 characters.");
|
|
398
|
+
next.body = patch.body.trim();
|
|
399
|
+
}
|
|
400
|
+
if (patch.subjects !== undefined)
|
|
401
|
+
next.subjects = validateLanguageTexts(patch.subjects, "subjects", 200);
|
|
402
|
+
if (patch.bodies !== undefined)
|
|
403
|
+
next.bodies = validateLanguageTexts(patch.bodies, "bodies", 4000);
|
|
404
|
+
if (patch.returnDate !== undefined) {
|
|
405
|
+
next.returnDate = validateLine(patch.returnDate, "returnDate", 100);
|
|
406
|
+
const day = next.returnDate && PLAIN_DATE.test(next.returnDate) ? Date.parse(`${next.returnDate}T00:00:00Z`) : undefined;
|
|
407
|
+
if (day !== undefined && (!Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== next.returnDate)) {
|
|
408
|
+
throw new InvalidArgumentError(`returnDate "${next.returnDate}" is not a real date.`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (patch.backupContact !== undefined)
|
|
412
|
+
next.backupContact = validateLine(patch.backupContact, "backupContact", 200);
|
|
413
|
+
if (patch.startsAt !== undefined)
|
|
414
|
+
next.startsAt = parseDate(patch.startsAt, "startsAt");
|
|
415
|
+
if (patch.endsAt !== undefined)
|
|
416
|
+
next.endsAt = parseDate(patch.endsAt, "endsAt");
|
|
417
|
+
if (patch.replyEveryDays !== undefined) {
|
|
418
|
+
if (!Number.isInteger(patch.replyEveryDays) || patch.replyEveryDays < 1 || patch.replyEveryDays > 90) {
|
|
419
|
+
throw new InvalidArgumentError("replyEveryDays must be a whole number from 1 to 90.");
|
|
420
|
+
}
|
|
421
|
+
next.replyEveryDays = patch.replyEveryDays;
|
|
422
|
+
}
|
|
423
|
+
if (patch.outgoingNote !== undefined) {
|
|
424
|
+
if (patch.outgoingNote.length > 1000)
|
|
425
|
+
throw new InvalidArgumentError("outgoingNote is limited to 1000 characters.");
|
|
426
|
+
next.outgoingNote = patch.outgoingNote.trim() || undefined;
|
|
427
|
+
}
|
|
428
|
+
if (patch.mode !== undefined) {
|
|
429
|
+
if (patch.mode !== "send" && patch.mode !== "draft")
|
|
430
|
+
throw new InvalidArgumentError('mode must be "send" or "draft".');
|
|
431
|
+
next.mode = patch.mode;
|
|
432
|
+
}
|
|
433
|
+
if (patch.timezone !== undefined) {
|
|
434
|
+
if (patch.timezone === "")
|
|
435
|
+
next.timezone = undefined;
|
|
436
|
+
else {
|
|
437
|
+
try {
|
|
438
|
+
new Intl.DateTimeFormat("en-US", { timeZone: patch.timezone });
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
throw new InvalidArgumentError(`timezone must be an IANA name such as Europe/Warsaw (got "${patch.timezone}").`);
|
|
442
|
+
}
|
|
443
|
+
next.timezone = patch.timezone;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (patch.outsideHours !== undefined)
|
|
447
|
+
next.outsideHours = validateOutsideHours(patch.outsideHours);
|
|
448
|
+
if (patch.dailyLimit !== undefined) {
|
|
449
|
+
if (!Number.isInteger(patch.dailyLimit) || patch.dailyLimit < 1 || patch.dailyLimit > MAX_DAILY_LIMIT) {
|
|
450
|
+
throw new InvalidArgumentError(`dailyLimit must be a whole number from 1 to ${MAX_DAILY_LIMIT}.`);
|
|
451
|
+
}
|
|
452
|
+
next.dailyLimit = patch.dailyLimit;
|
|
453
|
+
}
|
|
454
|
+
if (patch.skipIfAnswered !== undefined)
|
|
455
|
+
next.skipIfAnswered = patch.skipIfAnswered;
|
|
456
|
+
if (patch.rules !== undefined)
|
|
457
|
+
next.rules = validateRules(patch.rules);
|
|
458
|
+
if (next.rules && next.rules.length === 0)
|
|
459
|
+
next.rules = undefined;
|
|
460
|
+
if (next.startsAt && next.endsAt && Date.parse(next.endsAt) <= Date.parse(next.startsAt)) {
|
|
461
|
+
throw new InvalidArgumentError("endsAt must be after startsAt.");
|
|
462
|
+
}
|
|
463
|
+
const hasText = Boolean(next.body) || Boolean(next.bodies) || Boolean(next.outgoingNote) || Boolean(next.rules?.some((rule) => rule.action !== "skip" && rule.body));
|
|
464
|
+
if (next.enabled && !hasText) {
|
|
465
|
+
throw new InvalidArgumentError("Give a body (the reply), a rule with a body, or an outgoingNote before switching the autoresponder on.");
|
|
466
|
+
}
|
|
467
|
+
if (next.enabled && !before?.enabled)
|
|
468
|
+
next.activatedAt = new Date(now).toISOString();
|
|
469
|
+
return next;
|
|
470
|
+
}
|
|
277
471
|
export class AutoresponderService {
|
|
278
472
|
log;
|
|
279
473
|
storePath;
|
|
@@ -333,96 +527,17 @@ export class AutoresponderService {
|
|
|
333
527
|
recent: log.slice(-20).reverse(),
|
|
334
528
|
};
|
|
335
529
|
}
|
|
530
|
+
/** The rule as it would be after `set(patch)`, without storing anything (what a question to the person describes). */
|
|
531
|
+
async previewSet(patch, now = Date.now()) {
|
|
532
|
+
return mergePatch((await this.load()).rule, patch, now);
|
|
533
|
+
}
|
|
336
534
|
/** Create or change the rule. Switching it on (from off) restarts "mail from before this moment is not answered". */
|
|
337
|
-
async set(patch, now = Date.now()) {
|
|
535
|
+
async set(patch, now = Date.now(), verify) {
|
|
338
536
|
return this.withLock(async () => {
|
|
339
537
|
const state = await this.loadUnlocked();
|
|
340
538
|
const before = state.rule;
|
|
341
|
-
const next =
|
|
342
|
-
|
|
343
|
-
subject: before?.subject ?? DEFAULT_SUBJECT,
|
|
344
|
-
body: before?.body ?? "",
|
|
345
|
-
startsAt: before?.startsAt,
|
|
346
|
-
endsAt: before?.endsAt,
|
|
347
|
-
replyEveryDays: before?.replyEveryDays ?? 7,
|
|
348
|
-
outgoingNote: before?.outgoingNote,
|
|
349
|
-
updatedAt: new Date(now).toISOString(),
|
|
350
|
-
activatedAt: before?.activatedAt,
|
|
351
|
-
mode: before?.mode,
|
|
352
|
-
timezone: before?.timezone,
|
|
353
|
-
outsideHours: before?.outsideHours,
|
|
354
|
-
dailyLimit: before?.dailyLimit,
|
|
355
|
-
skipIfAnswered: before?.skipIfAnswered,
|
|
356
|
-
rules: before?.rules,
|
|
357
|
-
};
|
|
358
|
-
if (patch.enabled !== undefined)
|
|
359
|
-
next.enabled = patch.enabled;
|
|
360
|
-
if (patch.subject !== undefined) {
|
|
361
|
-
if (patch.subject.length > 200)
|
|
362
|
-
throw new InvalidArgumentError("subject is limited to 200 characters.");
|
|
363
|
-
next.subject = patch.subject.trim() || DEFAULT_SUBJECT;
|
|
364
|
-
}
|
|
365
|
-
if (patch.body !== undefined) {
|
|
366
|
-
if (patch.body.length > 4000)
|
|
367
|
-
throw new InvalidArgumentError("body is limited to 4000 characters.");
|
|
368
|
-
next.body = patch.body.trim();
|
|
369
|
-
}
|
|
370
|
-
if (patch.startsAt !== undefined)
|
|
371
|
-
next.startsAt = parseDate(patch.startsAt, "startsAt");
|
|
372
|
-
if (patch.endsAt !== undefined)
|
|
373
|
-
next.endsAt = parseDate(patch.endsAt, "endsAt");
|
|
374
|
-
if (patch.replyEveryDays !== undefined) {
|
|
375
|
-
if (!Number.isInteger(patch.replyEveryDays) || patch.replyEveryDays < 1 || patch.replyEveryDays > 90) {
|
|
376
|
-
throw new InvalidArgumentError("replyEveryDays must be a whole number from 1 to 90.");
|
|
377
|
-
}
|
|
378
|
-
next.replyEveryDays = patch.replyEveryDays;
|
|
379
|
-
}
|
|
380
|
-
if (patch.outgoingNote !== undefined) {
|
|
381
|
-
if (patch.outgoingNote.length > 1000)
|
|
382
|
-
throw new InvalidArgumentError("outgoingNote is limited to 1000 characters.");
|
|
383
|
-
next.outgoingNote = patch.outgoingNote.trim() || undefined;
|
|
384
|
-
}
|
|
385
|
-
if (patch.mode !== undefined) {
|
|
386
|
-
if (patch.mode !== "send" && patch.mode !== "draft")
|
|
387
|
-
throw new InvalidArgumentError('mode must be "send" or "draft".');
|
|
388
|
-
next.mode = patch.mode;
|
|
389
|
-
}
|
|
390
|
-
if (patch.timezone !== undefined) {
|
|
391
|
-
if (patch.timezone === "")
|
|
392
|
-
next.timezone = undefined;
|
|
393
|
-
else {
|
|
394
|
-
try {
|
|
395
|
-
new Intl.DateTimeFormat("en-US", { timeZone: patch.timezone });
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
throw new InvalidArgumentError(`timezone must be an IANA name such as Europe/Warsaw (got "${patch.timezone}").`);
|
|
399
|
-
}
|
|
400
|
-
next.timezone = patch.timezone;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (patch.outsideHours !== undefined)
|
|
404
|
-
next.outsideHours = validateOutsideHours(patch.outsideHours);
|
|
405
|
-
if (patch.dailyLimit !== undefined) {
|
|
406
|
-
if (!Number.isInteger(patch.dailyLimit) || patch.dailyLimit < 1 || patch.dailyLimit > MAX_DAILY_LIMIT) {
|
|
407
|
-
throw new InvalidArgumentError(`dailyLimit must be a whole number from 1 to ${MAX_DAILY_LIMIT}.`);
|
|
408
|
-
}
|
|
409
|
-
next.dailyLimit = patch.dailyLimit;
|
|
410
|
-
}
|
|
411
|
-
if (patch.skipIfAnswered !== undefined)
|
|
412
|
-
next.skipIfAnswered = patch.skipIfAnswered;
|
|
413
|
-
if (patch.rules !== undefined)
|
|
414
|
-
next.rules = validateRules(patch.rules);
|
|
415
|
-
if (next.rules && next.rules.length === 0)
|
|
416
|
-
next.rules = undefined;
|
|
417
|
-
if (next.startsAt && next.endsAt && Date.parse(next.endsAt) <= Date.parse(next.startsAt)) {
|
|
418
|
-
throw new InvalidArgumentError("endsAt must be after startsAt.");
|
|
419
|
-
}
|
|
420
|
-
const hasText = Boolean(next.body) || Boolean(next.outgoingNote) || Boolean(next.rules?.some((rule) => rule.action !== "skip" && rule.body));
|
|
421
|
-
if (next.enabled && !hasText) {
|
|
422
|
-
throw new InvalidArgumentError("Give a body (the reply), a rule with a body, or an outgoingNote before switching the autoresponder on.");
|
|
423
|
-
}
|
|
424
|
-
if (next.enabled && !before?.enabled)
|
|
425
|
-
next.activatedAt = new Date(now).toISOString();
|
|
539
|
+
const next = mergePatch(before, patch, now);
|
|
540
|
+
verify?.(next);
|
|
426
541
|
state.rule = next;
|
|
427
542
|
await this.save(state);
|
|
428
543
|
this.cache = { rule: next, loadedAt: Date.now() };
|
|
@@ -549,7 +664,7 @@ export class AutoresponderService {
|
|
|
549
664
|
}
|
|
550
665
|
async loadUnlocked() {
|
|
551
666
|
try {
|
|
552
|
-
const parsed = JSON.parse(await
|
|
667
|
+
const parsed = JSON.parse(await readRegularFile(this.storePath));
|
|
553
668
|
return {
|
|
554
669
|
...emptyState(),
|
|
555
670
|
...parsed,
|