Add email channel: IMAP IDLE inbound, SMTP outbound, thread matching

Inbound mail on support@top-sysops.ru creates/updates tickets in realtime
(IMAP IDLE, not polling). Replies thread via In-Reply-To/References against
stored Message-IDs, falling back to the customer's open ticket. Mailbox
credentials configured and encrypted via Settings -> Почта, same pattern as
the Telegram channel. TLS verification is opt-in-skippable per mailbox
(mail.top-sysops.ru's cert is currently expired; LAN-only server).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-07-26 13:01:21 +00:00
1 parent cf0b6cf58e
commit 7e62ed739b
18 files changed
+1739 -7

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
export const runtime = "nodejs";
import { NextResponse } from "next/server";
import { z } from "zod";
import { requireSession } from "@/lib/auth/require";
import { getMailboxStatus } from "@/lib/mail/config";
import { configureMailbox, disableMailListener } from "@/lib/mail/imap";
import { verifySmtp } from "@/lib/mail/smtp";
export async function GET() {
const { session, response } = await requireSession();
if (!session) return response;
return NextResponse.json(await getMailboxStatus());
}
const configureSchema = z.object({
host: z.string().min(1),
imapPort: z.coerce.number().int().positive(),
smtpPort: z.coerce.number().int().positive(),
user: z.string().email(),
password: z.string().min(1),
allowInsecureTls: z.boolean(),
});
export async function POST(request: Request) {
const { session, response } = await requireSession();
if (!session) return response;
const body = await request.json().catch(() => null);
const parsed = configureSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Invalid input" }, { status: 400 });
}
const settings = {
imapHost: parsed.data.host,
imapPort: parsed.data.imapPort,
smtpHost: parsed.data.host,
smtpPort: parsed.data.smtpPort,
user: parsed.data.user,
password: parsed.data.password,
allowInsecureTls: parsed.data.allowInsecureTls,
};
try {
await verifySmtp(settings);
await configureMailbox(settings);
return NextResponse.json({ ok: true });
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return NextResponse.json({ error: `Не удалось подключиться: ${message}` }, { status: 400 });
}
}
export async function DELETE() {
const { session, response } = await requireSession();
if (!session) return response;
await disableMailListener();
return NextResponse.json({ ok: true });
}