E-Mail Cache und Konto-Synchronisation vorbereiten
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.
This commit is contained in:
106
backend/db/migrations/0049_email_cache.sql
Normal file
106
backend/db/migrations/0049_email_cache.sql
Normal file
@@ -0,0 +1,106 @@
|
||||
CREATE TABLE "email_mailboxes" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tenant_id" bigint NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"path" text NOT NULL,
|
||||
"delimiter" text,
|
||||
"name" text NOT NULL,
|
||||
"special_use" text,
|
||||
"flags" jsonb,
|
||||
"exists" integer DEFAULT 0 NOT NULL,
|
||||
"unseen" integer DEFAULT 0 NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "email_messages" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tenant_id" bigint NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"mailbox_id" uuid NOT NULL,
|
||||
"mailbox_path" text NOT NULL,
|
||||
"uid" bigint NOT NULL,
|
||||
"email_id" text,
|
||||
"message_id" text,
|
||||
"in_reply_to" text,
|
||||
"thread_id" text,
|
||||
"subject" text,
|
||||
"from" jsonb,
|
||||
"to" jsonb,
|
||||
"cc" jsonb,
|
||||
"bcc" jsonb,
|
||||
"reply_to" jsonb,
|
||||
"preview" text,
|
||||
"flags" jsonb,
|
||||
"seen" boolean DEFAULT false NOT NULL,
|
||||
"flagged" boolean DEFAULT false NOT NULL,
|
||||
"has_attachments" boolean DEFAULT false NOT NULL,
|
||||
"size" bigint,
|
||||
"sent_at" timestamp with time zone,
|
||||
"received_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "email_message_bodies" (
|
||||
"message_id" uuid PRIMARY KEY NOT NULL,
|
||||
"text" text,
|
||||
"html" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "email_attachments" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"message_id" uuid NOT NULL,
|
||||
"filename" text,
|
||||
"content_type" text,
|
||||
"content_id" text,
|
||||
"disposition" text,
|
||||
"size" bigint,
|
||||
"checksum" text,
|
||||
"storage_key" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "email_sync_state" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tenant_id" bigint NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"account_id" uuid NOT NULL,
|
||||
"mailbox_id" uuid NOT NULL,
|
||||
"mailbox_path" text NOT NULL,
|
||||
"uid_validity" bigint,
|
||||
"highest_uid" bigint DEFAULT 0 NOT NULL,
|
||||
"mod_seq" text,
|
||||
"last_synced_at" timestamp with time zone,
|
||||
"sync_error" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "email_mailboxes" ADD CONSTRAINT "email_mailboxes_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_mailboxes" ADD CONSTRAINT "email_mailboxes_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_mailboxes" ADD CONSTRAINT "email_mailboxes_account_id_user_credentials_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."user_credentials"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_account_id_user_credentials_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."user_credentials"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_mailbox_id_email_mailboxes_id_fk" FOREIGN KEY ("mailbox_id") REFERENCES "public"."email_mailboxes"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_message_bodies" ADD CONSTRAINT "email_message_bodies_message_id_email_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."email_messages"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_attachments" ADD CONSTRAINT "email_attachments_message_id_email_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."email_messages"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_account_id_user_credentials_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."user_credentials"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_mailbox_id_email_mailboxes_id_fk" FOREIGN KEY ("mailbox_id") REFERENCES "public"."email_mailboxes"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "email_mailboxes_account_path_key" ON "email_mailboxes" USING btree ("account_id","path");--> statement-breakpoint
|
||||
CREATE INDEX "email_mailboxes_tenant_account_idx" ON "email_mailboxes" USING btree ("tenant_id","account_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "email_messages_mailbox_uid_key" ON "email_messages" USING btree ("mailbox_id","uid");--> statement-breakpoint
|
||||
CREATE INDEX "email_messages_account_mailbox_idx" ON "email_messages" USING btree ("account_id","mailbox_path");--> statement-breakpoint
|
||||
CREATE INDEX "email_messages_received_idx" ON "email_messages" USING btree ("received_at");--> statement-breakpoint
|
||||
CREATE INDEX "email_messages_message_id_idx" ON "email_messages" USING btree ("message_id");--> statement-breakpoint
|
||||
CREATE INDEX "email_messages_thread_idx" ON "email_messages" USING btree ("thread_id");--> statement-breakpoint
|
||||
CREATE INDEX "email_attachments_message_idx" ON "email_attachments" USING btree ("message_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "email_sync_state_mailbox_key" ON "email_sync_state" USING btree ("account_id","mailbox_path");--> statement-breakpoint
|
||||
CREATE INDEX "email_sync_state_tenant_account_idx" ON "email_sync_state" USING btree ("tenant_id","account_id");
|
||||
208
backend/db/schema/emails.ts
Normal file
208
backend/db/schema/emails.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
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
|
||||
@@ -27,6 +27,7 @@ export * from "./customerspaces"
|
||||
export * from "./customerinventoryitems"
|
||||
export * from "./devices"
|
||||
export * from "./documentboxes"
|
||||
export * from "./emails"
|
||||
export * from "./enums"
|
||||
export * from "./events"
|
||||
export * from "./entitybankaccounts"
|
||||
|
||||
Reference in New Issue
Block a user