From 2f306ca8a233f52553c8fcde266241925e24a562 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Mon, 7 Sep 2026 19:31:53 +0200 Subject: [PATCH] =?UTF-8?q?KI-AGENT:=20Defekte=20Mailkonto-Verschl=C3=BCss?= =?UTF-8?q?elung=20abfangen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/emailAsUser.ts | 75 ++++++++++++++----- frontend/pages/email/index.vue | 4 +- frontend/pages/email/new.vue | 2 +- .../settings/emailaccounts/[mode]/[[id]].vue | 27 ++++++- .../pages/settings/emailaccounts/index.vue | 6 +- 5 files changed, 87 insertions(+), 27 deletions(-) diff --git a/backend/src/routes/emailAsUser.ts b/backend/src/routes/emailAsUser.ts index ff68692..017c193 100644 --- a/backend/src/routes/emailAsUser.ts +++ b/backend/src/routes/emailAsUser.ts @@ -23,27 +23,62 @@ export default async function emailAsUserRoutes(server: FastifyInstance) { 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 accountResponse = (row: any) => { + const invalidEncryptedFields: string[] = [] + const safeEncryptedValue = (value: unknown, field: string) => { + if (!value) return null + try { + return encryptedValue(value) + } catch { + invalidEncryptedFields.push(field) + return null + } + } - const accountCredentials = (row: any) => ({ - ...accountResponse(row), - password: encryptedValue(row.passwordEncrypted), - }) + const email = safeEncryptedValue(row.emailEncrypted, "email") + const smtpHost = safeEncryptedValue(row.smtpHostEncrypted, "smtpHost") + const imapHost = safeEncryptedValue(row.imapHostEncrypted, "imapHost") + safeEncryptedValue(row.passwordEncrypted, "password") + + if (invalidEncryptedFields.length) { + server.log.warn({ + accountId: row.id, + invalidEncryptedFields, + }, "E-Mail-Kontodaten können mit dem aktuellen ENCRYPTION_KEY nicht entschlüsselt werden") + } + + return { + id: row.id, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + userId: row.userId, + tenantId: row.tenantId, + type: row.type, + email, + displayName: email || `Mailkonto ${String(row.id).slice(0, 8)} muss repariert werden`, + smtpHost, + smtpPort: row.smtpPort ? Number(row.smtpPort) : null, + smtpSsl: row.smtpSsl, + imapHost, + imapPort: row.imapPort ? Number(row.imapPort) : null, + imapSsl: row.imapSsl, + hasPassword: Boolean(row.passwordEncrypted), + credentialsReadable: invalidEncryptedFields.length === 0, + invalidEncryptedFields, + } + } + + const accountCredentials = (row: any) => { + const account = accountResponse(row) + if (!account.credentialsReadable) { + throw new Error("Die verschlüsselten Kontodaten sind nicht lesbar. Bitte das E-Mail-Konto in den Einstellungen vollständig neu speichern.") + } + + return { + ...account, + password: encryptedValue(row.passwordEncrypted), + } + } const bodyValue = (body: any, camelKey: string, snakeKey: string) => body[camelKey] ?? body[snakeKey] diff --git a/frontend/pages/email/index.vue b/frontend/pages/email/index.vue index 568f2cd..cca6da7 100644 --- a/frontend/pages/email/index.vue +++ b/frontend/pages/email/index.vue @@ -5,6 +5,8 @@ import { de as deLocale } from "date-fns/locale" type EmailAccount = { id: string email: string + displayName?: string + credentialsReadable?: boolean imapHost?: string | null hasPassword?: boolean } @@ -657,7 +659,7 @@ onMounted(loadAccounts) v-else-if="accounts.length" v-model="selectedAccountId" :items="accounts" - label-key="email" + label-key="displayName" value-key="id" class="w-full" /> diff --git a/frontend/pages/email/new.vue b/frontend/pages/email/new.vue index 9651398..9e66b48 100644 --- a/frontend/pages/email/new.vue +++ b/frontend/pages/email/new.vue @@ -302,7 +302,7 @@ const sendEmail = async () => { > diff --git a/frontend/pages/settings/emailaccounts/[mode]/[[id]].vue b/frontend/pages/settings/emailaccounts/[mode]/[[id]].vue index 3039448..e623f33 100644 --- a/frontend/pages/settings/emailaccounts/[mode]/[[id]].vue +++ b/frontend/pages/settings/emailaccounts/[mode]/[[id]].vue @@ -5,6 +5,7 @@ const toast = useToast() const mode = route.params.mode const isCreate = computed(() => mode === "create") const loading = ref(false) +const credentialsReadable = ref(true) const itemInfo = ref({ email: "", @@ -20,6 +21,7 @@ const itemInfo = ref({ const setup = async () => { if(!isCreate.value) { const account = await useNuxtApp().$api(`/api/email/accounts/${route.params.id}`) + credentialsReadable.value = account.credentialsReadable !== false itemInfo.value = { ...itemInfo.value, ...account, @@ -36,6 +38,13 @@ const payload = () => ({ password: itemInfo.value.password || undefined, }) +const canSave = computed(() => Boolean( + itemInfo.value.email + && itemInfo.value.imapHost + && itemInfo.value.smtpHost + && (isCreate.value || credentialsReadable.value || itemInfo.value.password) +)) + const createAccount = async () => { loading.value = true const res = await useNuxtApp().$api(`/api/email/accounts`, { @@ -123,7 +132,7 @@ const syncAccount = async () => { v-if="!isCreate" icon="i-heroicons-check" :loading="loading" - :disabled="!itemInfo.email || !itemInfo.imapHost || !itemInfo.smtpHost" + :disabled="!canSave" @click="saveAccount" > Speichern @@ -132,6 +141,16 @@ const syncAccount = async () => {
+ + { variant="soft" icon="i-heroicons-lock-closed" title="Passwort bleibt geschützt" - description="Das gespeicherte Passwort wird nicht angezeigt. Trage nur dann ein neues Passwort ein, wenn du es ändern möchtest." + :description="credentialsReadable ? 'Das gespeicherte Passwort wird nicht angezeigt. Trage nur dann ein neues Passwort ein, wenn du es ändern möchtest.' : 'Das bisherige Passwort kann nicht entschlüsselt werden und muss neu eingetragen werden.'" /> @@ -158,11 +177,11 @@ const syncAccount = async () => { /> - +
diff --git a/frontend/pages/settings/emailaccounts/index.vue b/frontend/pages/settings/emailaccounts/index.vue index ab5aeb4..6f824b7 100644 --- a/frontend/pages/settings/emailaccounts/index.vue +++ b/frontend/pages/settings/emailaccounts/index.vue @@ -73,7 +73,7 @@ setupPage()

- {{ account.email }} + {{ account.displayName || account.email }}

SMTP: {{ account.smtpHost || "nicht gesetzt" }}:{{ account.smtpPort || "-" }} {{ account.hasPassword ? "Passwort hinterlegt" : "Passwort fehlt" }}
+

+ Die verschlüsselten Zugangsdaten sind nicht lesbar. Öffne das Konto und trage alle Zugangsdaten neu ein. +

@@ -96,6 +99,7 @@ setupPage() color="neutral" variant="soft" :loading="syncingAccount === account.id" + :disabled="account.credentialsReadable === false" @click.stop="syncAccount(account)" > Synchronisieren