Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions apps/api/src/modules/mail/mail-delivery.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ export async function checkMailDelivery(exec: CommandExecutor): Promise<MailDeli
if (queue === null) {
return { ...base, ...unread, detail: firstLine(raw) };
}
return { ...base, ...queue, status: gradeDelivery(queue, relay) };
return {
...base,
...queue,
deferrals: topDeferrals(queue.deferrals),
status: gradeDelivery(queue, relay),
};
}

/** Which send hop the state file describes — pure, so the UI shape is testable. */
Expand Down Expand Up @@ -241,12 +246,16 @@ export function parseMailQueue(raw: string): MailQueueReading | null {
}
const deferrals: MailDeferral[] = [...tally.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, MAX_DEFERRALS)
.map(([reason, count]) => ({ kind: classifyReason(reason), count, reason }));

return { queued, sampled, deferrals };
}

/** The deferral rows we report. `gradeDelivery` reads the uncapped list. */
export function topDeferrals(deferrals: readonly MailDeferral[]): MailDeferral[] {
return deferrals.slice(0, MAX_DEFERRALS);
}

/**
* The verdict.
*
Expand Down
68 changes: 67 additions & 1 deletion apps/api/test/modules/mail/mail-delivery.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
describePath,
gradeDelivery,
parseMailQueue,
topDeferrals,
type MailQueueReading,
} from "../../../src/modules/mail/mail-delivery.service";
import { mailQueueProbeCommand } from "@repo/platform/engine/modules/mail/mail-engine";
Expand Down Expand Up @@ -51,6 +52,12 @@ const AUTH_REFUSAL =
"host email-smtp.us-east-1.amazonaws.com[203.0.113.9] said: 535 Authentication Credentials Invalid (in reply to AUTH LOGIN command)";
const GREYLIST_REFUSAL =
"host mx.receiver.example[198.51.100.4] said: 450 4.2.0 Greylisted, try again later (in reply to end of DATA command)";
const RELAY_TLS_REFUSAL =
"host email-smtp.us-east-1.amazonaws.com[203.0.113.9]: certificate verification failed";
const BUSY_RECEIVER = "host mx.b.example said: 451 4.3.0 Temporary local problem";
const FULL_MAILBOX = "host mx.c.example said: 452 4.2.2 Mailbox full";
const RELAY_NETWORK_REFUSAL =
"connect to email-smtp.us-east-1.amazonaws.com[203.0.113.9]:587: Connection timed out";

const RELAY: OutboundRelay = {
enabled: true,
Expand Down Expand Up @@ -147,7 +154,9 @@ describe("parseMailQueue", () => {
);

expect(parsed?.queued).toBe(6);
expect(parsed?.deferrals).toHaveLength(3);
const deferrals = parsed?.deferrals ?? [];
expect(deferrals).toHaveLength(6);
expect(topDeferrals(deferrals)).toEqual(deferrals.slice(0, 3));
});

/**
Expand Down Expand Up @@ -386,6 +395,63 @@ describe("checkMailDelivery", () => {
expect(health.deferrals[0]?.kind).toBe("auth");
});

const crowdedQueue = (last: string) =>
queueOutput([
{ reason: GREYLIST_REFUSAL },
{ reason: GREYLIST_REFUSAL },
{ reason: GREYLIST_REFUSAL },
{ reason: BUSY_RECEIVER },
{ reason: BUSY_RECEIVER },
{ reason: FULL_MAILBOX },
{ reason: FULL_MAILBOX },
{ reason: last },
]);

const crowdedRows = [GREYLIST_REFUSAL, BUSY_RECEIVER, FULL_MAILBOX];
const tryLater = (i: number) => `host mx${i}.example said: 451 try later`;

it.each([
["an auth refusal ranked fourth", RELAY, crowdedQueue(AUTH_REFUSAL), "fail", crowdedRows],
[
"a TLS failure at the smarthost ranked fourth",
RELAY,
crowdedQueue(RELAY_TLS_REFUSAL),
"fail",
crowdedRows,
],
[
"a connection failure at the smarthost ranked fourth",
RELAY,
crowdedQueue(RELAY_NETWORK_REFUSAL),
"fail",
crowdedRows,
],
[
"an auth refusal ranked thirteenth",
RELAY,
queueOutput([
...Array.from({ length: 24 }, (_, i) => ({ reason: tryLater(i % 12) })),
{ reason: AUTH_REFUSAL },
]),
"fail",
[tryLater(0), tryLater(1), tryLater(2)],
],
[
"an auth refusal queued after three one-off deferrals",
RELAY,
queueOutput(
[GREYLIST_REFUSAL, BUSY_RECEIVER, FULL_MAILBOX, AUTH_REFUSAL].map((reason) => ({ reason })),
),
"fail",
crowdedRows,
],
])("grades %s", async (_label, relay, queue, status, shown) => {
const health = await checkMailDelivery(box({ relay, queue }));

expect(health.status).toBe(status);
expect(health.deferrals.map((d) => d.reason)).toEqual(shown);
});

it("probes the queue through the engine, once", async () => {
const exec = box({});

Expand Down
Loading