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"
|
||||
|
||||
455
backend/src/modules/email/email.sync.service.ts
Normal file
455
backend/src/modules/email/email.sync.service.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { and, desc, eq } from "drizzle-orm"
|
||||
import { ImapFlow } from "imapflow"
|
||||
import { simpleParser } from "mailparser"
|
||||
|
||||
import {
|
||||
emailAttachments,
|
||||
emailMailboxes,
|
||||
emailMessageBodies,
|
||||
emailMessages,
|
||||
emailSyncState,
|
||||
userCredentials,
|
||||
} from "../../../db/schema"
|
||||
import { decrypt } from "../../utils/crypt"
|
||||
|
||||
type EmailAddress = {
|
||||
name?: string | null
|
||||
address?: string | null
|
||||
}
|
||||
|
||||
type SyncOptions = {
|
||||
mailbox?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
type MailAccountConnection = {
|
||||
id: string
|
||||
tenantId: number
|
||||
userId: string
|
||||
email: string
|
||||
password: string
|
||||
imapHost: string
|
||||
imapPort: number
|
||||
imapSsl: boolean
|
||||
}
|
||||
|
||||
const decryptValue = (value: unknown) => value ? decrypt(value as any) : ""
|
||||
|
||||
const normalizeAddressList = (addresses: any): EmailAddress[] => {
|
||||
const value = Array.isArray(addresses?.value) ? addresses.value : []
|
||||
return value.map((item: any) => ({
|
||||
name: item.name || null,
|
||||
address: item.address || null,
|
||||
}))
|
||||
}
|
||||
|
||||
const previewText = (text?: string | false | null) => {
|
||||
if (!text) return null
|
||||
return text.replace(/\s+/g, " ").trim().slice(0, 240) || null
|
||||
}
|
||||
|
||||
const flagsFromMessage = (flags: Set<string> | string[] | undefined) => {
|
||||
if (!flags) return []
|
||||
return Array.isArray(flags) ? flags : Array.from(flags)
|
||||
}
|
||||
|
||||
const mailboxDisplayName = (path: string) => {
|
||||
const parts = path.split(/[/.]/).filter(Boolean)
|
||||
return parts[parts.length - 1] || path
|
||||
}
|
||||
|
||||
export function emailSyncService(server: FastifyInstance) {
|
||||
const getAccount = async (
|
||||
tenantId: number,
|
||||
userId: string,
|
||||
accountId: string,
|
||||
): Promise<MailAccountConnection | null> => {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(and(
|
||||
eq(userCredentials.id, accountId),
|
||||
eq(userCredentials.tenantId, tenantId),
|
||||
eq(userCredentials.userId, userId),
|
||||
eq(userCredentials.type, "mail"),
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return null
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
tenantId: row.tenantId,
|
||||
userId: row.userId,
|
||||
email: decryptValue(row.emailEncrypted),
|
||||
password: decryptValue(row.passwordEncrypted),
|
||||
imapHost: decryptValue(row.imapHostEncrypted),
|
||||
imapPort: Number(row.imapPort || 993),
|
||||
imapSsl: row.imapSsl !== false,
|
||||
}
|
||||
}
|
||||
|
||||
const createClient = (account: MailAccountConnection) => new ImapFlow({
|
||||
host: account.imapHost,
|
||||
port: account.imapPort,
|
||||
secure: account.imapSsl,
|
||||
auth: {
|
||||
user: account.email,
|
||||
pass: account.password,
|
||||
},
|
||||
logger: false,
|
||||
})
|
||||
|
||||
const upsertMailbox = async (
|
||||
account: MailAccountConnection,
|
||||
mailbox: any,
|
||||
status?: { exists?: number; unseen?: number },
|
||||
) => {
|
||||
const path = mailbox.path || mailbox.name
|
||||
const [saved] = await server.db
|
||||
.insert(emailMailboxes)
|
||||
.values({
|
||||
tenantId: account.tenantId,
|
||||
userId: account.userId,
|
||||
accountId: account.id,
|
||||
path,
|
||||
delimiter: mailbox.delimiter || null,
|
||||
name: mailbox.name || mailboxDisplayName(path),
|
||||
specialUse: mailbox.specialUse || null,
|
||||
flags: flagsFromMessage(mailbox.flags),
|
||||
exists: status?.exists || 0,
|
||||
unseen: status?.unseen || 0,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [emailMailboxes.accountId, emailMailboxes.path],
|
||||
set: {
|
||||
delimiter: mailbox.delimiter || null,
|
||||
name: mailbox.name || mailboxDisplayName(path),
|
||||
specialUse: mailbox.specialUse || null,
|
||||
flags: flagsFromMessage(mailbox.flags),
|
||||
exists: status?.exists || 0,
|
||||
unseen: status?.unseen || 0,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
|
||||
return saved
|
||||
}
|
||||
|
||||
const syncMailboxes = async (account: MailAccountConnection, client: ImapFlow) => {
|
||||
const savedMailboxes = []
|
||||
|
||||
for await (const mailbox of await client.list()) {
|
||||
savedMailboxes.push(await upsertMailbox(account, mailbox))
|
||||
}
|
||||
|
||||
return savedMailboxes
|
||||
}
|
||||
|
||||
const loadSyncState = async (account: MailAccountConnection, mailbox: any) => {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(emailSyncState)
|
||||
.where(and(
|
||||
eq(emailSyncState.accountId, account.id),
|
||||
eq(emailSyncState.mailboxPath, mailbox.path),
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
return rows[0] || null
|
||||
}
|
||||
|
||||
const saveSyncState = async (
|
||||
account: MailAccountConnection,
|
||||
mailbox: any,
|
||||
highestUid: number,
|
||||
uidValidity?: number | null,
|
||||
syncError?: string | null,
|
||||
) => {
|
||||
await server.db
|
||||
.insert(emailSyncState)
|
||||
.values({
|
||||
tenantId: account.tenantId,
|
||||
userId: account.userId,
|
||||
accountId: account.id,
|
||||
mailboxId: mailbox.id,
|
||||
mailboxPath: mailbox.path,
|
||||
uidValidity: uidValidity || null,
|
||||
highestUid,
|
||||
lastSyncedAt: new Date(),
|
||||
syncError: syncError || null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [emailSyncState.accountId, emailSyncState.mailboxPath],
|
||||
set: {
|
||||
mailboxId: mailbox.id,
|
||||
uidValidity: uidValidity || null,
|
||||
highestUid,
|
||||
lastSyncedAt: new Date(),
|
||||
syncError: syncError || null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const storeMessage = async (
|
||||
account: MailAccountConnection,
|
||||
mailbox: any,
|
||||
message: any,
|
||||
) => {
|
||||
if (!message.source) return null
|
||||
|
||||
const parsed = await simpleParser(message.source)
|
||||
const flags = flagsFromMessage(message.flags)
|
||||
const receivedAt = parsed.date || message.envelope?.date || new Date()
|
||||
const threadId = parsed.inReplyTo || parsed.references?.[0] || parsed.messageId || message.emailId || null
|
||||
|
||||
const [saved] = await server.db
|
||||
.insert(emailMessages)
|
||||
.values({
|
||||
tenantId: account.tenantId,
|
||||
userId: account.userId,
|
||||
accountId: account.id,
|
||||
mailboxId: mailbox.id,
|
||||
mailboxPath: mailbox.path,
|
||||
uid: Number(message.uid),
|
||||
emailId: message.emailId || null,
|
||||
messageId: parsed.messageId || null,
|
||||
inReplyTo: parsed.inReplyTo || null,
|
||||
threadId,
|
||||
subject: parsed.subject || "(kein Betreff)",
|
||||
from: normalizeAddressList(parsed.from),
|
||||
to: normalizeAddressList(parsed.to),
|
||||
cc: normalizeAddressList(parsed.cc),
|
||||
bcc: normalizeAddressList(parsed.bcc),
|
||||
replyTo: normalizeAddressList(parsed.replyTo),
|
||||
preview: previewText(parsed.text),
|
||||
flags,
|
||||
seen: flags.includes("\\Seen"),
|
||||
flagged: flags.includes("\\Flagged"),
|
||||
hasAttachments: Boolean(parsed.attachments?.length),
|
||||
size: message.size || null,
|
||||
sentAt: parsed.date || null,
|
||||
receivedAt,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [emailMessages.mailboxId, emailMessages.uid],
|
||||
set: {
|
||||
emailId: message.emailId || null,
|
||||
messageId: parsed.messageId || null,
|
||||
inReplyTo: parsed.inReplyTo || null,
|
||||
threadId,
|
||||
subject: parsed.subject || "(kein Betreff)",
|
||||
from: normalizeAddressList(parsed.from),
|
||||
to: normalizeAddressList(parsed.to),
|
||||
cc: normalizeAddressList(parsed.cc),
|
||||
bcc: normalizeAddressList(parsed.bcc),
|
||||
replyTo: normalizeAddressList(parsed.replyTo),
|
||||
preview: previewText(parsed.text),
|
||||
flags,
|
||||
seen: flags.includes("\\Seen"),
|
||||
flagged: flags.includes("\\Flagged"),
|
||||
hasAttachments: Boolean(parsed.attachments?.length),
|
||||
size: message.size || null,
|
||||
sentAt: parsed.date || null,
|
||||
receivedAt,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
|
||||
await server.db
|
||||
.insert(emailMessageBodies)
|
||||
.values({
|
||||
messageId: saved.id,
|
||||
text: parsed.text || null,
|
||||
html: parsed.html || null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: emailMessageBodies.messageId,
|
||||
set: {
|
||||
text: parsed.text || null,
|
||||
html: parsed.html || null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
if (parsed.attachments?.length) {
|
||||
for (const attachment of parsed.attachments) {
|
||||
await server.db
|
||||
.insert(emailAttachments)
|
||||
.values({
|
||||
messageId: saved.id,
|
||||
filename: attachment.filename || null,
|
||||
contentType: attachment.contentType || null,
|
||||
contentId: attachment.contentId || null,
|
||||
disposition: attachment.contentDisposition || null,
|
||||
size: attachment.size || null,
|
||||
checksum: attachment.checksum || null,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
}
|
||||
|
||||
return saved
|
||||
}
|
||||
|
||||
const syncMailboxMessages = async (
|
||||
account: MailAccountConnection,
|
||||
client: ImapFlow,
|
||||
mailbox: any,
|
||||
limit: number,
|
||||
) => {
|
||||
const lock = await client.getMailboxLock(mailbox.path)
|
||||
let highestUid = 0
|
||||
|
||||
try {
|
||||
const opened: any = await client.mailboxOpen(mailbox.path)
|
||||
await upsertMailbox(account, mailbox, {
|
||||
exists: opened.exists || 0,
|
||||
unseen: opened.unseen || 0,
|
||||
})
|
||||
|
||||
const state = await loadSyncState(account, mailbox)
|
||||
const searchResult = await client.search({ all: true }, { uid: true })
|
||||
const allUids = Array.isArray(searchResult) ? searchResult : []
|
||||
const newUids = allUids
|
||||
.filter((uid: number) => !state?.highestUid || uid > state.highestUid)
|
||||
.slice(-limit)
|
||||
|
||||
highestUid = Math.max(state?.highestUid || 0, ...newUids, 0)
|
||||
|
||||
if (newUids.length) {
|
||||
for await (const message of client.fetch(newUids, {
|
||||
uid: true,
|
||||
envelope: true,
|
||||
flags: true,
|
||||
source: true,
|
||||
size: true,
|
||||
}, { uid: true })) {
|
||||
await storeMessage(account, mailbox, message)
|
||||
}
|
||||
}
|
||||
|
||||
await saveSyncState(account, mailbox, highestUid, Number(opened.uidValidity || 0))
|
||||
return { path: mailbox.path, fetched: newUids.length, highestUid }
|
||||
} catch (err: any) {
|
||||
await saveSyncState(account, mailbox, highestUid, null, err.message || "Sync fehlgeschlagen")
|
||||
throw err
|
||||
} finally {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
|
||||
const syncAccount = async (
|
||||
tenantId: number,
|
||||
userId: string,
|
||||
accountId: string,
|
||||
options: SyncOptions = {},
|
||||
) => {
|
||||
const account = await getAccount(tenantId, userId, accountId)
|
||||
if (!account) {
|
||||
throw new Error("E-Mail Konto nicht gefunden")
|
||||
}
|
||||
|
||||
const client = createClient(account)
|
||||
const limit = Math.min(Math.max(Number(options.limit || 50), 1), 200)
|
||||
|
||||
await client.connect()
|
||||
|
||||
try {
|
||||
const mailboxes = await syncMailboxes(account, client)
|
||||
const syncTargets = options.mailbox
|
||||
? mailboxes.filter((mailbox) => mailbox.path === options.mailbox)
|
||||
: mailboxes.filter((mailbox) => mailbox.specialUse === "\\Inbox" || mailbox.path.toUpperCase() === "INBOX")
|
||||
|
||||
const synced = []
|
||||
for (const mailbox of syncTargets.length ? syncTargets : mailboxes.slice(0, 1)) {
|
||||
synced.push(await syncMailboxMessages(account, client, mailbox, limit))
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
mailboxes: mailboxes.length,
|
||||
synced,
|
||||
}
|
||||
} finally {
|
||||
await client.logout().catch(() => client.close())
|
||||
}
|
||||
}
|
||||
|
||||
const listMailboxes = async (tenantId: number, userId: string, accountId: string) => {
|
||||
return await server.db
|
||||
.select()
|
||||
.from(emailMailboxes)
|
||||
.where(and(
|
||||
eq(emailMailboxes.tenantId, tenantId),
|
||||
eq(emailMailboxes.userId, userId),
|
||||
eq(emailMailboxes.accountId, accountId),
|
||||
))
|
||||
.orderBy(emailMailboxes.specialUse, emailMailboxes.name)
|
||||
}
|
||||
|
||||
const listMessages = async (
|
||||
tenantId: number,
|
||||
userId: string,
|
||||
accountId: string,
|
||||
mailboxPath = "INBOX",
|
||||
limit = 50,
|
||||
) => {
|
||||
return await server.db
|
||||
.select()
|
||||
.from(emailMessages)
|
||||
.where(and(
|
||||
eq(emailMessages.tenantId, tenantId),
|
||||
eq(emailMessages.userId, userId),
|
||||
eq(emailMessages.accountId, accountId),
|
||||
eq(emailMessages.mailboxPath, mailboxPath),
|
||||
))
|
||||
.orderBy(desc(emailMessages.receivedAt))
|
||||
.limit(Math.min(Math.max(Number(limit), 1), 200))
|
||||
}
|
||||
|
||||
const getMessage = async (tenantId: number, userId: string, messageId: string) => {
|
||||
const rows = await server.db
|
||||
.select({
|
||||
message: emailMessages,
|
||||
body: emailMessageBodies,
|
||||
})
|
||||
.from(emailMessages)
|
||||
.leftJoin(emailMessageBodies, eq(emailMessageBodies.messageId, emailMessages.id))
|
||||
.where(and(
|
||||
eq(emailMessages.tenantId, tenantId),
|
||||
eq(emailMessages.userId, userId),
|
||||
eq(emailMessages.id, messageId),
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
if (!rows[0]) return null
|
||||
|
||||
const attachments = await server.db
|
||||
.select()
|
||||
.from(emailAttachments)
|
||||
.where(eq(emailAttachments.messageId, messageId))
|
||||
|
||||
return {
|
||||
...rows[0].message,
|
||||
body: rows[0].body,
|
||||
attachments,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
syncAccount,
|
||||
listMailboxes,
|
||||
listMessages,
|
||||
getMessage,
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,53 @@
|
||||
import nodemailer from "nodemailer"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq } from "drizzle-orm"
|
||||
|
||||
import { sendMailAsUser } from "../utils/emailengine"
|
||||
import { encrypt, decrypt } from "../utils/crypt"
|
||||
import { userCredentials } from "../../db/schema"
|
||||
// Pfad ggf. anpassen
|
||||
import { emailSyncService } from "../modules/email/email.sync.service"
|
||||
|
||||
// @ts-ignore
|
||||
import MailComposer from "nodemailer/lib/mail-composer/index.js"
|
||||
import { ImapFlow } from "imapflow"
|
||||
|
||||
export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const emailSync = emailSyncService(server)
|
||||
|
||||
const encryptedValue = (value: unknown) => value ? decrypt(value as any) : null
|
||||
|
||||
const accountResponse = (row: any) => ({
|
||||
id: row.id,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
userId: row.userId,
|
||||
tenantId: row.tenantId,
|
||||
type: row.type,
|
||||
email: encryptedValue(row.emailEncrypted),
|
||||
smtpHost: encryptedValue(row.smtpHostEncrypted),
|
||||
smtpPort: row.smtpPort ? Number(row.smtpPort) : null,
|
||||
smtpSsl: row.smtpSsl,
|
||||
imapHost: encryptedValue(row.imapHostEncrypted),
|
||||
imapPort: row.imapPort ? Number(row.imapPort) : null,
|
||||
imapSsl: row.imapSsl,
|
||||
hasPassword: Boolean(row.passwordEncrypted),
|
||||
})
|
||||
|
||||
const accountCredentials = (row: any) => ({
|
||||
...accountResponse(row),
|
||||
password: encryptedValue(row.passwordEncrypted),
|
||||
})
|
||||
|
||||
const bodyValue = (body: any, camelKey: string, snakeKey: string) => body[camelKey] ?? body[snakeKey]
|
||||
|
||||
const accountWhere = (tenantId: number, userId: string, id?: string) => {
|
||||
const conditions = [
|
||||
eq(userCredentials.tenantId, tenantId),
|
||||
eq(userCredentials.userId, userId),
|
||||
eq(userCredentials.type, "mail"),
|
||||
]
|
||||
if (id) conditions.push(eq(userCredentials.id, id))
|
||||
return and(...conditions)
|
||||
}
|
||||
|
||||
|
||||
// ======================================================================
|
||||
@@ -28,34 +64,49 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const body = req.body as {
|
||||
email: string
|
||||
password: string
|
||||
smtp_host: string
|
||||
smtp_port: number
|
||||
smtp_ssl: boolean
|
||||
imap_host: string
|
||||
imap_port: number
|
||||
imap_ssl: boolean
|
||||
smtpHost?: string
|
||||
smtpPort?: number
|
||||
smtpSsl?: boolean
|
||||
imapHost?: string
|
||||
imapPort?: number
|
||||
imapSsl?: boolean
|
||||
smtp_host?: string
|
||||
smtp_port?: number
|
||||
smtp_ssl?: boolean
|
||||
imap_host?: string
|
||||
imap_port?: number
|
||||
imap_ssl?: boolean
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// UPDATE EXISTING
|
||||
// -----------------------------
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select({ id: userCredentials.id })
|
||||
.from(userCredentials)
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!rows[0]) return reply.code(404).send({ error: "Account not found" })
|
||||
|
||||
const saveData = {
|
||||
emailEncrypted: body.email ? encrypt(body.email) : undefined,
|
||||
passwordEncrypted: body.password ? encrypt(body.password) : undefined,
|
||||
smtpHostEncrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined,
|
||||
smtpPort: body.smtp_port,
|
||||
smtpSsl: body.smtp_ssl,
|
||||
imapHostEncrypted: body.imap_host ? encrypt(body.imap_host) : undefined,
|
||||
imapPort: body.imap_port,
|
||||
imapSsl: body.imap_ssl,
|
||||
smtpHostEncrypted: bodyValue(body, "smtpHost", "smtp_host") ? encrypt(bodyValue(body, "smtpHost", "smtp_host")) : undefined,
|
||||
smtpPort: bodyValue(body, "smtpPort", "smtp_port"),
|
||||
smtpSsl: bodyValue(body, "smtpSsl", "smtp_ssl"),
|
||||
imapHostEncrypted: bodyValue(body, "imapHost", "imap_host") ? encrypt(bodyValue(body, "imapHost", "imap_host")) : undefined,
|
||||
imapPort: bodyValue(body, "imapPort", "imap_port"),
|
||||
imapSsl: bodyValue(body, "imapSsl", "imap_ssl"),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
await server.db
|
||||
.update(userCredentials)
|
||||
//@ts-ignore
|
||||
.set(saveData)
|
||||
.where(eq(userCredentials.id, id))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, id))
|
||||
|
||||
return reply.send({ success: true })
|
||||
}
|
||||
@@ -71,13 +122,13 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
emailEncrypted: encrypt(body.email),
|
||||
passwordEncrypted: encrypt(body.password),
|
||||
|
||||
smtpHostEncrypted: encrypt(body.smtp_host),
|
||||
smtpPort: body.smtp_port,
|
||||
smtpSsl: body.smtp_ssl,
|
||||
smtpHostEncrypted: encrypt(bodyValue(body, "smtpHost", "smtp_host")),
|
||||
smtpPort: bodyValue(body, "smtpPort", "smtp_port"),
|
||||
smtpSsl: bodyValue(body, "smtpSsl", "smtp_ssl"),
|
||||
|
||||
imapHostEncrypted: encrypt(body.imap_host),
|
||||
imapPort: body.imap_port,
|
||||
imapSsl: body.imap_ssl,
|
||||
imapHostEncrypted: encrypt(bodyValue(body, "imapHost", "imap_host")),
|
||||
imapPort: bodyValue(body, "imapPort", "imap_port"),
|
||||
imapSsl: bodyValue(body, "imapSsl", "imap_ssl"),
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
@@ -110,24 +161,13 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.id, id))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, id))
|
||||
.limit(1)
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const returnData: any = {}
|
||||
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted")) {
|
||||
const cleanKey = key.replace("Encrypted", "")
|
||||
// @ts-ignore
|
||||
returnData[cleanKey] = decrypt(val as string)
|
||||
} else {
|
||||
returnData[key] = val
|
||||
}
|
||||
})
|
||||
|
||||
return reply.send(returnData)
|
||||
return reply.send(accountResponse(row))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -136,24 +176,9 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.tenantId, req.user.tenant_id))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id))
|
||||
|
||||
const accounts = rows.map(row => {
|
||||
const temp: any = {}
|
||||
console.log(row)
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
console.log(key,val)
|
||||
if (key.endsWith("Encrypted") && val) {
|
||||
// @ts-ignore
|
||||
temp[key.replace("Encrypted", "")] = decrypt(val)
|
||||
} else {
|
||||
temp[key] = val
|
||||
}
|
||||
})
|
||||
return temp
|
||||
})
|
||||
|
||||
return reply.send(accounts)
|
||||
return reply.send(rows.map(accountResponse))
|
||||
|
||||
} catch (err) {
|
||||
console.error("GET /email/accounts error:", err)
|
||||
@@ -183,21 +208,13 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.id, body.account))
|
||||
.where(accountWhere(req.user.tenant_id, req.user.user_id, body.account))
|
||||
.limit(1)
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return reply.code(404).send({ error: "Account not found" })
|
||||
|
||||
const accountData: any = {}
|
||||
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted") && val) {
|
||||
// @ts-ignore
|
||||
accountData[key.replace("Encrypted", "")] = decrypt(val as string)
|
||||
} else {
|
||||
accountData[key] = val
|
||||
}
|
||||
})
|
||||
const accountData = accountCredentials(row)
|
||||
|
||||
// -------------------------
|
||||
// SEND EMAIL VIA SMTP
|
||||
@@ -243,14 +260,31 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const mail = new MailComposer(message)
|
||||
const raw = await mail.compile().build()
|
||||
|
||||
let savedToSent = false
|
||||
for await (const mailbox of await imap.list()) {
|
||||
if (mailbox.specialUse === "\\Sent") {
|
||||
await imap.mailboxOpen(mailbox.path)
|
||||
await imap.append(mailbox.path, raw, ["\\Seen"])
|
||||
await imap.logout()
|
||||
savedToSent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!savedToSent) {
|
||||
const sentFallbacks = ["Sent", "Gesendet", "INBOX.Sent"]
|
||||
for (const path of sentFallbacks) {
|
||||
try {
|
||||
await imap.append(path, raw, ["\\Seen"])
|
||||
savedToSent = true
|
||||
break
|
||||
} catch (err) {
|
||||
// Fallback wird nur genutzt, wenn der Ordner existiert.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await imap.logout()
|
||||
|
||||
return reply.send({ success: true })
|
||||
|
||||
} catch (err) {
|
||||
@@ -259,4 +293,80 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
server.post("/email/accounts/:id/sync", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const body = (req.body || {}) as { mailbox?: string; limit?: number }
|
||||
|
||||
const result = await emailSync.syncAccount(
|
||||
req.user.tenant_id,
|
||||
req.user.user_id,
|
||||
id,
|
||||
body,
|
||||
)
|
||||
|
||||
return reply.send({ success: true, ...result })
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail Sync fehlgeschlagen" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/accounts/:id/mailboxes", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
return reply.send(await emailSync.listMailboxes(req.user.tenant_id, req.user.user_id, id))
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "Postfächer konnten nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/accounts/:id/messages", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const query = req.query as { mailbox?: string; limit?: string }
|
||||
|
||||
return reply.send(await emailSync.listMessages(
|
||||
req.user.tenant_id,
|
||||
req.user.user_id,
|
||||
id,
|
||||
query.mailbox || "INBOX",
|
||||
Number(query.limit || 50),
|
||||
))
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mails konnten nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
server.get("/email/messages/:id", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
const message = await emailSync.getMessage(req.user.tenant_id, req.user.user_id, id)
|
||||
if (!message) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
|
||||
|
||||
return reply.send(message)
|
||||
} catch (err: any) {
|
||||
req.log.error(err)
|
||||
return reply.code(500).send({ error: err.message || "E-Mail konnte nicht geladen werden" })
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user