import { pgTable, text, timestamp, index } from 'drizzle-orm/pg-core'; import { ports } from './ports'; /** * Bounce-monitoring storage. The IMAP poller writes one row per parsed * DSN (Delivery Status Notification) it finds in a monitored sender's * inbox. Consumers (admin/bounces page, notifications worker, UI badges * on document_sends rows) join in via `originalSendType` / * `originalSendId`. */ export const emailBounces = pgTable( 'email_bounces', { id: text('id') .primaryKey() .$defaultFn(() => crypto.randomUUID()), portId: text('port_id') .notNull() .references(() => ports.id, { onDelete: 'cascade' }), /** The mailbox we polled to find this bounce. */ mailboxAddress: text('mailbox_address').notNull(), /** The address that bounced (the original recipient). */ bouncedAddress: text('bounced_address').notNull(), /** One of `document_send` / `notification` / `email_thread` / `null`. */ originalSendType: text('original_send_type'), /** The id of the original send row, when resolvable. */ originalSendId: text('original_send_id'), /** RFC 3464 DSN fields. Null when the upstream provider uses a * non-RFC bounce format (some providers send HTML-only bounces). */ dsnAction: text('dsn_action'), dsnStatus: text('dsn_status'), dsnDiagnostic: text('dsn_diagnostic'), receivedAt: timestamp('received_at', { withTimezone: true }).notNull().defaultNow(), /** Full raw message for forensics. Trimmed to ~32KB before insert. */ rawMessage: text('raw_message'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ index('idx_email_bounces_port_received').on(table.portId, table.receivedAt), index('idx_email_bounces_bounced_address').on(table.bouncedAddress), index('idx_email_bounces_original_send').on(table.originalSendType, table.originalSendId), ], ); export type EmailBounce = typeof emailBounces.$inferSelect; export type NewEmailBounce = typeof emailBounces.$inferInsert;