KI-AGENT: Ergänzt Tabellen für lokalen E-Mail-Cache, IMAP-Sync-Service und Inbox-API. Überarbeitet außerdem die E-Mail-Konto-Seiten mit sicherer Passwortbehandlung und manuellem Sync.
209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
import {
|
|
bigint,
|
|
boolean,
|
|
index,
|
|
integer,
|
|
jsonb,
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
uniqueIndex,
|
|
uuid,
|
|
} from "drizzle-orm/pg-core"
|
|
|
|
import { tenants } from "./tenants"
|
|
import { authUsers } from "./auth_users"
|
|
import { userCredentials } from "./user_credentials"
|
|
|
|
export const emailMailboxes = pgTable(
|
|
"email_mailboxes",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
|
|
tenantId: bigint("tenant_id", { mode: "number" })
|
|
.notNull()
|
|
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
userId: uuid("user_id")
|
|
.notNull()
|
|
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
accountId: uuid("account_id")
|
|
.notNull()
|
|
.references(() => userCredentials.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
path: text("path").notNull(),
|
|
delimiter: text("delimiter"),
|
|
name: text("name").notNull(),
|
|
specialUse: text("special_use"),
|
|
flags: jsonb("flags").$type<string[]>(),
|
|
exists: integer("exists").notNull().default(0),
|
|
unseen: integer("unseen").notNull().default(0),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
|
},
|
|
(table) => ({
|
|
accountPathKey: uniqueIndex("email_mailboxes_account_path_key")
|
|
.on(table.accountId, table.path),
|
|
tenantAccountIdx: index("email_mailboxes_tenant_account_idx")
|
|
.on(table.tenantId, table.accountId),
|
|
}),
|
|
)
|
|
|
|
export const emailMessages = pgTable(
|
|
"email_messages",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
|
|
tenantId: bigint("tenant_id", { mode: "number" })
|
|
.notNull()
|
|
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
userId: uuid("user_id")
|
|
.notNull()
|
|
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
accountId: uuid("account_id")
|
|
.notNull()
|
|
.references(() => userCredentials.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
mailboxId: uuid("mailbox_id")
|
|
.notNull()
|
|
.references(() => emailMailboxes.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
mailboxPath: text("mailbox_path").notNull(),
|
|
uid: bigint("uid", { mode: "number" }).notNull(),
|
|
emailId: text("email_id"),
|
|
messageId: text("message_id"),
|
|
inReplyTo: text("in_reply_to"),
|
|
threadId: text("thread_id"),
|
|
subject: text("subject"),
|
|
from: jsonb("from").$type<Array<{ name?: string | null; address?: string | null }>>(),
|
|
to: jsonb("to").$type<Array<{ name?: string | null; address?: string | null }>>(),
|
|
cc: jsonb("cc").$type<Array<{ name?: string | null; address?: string | null }>>(),
|
|
bcc: jsonb("bcc").$type<Array<{ name?: string | null; address?: string | null }>>(),
|
|
replyTo: jsonb("reply_to").$type<Array<{ name?: string | null; address?: string | null }>>(),
|
|
preview: text("preview"),
|
|
flags: jsonb("flags").$type<string[]>(),
|
|
seen: boolean("seen").notNull().default(false),
|
|
flagged: boolean("flagged").notNull().default(false),
|
|
hasAttachments: boolean("has_attachments").notNull().default(false),
|
|
size: bigint("size", { mode: "number" }),
|
|
sentAt: timestamp("sent_at", { withTimezone: true }),
|
|
receivedAt: timestamp("received_at", { withTimezone: true }),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
|
},
|
|
(table) => ({
|
|
mailboxUidKey: uniqueIndex("email_messages_mailbox_uid_key")
|
|
.on(table.mailboxId, table.uid),
|
|
accountMailboxIdx: index("email_messages_account_mailbox_idx")
|
|
.on(table.accountId, table.mailboxPath),
|
|
receivedIdx: index("email_messages_received_idx")
|
|
.on(table.receivedAt),
|
|
messageIdIdx: index("email_messages_message_id_idx")
|
|
.on(table.messageId),
|
|
threadIdx: index("email_messages_thread_idx")
|
|
.on(table.threadId),
|
|
}),
|
|
)
|
|
|
|
export const emailMessageBodies = pgTable("email_message_bodies", {
|
|
messageId: uuid("message_id")
|
|
.primaryKey()
|
|
.references(() => emailMessages.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
text: text("text"),
|
|
html: text("html"),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
|
})
|
|
|
|
export const emailAttachments = pgTable(
|
|
"email_attachments",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
|
|
messageId: uuid("message_id")
|
|
.notNull()
|
|
.references(() => emailMessages.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
filename: text("filename"),
|
|
contentType: text("content_type"),
|
|
contentId: text("content_id"),
|
|
disposition: text("disposition"),
|
|
size: bigint("size", { mode: "number" }),
|
|
checksum: text("checksum"),
|
|
storageKey: text("storage_key"),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
},
|
|
(table) => ({
|
|
messageIdx: index("email_attachments_message_idx")
|
|
.on(table.messageId),
|
|
}),
|
|
)
|
|
|
|
export const emailSyncState = pgTable(
|
|
"email_sync_state",
|
|
{
|
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
|
|
tenantId: bigint("tenant_id", { mode: "number" })
|
|
.notNull()
|
|
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
userId: uuid("user_id")
|
|
.notNull()
|
|
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
accountId: uuid("account_id")
|
|
.notNull()
|
|
.references(() => userCredentials.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
mailboxId: uuid("mailbox_id")
|
|
.notNull()
|
|
.references(() => emailMailboxes.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
|
|
|
mailboxPath: text("mailbox_path").notNull(),
|
|
uidValidity: bigint("uid_validity", { mode: "number" }),
|
|
highestUid: bigint("highest_uid", { mode: "number" }).notNull().default(0),
|
|
modSeq: text("mod_seq"),
|
|
lastSyncedAt: timestamp("last_synced_at", { withTimezone: true }),
|
|
syncError: text("sync_error"),
|
|
|
|
createdAt: timestamp("created_at", { withTimezone: true })
|
|
.notNull()
|
|
.defaultNow(),
|
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
|
},
|
|
(table) => ({
|
|
mailboxKey: uniqueIndex("email_sync_state_mailbox_key")
|
|
.on(table.accountId, table.mailboxPath),
|
|
tenantAccountIdx: index("email_sync_state_tenant_account_idx")
|
|
.on(table.tenantId, table.accountId),
|
|
}),
|
|
)
|
|
|
|
export type EmailMailbox = typeof emailMailboxes.$inferSelect
|
|
export type NewEmailMailbox = typeof emailMailboxes.$inferInsert
|
|
export type EmailMessage = typeof emailMessages.$inferSelect
|
|
export type NewEmailMessage = typeof emailMessages.$inferInsert
|
|
export type EmailMessageBody = typeof emailMessageBodies.$inferSelect
|
|
export type NewEmailMessageBody = typeof emailMessageBodies.$inferInsert
|
|
export type EmailAttachment = typeof emailAttachments.$inferSelect
|
|
export type NewEmailAttachment = typeof emailAttachments.$inferInsert
|
|
export type EmailSyncState = typeof emailSyncState.$inferSelect
|
|
export type NewEmailSyncState = typeof emailSyncState.$inferInsert
|