diff --git a/backend/src/modules/email/email.sync.service.ts b/backend/src/modules/email/email.sync.service.ts
index aa4b4a2..e3223bc 100644
--- a/backend/src/modules/email/email.sync.service.ts
+++ b/backend/src/modules/email/email.sync.service.ts
@@ -301,6 +301,25 @@ export function emailSyncService(server: FastifyInstance) {
return saved
}
+ const updateCachedMessageFlags = async (
+ mailboxId: string,
+ uid: number,
+ flags: string[],
+ ) => {
+ await server.db
+ .update(emailMessages)
+ .set({
+ flags,
+ seen: flags.includes("\\Seen"),
+ flagged: flags.includes("\\Flagged"),
+ updatedAt: new Date(),
+ })
+ .where(and(
+ eq(emailMessages.mailboxId, mailboxId),
+ eq(emailMessages.uid, uid),
+ ))
+ }
+
const syncMailboxMessages = async (
account: MailAccountConnection,
client: ImapFlow,
@@ -323,9 +342,23 @@ export function emailSyncService(server: FastifyInstance) {
const newUids = allUids
.filter((uid: number) => !state?.highestUid || uid > state.highestUid)
.slice(-limit)
+ const flagSyncUids = allUids.slice(-limit)
highestUid = Math.max(state?.highestUid || 0, ...newUids, 0)
+ if (flagSyncUids.length) {
+ for await (const message of client.fetch(flagSyncUids, {
+ uid: true,
+ flags: true,
+ }, { uid: true })) {
+ await updateCachedMessageFlags(
+ mailbox.id,
+ Number(message.uid),
+ flagsFromMessage(message.flags),
+ )
+ }
+ }
+
if (newUids.length) {
for await (const message of client.fetch(newUids, {
uid: true,
@@ -385,6 +418,67 @@ export function emailSyncService(server: FastifyInstance) {
}
}
+ const setMessageSeen = async (
+ tenantId: number,
+ userId: string,
+ messageId: string,
+ seen: boolean,
+ ) => {
+ const rows = await server.db
+ .select()
+ .from(emailMessages)
+ .where(and(
+ eq(emailMessages.tenantId, tenantId),
+ eq(emailMessages.userId, userId),
+ eq(emailMessages.id, messageId),
+ ))
+ .limit(1)
+
+ const message = rows[0]
+ if (!message) return null
+
+ const account = await getAccount(tenantId, userId, message.accountId)
+ if (!account) {
+ throw new Error("E-Mail Konto nicht gefunden")
+ }
+
+ const client = createClient(account)
+ await client.connect()
+
+ try {
+ const lock = await client.getMailboxLock(message.mailboxPath)
+ try {
+ await client.mailboxOpen(message.mailboxPath)
+ if (seen) {
+ await client.messageFlagsAdd(message.uid, ["\\Seen"], { uid: true })
+ } else {
+ await client.messageFlagsRemove(message.uid, ["\\Seen"], { uid: true })
+ }
+ } finally {
+ lock.release()
+ }
+ } finally {
+ await client.logout().catch(() => client.close())
+ }
+
+ const currentFlags = Array.isArray(message.flags) ? message.flags : []
+ const nextFlags = seen
+ ? Array.from(new Set([...currentFlags, "\\Seen"]))
+ : currentFlags.filter((flag) => flag !== "\\Seen")
+
+ const [updated] = await server.db
+ .update(emailMessages)
+ .set({
+ flags: nextFlags,
+ seen,
+ updatedAt: new Date(),
+ })
+ .where(eq(emailMessages.id, messageId))
+ .returning()
+
+ return updated
+ }
+
const listMailboxes = async (tenantId: number, userId: string, accountId: string) => {
return await server.db
.select()
@@ -451,5 +545,6 @@ export function emailSyncService(server: FastifyInstance) {
listMailboxes,
listMessages,
getMessage,
+ setMessageSeen,
}
}
diff --git a/backend/src/routes/emailAsUser.ts b/backend/src/routes/emailAsUser.ts
index de6c29b..c11a3e2 100644
--- a/backend/src/routes/emailAsUser.ts
+++ b/backend/src/routes/emailAsUser.ts
@@ -369,4 +369,28 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
}
})
+ server.post("/email/messages/:id/read", 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 { seen?: boolean }
+ const message = await emailSync.setMessageSeen(
+ req.user.tenant_id,
+ req.user.user_id,
+ id,
+ body.seen !== false,
+ )
+
+ if (!message) return reply.code(404).send({ error: "E-Mail nicht gefunden" })
+
+ return reply.send({ success: true, message })
+ } catch (err: any) {
+ req.log.error(err)
+ return reply.code(500).send({ error: err.message || "Lesestatus konnte nicht synchronisiert werden" })
+ }
+ })
+
}
diff --git a/frontend/pages/email/index.vue b/frontend/pages/email/index.vue
index 9a51240..ebe4f9f 100644
--- a/frontend/pages/email/index.vue
+++ b/frontend/pages/email/index.vue
@@ -13,10 +13,16 @@ type EmailMailbox = {
id: string
path: string
name: string
+ delimiter?: string | null
specialUse?: string | null
unseen?: number
}
+type EmailMailboxNode = {
+ mailbox: EmailMailbox
+ children: EmailMailboxNode[]
+}
+
type EmailAddress = {
name?: string | null
address?: string | null
@@ -72,20 +78,80 @@ const selectedMailbox = computed(() =>
mailboxes.value.find((mailbox) => mailbox.path === selectedMailboxPath.value) || null
)
-const sortedMailboxes = computed(() => {
- const priority = (mailbox: EmailMailbox) => {
- if (mailbox.specialUse === "\\Inbox" || mailbox.path.toUpperCase() === "INBOX") return 0
- if (mailbox.specialUse === "\\Sent") return 1
- if (mailbox.specialUse === "\\Drafts") return 2
- if (mailbox.specialUse === "\\Archive") return 3
- if (mailbox.specialUse === "\\Junk") return 4
- if (mailbox.specialUse === "\\Trash") return 5
- return 9
+const mailboxPriority = (mailbox: EmailMailbox) => {
+ if (mailbox.specialUse === "\\Inbox" || mailbox.path.toUpperCase() === "INBOX") return 0
+ if (mailbox.specialUse === "\\Sent") return 1
+ if (mailbox.specialUse === "\\Drafts") return 2
+ if (mailbox.specialUse === "\\Archive") return 3
+ if (mailbox.specialUse === "\\Junk") return 4
+ if (mailbox.specialUse === "\\Trash") return 5
+ return 9
+}
+
+const mailboxDelimiter = (mailbox: EmailMailbox) => {
+ if (mailbox.delimiter) return mailbox.delimiter
+ if (mailbox.path.includes("/")) return "/"
+ if (mailbox.path.includes(".")) return "."
+ return "/"
+}
+
+const parentMailboxPath = (mailbox: EmailMailbox, mailboxPaths: Set