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:
1 parent
cf0b6cf58e
commit
7e62ed739b
18 files changed
+1739
-7
No files matched your search
+24
-1
@@ -63,7 +63,7 @@ export const tickets = sqliteTable("tickets", {
|
||||
priority: text("priority", { enum: ["low", "normal", "high", "urgent"] })
|
||||
.notNull()
|
||||
.default("normal"),
|
||||
channel: text("channel", { enum: ["telegram", "portal", "manual"] }).notNull(),
|
||||
channel: text("channel", { enum: ["telegram", "portal", "manual", "email"] }).notNull(),
|
||||
customerId: text("customer_id")
|
||||
.notNull()
|
||||
.references(() => customers.id, { onDelete: "cascade" }),
|
||||
@@ -85,6 +85,10 @@ export const messages = sqliteTable("messages", {
|
||||
authorName: text("author_name").notNull(),
|
||||
body: text("body").notNull(),
|
||||
direction: text("direction", { enum: ["in", "out"] }).notNull(),
|
||||
// RFC 5322 Message-ID of this email, when the message came in/out over the
|
||||
// email channel — lets a customer's future reply be matched back to the
|
||||
// exact ticket via In-Reply-To/References, even if they have other tickets.
|
||||
emailMessageId: text("email_message_id"),
|
||||
createdAt: timestamps.createdAt,
|
||||
});
|
||||
|
||||
@@ -97,3 +101,22 @@ export const telegramConfig = sqliteTable("telegram_config", {
|
||||
verifiedAt: integer("verified_at", { mode: "timestamp_ms" }),
|
||||
createdAt: timestamps.createdAt,
|
||||
});
|
||||
|
||||
/** Single-row-ish config for the email channel (one support mailbox per deployment). */
|
||||
export const mailboxConfig = sqliteTable("mailbox_config", {
|
||||
id: id(),
|
||||
imapHost: text("imap_host").notNull(),
|
||||
imapPort: integer("imap_port").notNull().default(993),
|
||||
smtpHost: text("smtp_host").notNull(),
|
||||
smtpPort: integer("smtp_port").notNull().default(587),
|
||||
user: text("user").notNull(),
|
||||
passwordEnc: text("password_enc").notNull(),
|
||||
// Both IMAP and SMTP presented an expired cert on support@top-sysops.ru at
|
||||
// setup time. This is a LAN-only mailbox (never exposed to the internet),
|
||||
// so skipping verification is an accepted risk — not a default for
|
||||
// arbitrary/public mail servers. Revisit once the cert is renewed.
|
||||
allowInsecureTls: integer("allow_insecure_tls", { mode: "boolean" }).notNull().default(false),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
|
||||
verifiedAt: integer("verified_at", { mode: "timestamp_ms" }),
|
||||
createdAt: timestamps.createdAt,
|
||||
});
|
||||
Reference in new issue
Block a user