Fix dark-mode email readability, silent reply failures, and add an IMAP connection watchdog

Dark theme: inbound email HTML intentionally keeps its own inline colors
for fidelity (sanitize-html.ts), but a sender's hardcoded dark text (the
common case) was unreadable once the surrounding chrome went dark. Now
forced to the theme's own text color in dark mode only; light mode keeps
full fidelity as before.

Send button: deliverAgentMessage awaited the outbound email/Telegram send
*before* recording the reply, so a broken mail/bot account (e.g. bad SMTP
creds) made the reply vanish entirely — not sent, not saved, no error
shown — with the API's catch-all masking the real cause as a generic
"Ticket not found". Channel delivery is now best-effort: the reply is
always recorded even if outbound delivery fails, and real errors surface
to the compose box instead of failing silently.

IMAP watchdog: a live IDLE connection has gone quietly unresponsive in
production more than once, with mail landing server-side but never
noticed here and no error ever firing. Added a periodic forced NOOP
round-trip with its own deadline, independent of ImapFlow's internal
socket timeout, that tears down and reconnects a connection that stops
answering.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-10-07 07:24:14 +00:00
1 parent 93e17c37c7
commit 8d1f91187a
7 files changed
+109 -31

No files matched your search

+32 -17
View File
@@ -49,19 +49,30 @@ export async function deliverAgentMessage(params: {
});
}
// Channel delivery (email/Telegram) is best-effort: the agent's reply is
// the thing that must never be lost. Previously an SMTP/Telegram failure
// here threw straight out of this function, so recordAgentReply below was
// never reached — the reply vanished entirely (not recorded, nothing
// shown in the thread) and the API route's catch-all turned it into a
// misleading "Ticket not found". A broken mail/bot account should degrade
// to "reply saved internally, not delivered" — never silent data loss.
let emailMessageId: string | undefined;
if (ticket.channel === "email") {
const context = await getEmailThreadContext(params.ticketId);
if (context) {
emailMessageId = await sendTicketReplyEmail({
to: context.customerEmail,
subject: context.subject,
body: params.body,
inReplyTo: context.inReplyTo,
attachment: params.attachment
? { filename: params.attachment.filename, content: params.attachment.buffer }
: undefined,
});
try {
emailMessageId = await sendTicketReplyEmail({
to: context.customerEmail,
subject: context.subject,
body: params.body,
inReplyTo: context.inReplyTo,
attachment: params.attachment
? { filename: params.attachment.filename, content: params.attachment.buffer }
: undefined,
});
} catch (err) {
console.error(`Failed to send reply email for ticket ${params.ticketId}:`, err);
}
}
}
@@ -79,14 +90,18 @@ export async function deliverAgentMessage(params: {
where: eq(customers.id, result.ticket.customerId),
});
if (customer?.telegramChatId) {
if (params.attachment) {
await sendTelegramFile(
customer.telegramChatId,
{ buffer: params.attachment.buffer, filename: params.attachment.filename, mimeType: params.attachment.mimeType },
params.body,
);
} else {
await sendTelegramMessage(customer.telegramChatId, params.body);
try {
if (params.attachment) {
await sendTelegramFile(
customer.telegramChatId,
{ buffer: params.attachment.buffer, filename: params.attachment.filename, mimeType: params.attachment.mimeType },
params.body,
);
} else {
await sendTelegramMessage(customer.telegramChatId, params.body);
}
} catch (err) {
console.error(`Failed to deliver reply via Telegram for ticket ${params.ticketId}:`, err);
}
}
}