@remit/mailbox-service 0.0.55 → 0.0.57
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/message-sync-address-spoof.test.ts +0 -23
- package/src/spam-report.test.ts +163 -11
- package/src/spam-report.ts +110 -13
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Harvesting takes the display name straight off the envelope, and a sender
|
|
3
|
-
* chooses that string. A 2021 spam message reached this account's INBOX with
|
|
4
|
-
* `matthijs@ischen.nl <aramirez@secresaludguaviare.gov.co>`: the account's own
|
|
5
|
-
* address as the label on a stranger's. It was stored, autocomplete offered it
|
|
6
|
-
* back when the account holder typed his own address, and a private reply left
|
|
7
|
-
* the instance (issue #826).
|
|
8
|
-
*
|
|
9
|
-
* The name lands in three columns on this one save — the address book, the
|
|
10
|
-
* envelope the message header renders, and the sender label the message list
|
|
11
|
-
* shows and search indexes — so all three are asserted here.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
import assert from "node:assert/strict";
|
|
15
2
|
import { describe, it } from "node:test";
|
|
16
3
|
import type {
|
|
@@ -50,10 +37,6 @@ interface Saved {
|
|
|
50
37
|
threadMessages: CreateThreadMessageInput[];
|
|
51
38
|
}
|
|
52
39
|
|
|
53
|
-
/**
|
|
54
|
-
* Drive the real save path so the stored value is the one the write path
|
|
55
|
-
* produces, not one a test handed to a private method.
|
|
56
|
-
*/
|
|
57
40
|
const harvest = async (envelope: Partial<ImapEnvelope>): Promise<Saved> => {
|
|
58
41
|
const saved: Saved = {
|
|
59
42
|
addresses: [],
|
|
@@ -149,12 +132,6 @@ describe("harvesting an envelope display name", () => {
|
|
|
149
132
|
);
|
|
150
133
|
});
|
|
151
134
|
|
|
152
|
-
/**
|
|
153
|
-
* The incident with one word prepended. An anchored guard reads this as an
|
|
154
|
-
* ordinary name and stores it, and autocomplete then ties it with the real
|
|
155
|
-
* address on the term `matthijs` and breaks the tie on correspondence. The
|
|
156
|
-
* word itself is not the lie, so it survives the address being removed.
|
|
157
|
-
*/
|
|
158
135
|
it("keeps what a name says besides the address it claims", async () => {
|
|
159
136
|
const input = await onlyAddress({
|
|
160
137
|
from: [{ ...spoof, name: "Matthijs <matthijs@ischen.nl>" }],
|
package/src/spam-report.test.ts
CHANGED
|
@@ -10,10 +10,15 @@ import type {
|
|
|
10
10
|
IMessageRepository,
|
|
11
11
|
IThreadMessageRepository,
|
|
12
12
|
} from "@remit/data-ports";
|
|
13
|
+
import { deriveAddressId } from "@remit/data-ports/id";
|
|
13
14
|
import { AddressRole } from "@remit/domain-enums";
|
|
14
15
|
import { FlagPushService } from "./flag-push.js";
|
|
15
16
|
import { MessageMoveService } from "./message-move.js";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
MoveNotSettledError,
|
|
19
|
+
NoJunkMailboxError,
|
|
20
|
+
SpamReportService,
|
|
21
|
+
} from "./spam-report.js";
|
|
17
22
|
|
|
18
23
|
const ACCOUNT = "acc-1";
|
|
19
24
|
const ACCOUNT_CONFIG = "cfg-1";
|
|
@@ -21,7 +26,8 @@ const ACCOUNT_EMAIL = "me@example.com";
|
|
|
21
26
|
const INBOX_MAILBOX = "mbx-inbox";
|
|
22
27
|
const JUNK_MAILBOX = "mbx-junk";
|
|
23
28
|
const MESSAGE_ID = "msg-1";
|
|
24
|
-
const
|
|
29
|
+
const SENDER_EMAIL = "sender@example.com";
|
|
30
|
+
const ADDRESS_ID = deriveAddressId(ACCOUNT_CONFIG, SENDER_EMAIL);
|
|
25
31
|
const THREAD_ID = "thread-1";
|
|
26
32
|
|
|
27
33
|
interface ThreadRow {
|
|
@@ -59,10 +65,21 @@ const buildWorld = (
|
|
|
59
65
|
startMailbox?: string;
|
|
60
66
|
fromEmail?: string;
|
|
61
67
|
originalMailboxId?: string;
|
|
68
|
+
/** Whether the sender was ever harvested into an `address` row. */
|
|
69
|
+
harvestedSender?: boolean;
|
|
70
|
+
/** Flags already standing on the harvested sender's row. */
|
|
71
|
+
senderFlags?: Record<string, unknown>;
|
|
72
|
+
junkMailbox?: { mailboxId: string; fullPath: string } | null;
|
|
62
73
|
} = {},
|
|
63
74
|
): World => {
|
|
64
75
|
const startMailbox = opts.startMailbox ?? INBOX_MAILBOX;
|
|
65
|
-
const fromEmail = opts.fromEmail ??
|
|
76
|
+
const fromEmail = opts.fromEmail ?? SENDER_EMAIL;
|
|
77
|
+
const fromAddressId = deriveAddressId(ACCOUNT_CONFIG, fromEmail);
|
|
78
|
+
const harvestedSender = opts.harvestedSender ?? true;
|
|
79
|
+
const junkMailbox =
|
|
80
|
+
opts.junkMailbox === undefined
|
|
81
|
+
? { mailboxId: JUNK_MAILBOX, fullPath: "Junk" }
|
|
82
|
+
: opts.junkMailbox;
|
|
66
83
|
|
|
67
84
|
const messages = new Map<string, Record<string, unknown>>([
|
|
68
85
|
[
|
|
@@ -85,9 +102,11 @@ const buildWorld = (
|
|
|
85
102
|
],
|
|
86
103
|
]);
|
|
87
104
|
|
|
88
|
-
const addresses = new Map<string, { flags: AddressFlags }>(
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
const addresses = new Map<string, { flags: AddressFlags }>(
|
|
106
|
+
harvestedSender
|
|
107
|
+
? [[fromAddressId, { flags: (opts.senderFlags ?? {}) as AddressFlags }]]
|
|
108
|
+
: [],
|
|
109
|
+
);
|
|
91
110
|
|
|
92
111
|
const threadRows: ThreadRow[] = [
|
|
93
112
|
{
|
|
@@ -134,7 +153,8 @@ const buildWorld = (
|
|
|
134
153
|
{
|
|
135
154
|
envelopeAddressId: "ea-1",
|
|
136
155
|
messageId: id,
|
|
137
|
-
addressId:
|
|
156
|
+
addressId: fromAddressId,
|
|
157
|
+
displayName: "Spammy Sender",
|
|
138
158
|
normalizedEmail: fromEmail,
|
|
139
159
|
addressRole: AddressRole.From,
|
|
140
160
|
addressOrder: 0,
|
|
@@ -173,6 +193,17 @@ const buildWorld = (
|
|
|
173
193
|
} as unknown as IMessageRepository;
|
|
174
194
|
|
|
175
195
|
const addressService = {
|
|
196
|
+
getAddress: async (_accountConfigId: string, addressIds: string[]) =>
|
|
197
|
+
addressIds
|
|
198
|
+
.filter((id) => addresses.has(id))
|
|
199
|
+
.map((id) => ({ addressId: id, ...addresses.get(id) })),
|
|
200
|
+
// Deliberately destructive on conflict, mirroring the real repo's
|
|
201
|
+
// `onConflictDoUpdate`: a caller that upserts over a row it did not
|
|
202
|
+
// create loses what stood on it.
|
|
203
|
+
upsertAddress: async (input: { addressId: string }) => {
|
|
204
|
+
addresses.set(input.addressId, { flags: {} });
|
|
205
|
+
return { ...input, flags: {} };
|
|
206
|
+
},
|
|
176
207
|
mergeFlags: async (
|
|
177
208
|
_accountConfigId: string,
|
|
178
209
|
addressId: string,
|
|
@@ -199,10 +230,7 @@ const buildWorld = (
|
|
|
199
230
|
} as unknown as IAccountRepository;
|
|
200
231
|
|
|
201
232
|
const mailboxSpecialUseService = {
|
|
202
|
-
findJunkMailbox: async () =>
|
|
203
|
-
mailboxId: JUNK_MAILBOX,
|
|
204
|
-
fullPath: "Junk",
|
|
205
|
-
}),
|
|
233
|
+
findJunkMailbox: async () => junkMailbox,
|
|
206
234
|
findTrashMailbox: async () => null,
|
|
207
235
|
} as unknown as IMailboxSpecialUseRepository;
|
|
208
236
|
|
|
@@ -399,9 +427,133 @@ describe("SpamReportService.reportSpam", () => {
|
|
|
399
427
|
assert.ok(message !== undefined);
|
|
400
428
|
assert.ok((message.spamReport as { reportedAt: number }).reportedAt > 0);
|
|
401
429
|
});
|
|
430
|
+
|
|
431
|
+
it("refuses to mint a row for a sender address carrying no @ at all", async () => {
|
|
432
|
+
// slice() around a lastIndexOf of -1 splits "no-at-sign" into a
|
|
433
|
+
// plausible-looking local part and domain, so a length check alone
|
|
434
|
+
// waves the one input it exists to catch straight through into a
|
|
435
|
+
// corrupt address row.
|
|
436
|
+
const { service, addresses } = buildWorld({
|
|
437
|
+
harvestedSender: false,
|
|
438
|
+
fromEmail: "no-at-sign",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await assert.rejects(
|
|
442
|
+
() =>
|
|
443
|
+
service.reportSpam({
|
|
444
|
+
accountConfigId: ACCOUNT_CONFIG,
|
|
445
|
+
accountId: ACCOUNT,
|
|
446
|
+
messageId: MESSAGE_ID,
|
|
447
|
+
}),
|
|
448
|
+
/not a usable email address/,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
assert.equal(addresses.size, 0);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("reports a message whose sender was never harvested into an address row", async () => {
|
|
455
|
+
// The row is what carries the blocked flag, and harvesting is what
|
|
456
|
+
// ordinarily writes it. When it is absent, blocking the sender used to
|
|
457
|
+
// throw NotFoundError and take the whole report down with it — the
|
|
458
|
+
// message never reached Junk and the user got a retry that could not
|
|
459
|
+
// work (test.remit.email, 18 Aug 2026).
|
|
460
|
+
const { service, messages, addresses, sent } = buildWorld({
|
|
461
|
+
harvestedSender: false,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await service.reportSpam({
|
|
465
|
+
accountConfigId: ACCOUNT_CONFIG,
|
|
466
|
+
accountId: ACCOUNT,
|
|
467
|
+
messageId: MESSAGE_ID,
|
|
468
|
+
setBy: "user-1",
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const address = addresses.get(ADDRESS_ID);
|
|
472
|
+
assert.equal(address?.flags.blocked?.value, true);
|
|
473
|
+
|
|
474
|
+
const message = messages.get(MESSAGE_ID);
|
|
475
|
+
assert.equal(message?.mailboxId, JUNK_MAILBOX);
|
|
476
|
+
assert.equal(moveEvents(sent).length, 1);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("keeps the junkOnly mark that withholds the sender from autocomplete", async () => {
|
|
480
|
+
// Report spam adds the block; it is not a sighting of the sender and
|
|
481
|
+
// must not carry an upsert's on-conflict behaviour onto a row it did
|
|
482
|
+
// not create. The mark that withholds a spammer from autocomplete (#822)
|
|
483
|
+
// lives in these same flags, and clearing it here would put the spammer
|
|
484
|
+
// back in the compose picker — the opposite of what the button means.
|
|
485
|
+
const { service, addresses } = buildWorld({
|
|
486
|
+
senderFlags: {
|
|
487
|
+
junkOnly: { value: true, setAt: 1, setBy: "junk-harvest" },
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
await service.reportSpam({
|
|
492
|
+
accountConfigId: ACCOUNT_CONFIG,
|
|
493
|
+
accountId: ACCOUNT,
|
|
494
|
+
messageId: MESSAGE_ID,
|
|
495
|
+
setBy: "user-1",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const flags = addresses.get(ADDRESS_ID)?.flags as Record<string, unknown>;
|
|
499
|
+
assert.equal((flags.blocked as { value: boolean }).value, true);
|
|
500
|
+
assert.deepEqual(flags.junkOnly, {
|
|
501
|
+
value: true,
|
|
502
|
+
setAt: 1,
|
|
503
|
+
setBy: "junk-harvest",
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("names the missing Junk folder and changes nothing when the account has none", async () => {
|
|
508
|
+
const { service, messages, addresses, sent, markerPuts } = buildWorld({
|
|
509
|
+
junkMailbox: null,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
await assert.rejects(
|
|
513
|
+
() =>
|
|
514
|
+
service.reportSpam({
|
|
515
|
+
accountConfigId: ACCOUNT_CONFIG,
|
|
516
|
+
accountId: ACCOUNT,
|
|
517
|
+
messageId: MESSAGE_ID,
|
|
518
|
+
}),
|
|
519
|
+
(error: unknown) =>
|
|
520
|
+
error instanceof NoJunkMailboxError &&
|
|
521
|
+
/no Junk folder/.test(error.message) &&
|
|
522
|
+
/Create one/.test(error.message),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
// Nothing half-applied: no sender blocked, no report stamp on a message
|
|
526
|
+
// still sitting where it was, no move, no keyword marker.
|
|
527
|
+
assert.equal(addresses.get(ADDRESS_ID)?.flags.blocked, undefined);
|
|
528
|
+
const message = messages.get(MESSAGE_ID);
|
|
529
|
+
assert.equal(message?.spamReport, undefined);
|
|
530
|
+
assert.equal(message?.mailboxId, INBOX_MAILBOX);
|
|
531
|
+
assert.equal(moveEvents(sent).length, 0);
|
|
532
|
+
assert.equal(markerPuts.length, 0);
|
|
533
|
+
});
|
|
402
534
|
});
|
|
403
535
|
|
|
404
536
|
describe("SpamReportService.notSpam", () => {
|
|
537
|
+
it("mints no address row for a sender that was never harvested", async () => {
|
|
538
|
+
// A sender with no row has no block to lift, so the flag write is
|
|
539
|
+
// already true. Creating the row to write it would put an address
|
|
540
|
+
// nothing ever harvested into the address book, where autocomplete
|
|
541
|
+
// would then offer it.
|
|
542
|
+
const { service, messages, addresses } = buildWorld({
|
|
543
|
+
harvestedSender: false,
|
|
544
|
+
startMailbox: JUNK_MAILBOX,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
await service.notSpam({
|
|
548
|
+
accountConfigId: ACCOUNT_CONFIG,
|
|
549
|
+
accountId: ACCOUNT,
|
|
550
|
+
messageId: MESSAGE_ID,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
assert.equal(addresses.size, 0);
|
|
554
|
+
assert.equal(messages.get(MESSAGE_ID)?.spamReport, undefined);
|
|
555
|
+
});
|
|
556
|
+
|
|
405
557
|
it("restores the original mailbox and clears the flag without setting trust", async () => {
|
|
406
558
|
const { service, messages, addresses } = buildWorld();
|
|
407
559
|
|
package/src/spam-report.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
IMessageRepository,
|
|
6
6
|
MessageItem,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
|
+
import { deriveAddressId } from "@remit/data-ports/id";
|
|
8
9
|
import {
|
|
9
10
|
AddressRole,
|
|
10
11
|
MessageKeywordFlag,
|
|
@@ -33,6 +34,22 @@ export class MoveNotSettledError extends Error {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The account advertises no Junk folder and has none under any conventional
|
|
39
|
+
* name, so there is nowhere to file the report. A designed, user-facing
|
|
40
|
+
* outcome — the user has to create the folder, and no amount of retrying
|
|
41
|
+
* changes that — so it carries its own text through the bulk handler's
|
|
42
|
+
* allowlist rather than being flattened to the generic retry copy.
|
|
43
|
+
*/
|
|
44
|
+
export class NoJunkMailboxError extends Error {
|
|
45
|
+
constructor() {
|
|
46
|
+
super(
|
|
47
|
+
"This account has no Junk folder. Create one named Junk or Spam in your mail provider, then report this message again.",
|
|
48
|
+
);
|
|
49
|
+
this.name = "NoJunkMailboxError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
const noopLogger: SpamReportLogger = {
|
|
37
54
|
info: () => {},
|
|
38
55
|
error: () => {},
|
|
@@ -86,9 +103,20 @@ export class SpamReportService {
|
|
|
86
103
|
config.moveSettlePollMs ?? DEFAULT_MOVE_SETTLE_POLL_MS;
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The sender to act on. `addressId` is derived rather than read off the
|
|
108
|
+
* envelope row so it always names the row `ensureSenderAddress` writes and
|
|
109
|
+
* the harvester writes — one derivation, one row, whatever the envelope
|
|
110
|
+
* happens to carry.
|
|
111
|
+
*/
|
|
89
112
|
private resolveFromAddress = async (
|
|
113
|
+
accountConfigId: string,
|
|
90
114
|
messageId: string,
|
|
91
|
-
): Promise<{
|
|
115
|
+
): Promise<{
|
|
116
|
+
addressId: string;
|
|
117
|
+
normalizedEmail: string;
|
|
118
|
+
displayName: string;
|
|
119
|
+
}> => {
|
|
92
120
|
const description = await this.messageService.describe(messageId);
|
|
93
121
|
const from = description.envelopeAddress.find(
|
|
94
122
|
(a) => a.addressRole === AddressRole.From,
|
|
@@ -96,7 +124,65 @@ export class SpamReportService {
|
|
|
96
124
|
if (!from) {
|
|
97
125
|
throw new Error(`Message ${messageId} has no From address to act on`);
|
|
98
126
|
}
|
|
99
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
addressId: deriveAddressId(accountConfigId, from.normalizedEmail),
|
|
129
|
+
normalizedEmail: from.normalizedEmail,
|
|
130
|
+
displayName: from.displayName ?? "",
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The list form answers `[]` for a row that is not there, where the single
|
|
136
|
+
* form throws — absence is a case both callers handle, not a fault.
|
|
137
|
+
*/
|
|
138
|
+
private senderIsKnown = async (
|
|
139
|
+
accountConfigId: string,
|
|
140
|
+
addressId: string,
|
|
141
|
+
): Promise<boolean> => {
|
|
142
|
+
const [known] = await this.addressService.getAddress(accountConfigId, [
|
|
143
|
+
addressId,
|
|
144
|
+
]);
|
|
145
|
+
return known !== undefined;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Blocking a sender owns the row it writes to. Harvesting is what ordinarily
|
|
150
|
+
* creates it, but a message whose harvest never landed — or whose row was
|
|
151
|
+
* taken by a cascade — still has a sender the user is entitled to block, and
|
|
152
|
+
* a missing row used to abort report-spam before the message ever reached
|
|
153
|
+
* Junk. The id is the harvester's own derivation, so this converges on the
|
|
154
|
+
* one row rather than minting a parallel one.
|
|
155
|
+
*
|
|
156
|
+
* A row that already exists is left exactly as it stands. Reporting spam is
|
|
157
|
+
* evidence about the sender, never about the account's standing record of
|
|
158
|
+
* them: an upsert here would carry whatever the upsert path does on
|
|
159
|
+
* conflict — today a display name chosen by the spammer, and any flag a
|
|
160
|
+
* later sighting-clearing rule hangs off it — into an operation that means
|
|
161
|
+
* the opposite.
|
|
162
|
+
*/
|
|
163
|
+
private ensureSenderAddress = async (
|
|
164
|
+
accountConfigId: string,
|
|
165
|
+
from: { addressId: string; normalizedEmail: string; displayName: string },
|
|
166
|
+
): Promise<void> => {
|
|
167
|
+
const separator = from.normalizedEmail.lastIndexOf("@");
|
|
168
|
+
const localPart = from.normalizedEmail.slice(0, separator);
|
|
169
|
+
const domain = from.normalizedEmail.slice(separator + 1);
|
|
170
|
+
if (separator < 0 || localPart.length === 0 || domain.length === 0) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Sender address ${from.normalizedEmail} is not a usable email address`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (await this.senderIsKnown(accountConfigId, from.addressId)) return;
|
|
176
|
+
await this.addressService.upsertAddress({
|
|
177
|
+
addressId: from.addressId,
|
|
178
|
+
accountConfigId,
|
|
179
|
+
localPart,
|
|
180
|
+
domain,
|
|
181
|
+
normalizedEmail: from.normalizedEmail,
|
|
182
|
+
normalizedCompound:
|
|
183
|
+
`${from.displayName.toLowerCase()} ${from.normalizedEmail}`.trim(),
|
|
184
|
+
displayName: from.displayName,
|
|
185
|
+
});
|
|
100
186
|
};
|
|
101
187
|
|
|
102
188
|
/**
|
|
@@ -128,12 +214,24 @@ export class SpamReportService {
|
|
|
128
214
|
const { accountConfigId, accountId, messageId, setBy } = params;
|
|
129
215
|
const now = Date.now();
|
|
130
216
|
|
|
131
|
-
|
|
217
|
+
// Resolved before anything is written: with nowhere to file the report,
|
|
218
|
+
// the operation cannot happen, and a blocked sender plus a `spamReport`
|
|
219
|
+
// stamp on a message still sitting in the inbox is a report that half
|
|
220
|
+
// happened. Fail here and the press changed nothing.
|
|
221
|
+
const junkMailbox =
|
|
222
|
+
await this.mailboxSpecialUseService.findJunkMailbox(accountId);
|
|
223
|
+
if (!junkMailbox) {
|
|
224
|
+
this.log.error({ accountId, messageId }, "No Junk mailbox for account");
|
|
225
|
+
throw new NoJunkMailboxError();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const from = await this.resolveFromAddress(accountConfigId, messageId);
|
|
132
229
|
const account = await this.accountService.get(accountId);
|
|
133
230
|
const isOwnAddress =
|
|
134
231
|
from.normalizedEmail.toLowerCase() === account.email.toLowerCase();
|
|
135
232
|
|
|
136
233
|
if (!isOwnAddress) {
|
|
234
|
+
await this.ensureSenderAddress(accountConfigId, from);
|
|
137
235
|
await this.addressService.mergeFlags(accountConfigId, from.addressId, {
|
|
138
236
|
blocked: { value: true, setAt: now, setBy },
|
|
139
237
|
});
|
|
@@ -153,12 +251,6 @@ export class SpamReportService {
|
|
|
153
251
|
spamReport: { reportedAt: now },
|
|
154
252
|
});
|
|
155
253
|
|
|
156
|
-
const junkMailbox =
|
|
157
|
-
await this.mailboxSpecialUseService.findJunkMailbox(accountId);
|
|
158
|
-
if (!junkMailbox) {
|
|
159
|
-
throw new Error(`No Junk mailbox found for account ${accountId}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
254
|
const alreadyInJunk = before.mailboxId === junkMailbox.mailboxId;
|
|
163
255
|
|
|
164
256
|
await this.messageMoveService.moveMessage(
|
|
@@ -200,10 +292,15 @@ export class SpamReportService {
|
|
|
200
292
|
notSpam = async (params: SpamReportParams): Promise<void> => {
|
|
201
293
|
const { accountConfigId, accountId, messageId } = params;
|
|
202
294
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
295
|
+
// Unblocking a sender the account has no row for is already true, and
|
|
296
|
+
// minting one here would put an address nothing ever harvested into the
|
|
297
|
+
// address book — the withdrawal of a judgement is not a sighting.
|
|
298
|
+
const from = await this.resolveFromAddress(accountConfigId, messageId);
|
|
299
|
+
if (await this.senderIsKnown(accountConfigId, from.addressId)) {
|
|
300
|
+
await this.addressService.mergeFlags(accountConfigId, from.addressId, {
|
|
301
|
+
blocked: null,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
207
304
|
|
|
208
305
|
const message = await this.messageService.get(messageId);
|
|
209
306
|
|