From d19fd266c7e731f4af3e773e65c11b8066915acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Wed, 6 May 2026 20:49:09 +0200 Subject: [PATCH] feat: add configurable sender name and allow users to choose between system and personal SMTP accounts when sending emails --- docker-compose.yml | 1 + .../database/entities/user-settings.entity.ts | 3 +++ .../src/inbox/inbox.controller.ts | 5 +++- .../src/postprocessing/mail.service.ts | 4 +++- .../user-settings/user-settings.controller.ts | 5 ++++ .../user-settings/user-settings.service.ts | 24 ++++++++++++++++++- paperless-frontend/src/api/inbox.ts | 1 + paperless-frontend/src/api/userSettings.ts | 9 +++++++ .../src/pages/InboxDetailPage.tsx | 20 ++++++++++++++-- .../src/pages/UserSettingsPage.tsx | 5 ++++ 10 files changed, 72 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a5ddcb5..1e6d33e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: - SMTP_USER=${SMTP_USER:-} - SMTP_PASS=${SMTP_PASS:-} - SMTP_FROM=${SMTP_FROM:-paperless@localhost} + - SMTP_FROM_NAME=${SMTP_FROM_NAME:-} - SMTP_ENCRYPTION_KEY=${SMTP_ENCRYPTION_KEY:-} - POSTPROCESSING_ERROR_TAG=${POSTPROCESSING_ERROR_TAG:-0} - MANUELL_BEARBEITEN_TAG=${MANUELL_BEARBEITEN_TAG:-6} diff --git a/paperless-backend/src/database/entities/user-settings.entity.ts b/paperless-backend/src/database/entities/user-settings.entity.ts index 06c4987..4ae84e3 100644 --- a/paperless-backend/src/database/entities/user-settings.entity.ts +++ b/paperless-backend/src/database/entities/user-settings.entity.ts @@ -23,6 +23,9 @@ export class UserSettings { @Column({ type: 'varchar', length: 255, nullable: true }) SmtpFrom!: string | null; + @Column({ type: 'varchar', length: 255, nullable: true }) + SmtpFromName!: string | null; + @Column({ type: 'text', nullable: true }) MailSignatureHtml!: string | null; } diff --git a/paperless-backend/src/inbox/inbox.controller.ts b/paperless-backend/src/inbox/inbox.controller.ts index ae0be74..7402f95 100644 --- a/paperless-backend/src/inbox/inbox.controller.ts +++ b/paperless-backend/src/inbox/inbox.controller.ts @@ -203,11 +203,14 @@ export class InboxController { body: string; html?: string; segments: { pages: number[]; filename: string }[]; + sender?: string; }, @Request() req: any, ): Promise { const preferredUsername: string | null = req.user?.preferredUsername ?? null; - const smtpOverride = await this.userSettingsService.getSmtpConfig(req.user.userId); + const smtpOverride = body.sender === 'user' + ? await this.userSettingsService.getSmtpConfig(req.user.userId) + : null; await this.inboxService.sendAsEmail(id, preferredUsername, { ...body, smtpOverride: smtpOverride ?? undefined, diff --git a/paperless-backend/src/postprocessing/mail.service.ts b/paperless-backend/src/postprocessing/mail.service.ts index 4b897e8..3acc530 100644 --- a/paperless-backend/src/postprocessing/mail.service.ts +++ b/paperless-backend/src/postprocessing/mail.service.ts @@ -28,7 +28,9 @@ export class MailService { smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string }; }): Promise { let transporter = this.transporter; - let from = this.configService.get('SMTP_FROM', 'paperless@localhost'); + const globalFromEmail = this.configService.get('SMTP_FROM', 'paperless@localhost'); + const globalFromName = this.configService.get('SMTP_FROM_NAME', ''); + let from = globalFromName ? `"${globalFromName}" <${globalFromEmail}>` : globalFromEmail; if (options.smtpOverride) { const o = options.smtpOverride; diff --git a/paperless-backend/src/user-settings/user-settings.controller.ts b/paperless-backend/src/user-settings/user-settings.controller.ts index e65e732..49d8ee8 100644 --- a/paperless-backend/src/user-settings/user-settings.controller.ts +++ b/paperless-backend/src/user-settings/user-settings.controller.ts @@ -15,6 +15,11 @@ export class UserSettingsController { return this.userSettingsService.updateSettings(req.user.userId, body); } + @Get('senders') + async getSenders(@Request() req: any) { + return this.userSettingsService.getAvailableSenders(req.user.userId); + } + @Post('test-smtp') @HttpCode(200) async testSmtp(@Body() body: { host: string; port: number; secure: boolean; user: string; pass: string }) { diff --git a/paperless-backend/src/user-settings/user-settings.service.ts b/paperless-backend/src/user-settings/user-settings.service.ts index 21a8cf0..928efab 100644 --- a/paperless-backend/src/user-settings/user-settings.service.ts +++ b/paperless-backend/src/user-settings/user-settings.service.ts @@ -15,6 +15,7 @@ export interface UserSettingsDto { smtpUser: string | null; smtpPassSet: boolean; smtpFrom: string | null; + smtpFromName: string | null; mailSignatureHtml: string | null; } @@ -75,6 +76,7 @@ export class UserSettingsService { smtpUser?: string | null; smtpPass?: string | null; smtpFrom?: string | null; + smtpFromName?: string | null; mailSignatureHtml?: string | null; }, ): Promise { @@ -91,6 +93,7 @@ export class UserSettingsService { entity.SmtpPass = this.encrypt(data.smtpPass); } if (data.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom; + if (data.smtpFromName !== undefined) entity.SmtpFromName = data.smtpFromName; if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml; await this.repo.save(entity); @@ -123,16 +126,34 @@ export class UserSettingsService { } | null> { const entity = await this.repo.findOne({ where: { UserId: userId } }); if (!entity?.SmtpHost || !entity?.SmtpPass) return null; + const fromEmail = entity.SmtpFrom ?? entity.SmtpUser ?? ''; + const from = entity.SmtpFromName ? `"${entity.SmtpFromName}" <${fromEmail}>` : fromEmail; return { host: entity.SmtpHost, port: entity.SmtpPort ?? 587, secure: entity.SmtpSecure, user: entity.SmtpUser ?? '', pass: this.decrypt(entity.SmtpPass), - from: entity.SmtpFrom ?? entity.SmtpUser ?? '', + from, }; } + async getAvailableSenders(userId: string): Promise<{ id: string; label: string }[]> { + const defaultEmail = this.configService.get('SMTP_FROM', 'paperless@localhost'); + const defaultName = this.configService.get('SMTP_FROM_NAME', ''); + const defaultLabel = defaultName ? `${defaultName} <${defaultEmail}>` : defaultEmail; + const senders: { id: string; label: string }[] = [{ id: 'default', label: defaultLabel }]; + + const entity = await this.repo.findOne({ where: { UserId: userId } }); + if (entity?.SmtpHost && entity?.SmtpPass) { + const userEmail = entity.SmtpFrom ?? entity.SmtpUser ?? ''; + const userLabel = entity.SmtpFromName ? `${entity.SmtpFromName} <${userEmail}>` : userEmail; + senders.push({ id: 'user', label: userLabel }); + } + + return senders; + } + private toDto(entity: UserSettings | null): UserSettingsDto { return { smtpHost: entity?.SmtpHost ?? null, @@ -141,6 +162,7 @@ export class UserSettingsService { smtpUser: entity?.SmtpUser ?? null, smtpPassSet: !!(entity?.SmtpPass), smtpFrom: entity?.SmtpFrom ?? null, + smtpFromName: entity?.SmtpFromName ?? null, mailSignatureHtml: entity?.MailSignatureHtml ?? null, }; } diff --git a/paperless-frontend/src/api/inbox.ts b/paperless-frontend/src/api/inbox.ts index 5f9a238..3e7f885 100644 --- a/paperless-frontend/src/api/inbox.ts +++ b/paperless-frontend/src/api/inbox.ts @@ -120,6 +120,7 @@ export const inboxApi = { body: string; html?: string; segments: { pages: number[]; filename: string }[]; + sender?: string; }, ) => api.post(`/api/inbox/${encodeURIComponent(id)}/send-email`, body).then(() => {}), diff --git a/paperless-frontend/src/api/userSettings.ts b/paperless-frontend/src/api/userSettings.ts index 5d51bcd..c128b7f 100644 --- a/paperless-frontend/src/api/userSettings.ts +++ b/paperless-frontend/src/api/userSettings.ts @@ -7,9 +7,15 @@ export interface UserSettingsData { smtpUser: string | null; smtpPassSet: boolean; smtpFrom: string | null; + smtpFromName: string | null; mailSignatureHtml: string | null; } +export interface SenderOption { + id: string; + label: string; +} + export const userSettingsApi = { get: () => api.get('/api/user-settings').then((r) => r.data), @@ -20,4 +26,7 @@ export const userSettingsApi = { api .post<{ ok: boolean; error?: string }>('/api/user-settings/test-smtp', cfg) .then((r) => r.data), + + getSenders: () => + api.get('/api/user-settings/senders').then((r) => r.data), }; diff --git a/paperless-frontend/src/pages/InboxDetailPage.tsx b/paperless-frontend/src/pages/InboxDetailPage.tsx index edeb024..3a2b750 100644 --- a/paperless-frontend/src/pages/InboxDetailPage.tsx +++ b/paperless-frontend/src/pages/InboxDetailPage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Button, Dropdown, Empty, Form, Input, Modal, Popconfirm, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd'; +import { Button, Dropdown, Empty, Form, Input, Modal, Popconfirm, Select, Space, Spin, Steps, Tag, Tooltip, Typography, message } from 'antd'; import type { MenuProps } from 'antd'; import { ArrowLeftOutlined, @@ -26,7 +26,7 @@ import StarterKit from '@tiptap/starter-kit'; import Underline from '@tiptap/extension-underline'; import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox'; import { paperlessApi } from '../api/paperless'; -import { userSettingsApi } from '../api/userSettings'; +import { userSettingsApi, type SenderOption } from '../api/userSettings'; const ZOOM_MIN = 0.5; const ZOOM_MAX = 3; @@ -670,6 +670,8 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma const [form] = Form.useForm(); const [submitting, setSubmitting] = useState(false); const [filenames, setFilenames] = useState([]); + const [senders, setSenders] = useState([]); + const [selectedSender, setSelectedSender] = useState('default'); const editor = useEditor({ extensions: [StarterKit, Underline], @@ -690,6 +692,10 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma }).catch(() => { editor?.commands.clearContent(); }); + userSettingsApi.getSenders().then((s) => { + setSenders(s); + setSelectedSender(s.length > 1 ? 'user' : 'default'); + }).catch(() => setSenders([])); }, [open, documents, fileName, form, editor]); const handleOk = async () => { @@ -705,6 +711,7 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma pages: doc.pages, filename: filenames[i] || fileName, })), + sender: senders.length > 1 ? selectedSender : undefined, }); message.success('E-Mail wurde gesendet'); onClose(); @@ -729,6 +736,15 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma width={580} >
+ {senders.length > 1 && ( + + diff --git a/paperless-frontend/src/pages/UserSettingsPage.tsx b/paperless-frontend/src/pages/UserSettingsPage.tsx index 3a73fbb..75f03d2 100644 --- a/paperless-frontend/src/pages/UserSettingsPage.tsx +++ b/paperless-frontend/src/pages/UserSettingsPage.tsx @@ -50,6 +50,7 @@ function MailSettingsTab() { smtpSecure: data.smtpSecure, smtpUser: data.smtpUser ?? '', smtpFrom: data.smtpFrom ?? '', + smtpFromName: data.smtpFromName ?? '', }); setPassSet(data.smtpPassSet); if (data.mailSignatureHtml) { @@ -72,6 +73,7 @@ function MailSettingsTab() { smtpUser: values.smtpUser || null, smtpPass: values.smtpPass || undefined, smtpFrom: values.smtpFrom || null, + smtpFromName: values.smtpFromName || null, mailSignatureHtml: editor?.getHTML() ?? null, }); setPassSet(updated.smtpPassSet); @@ -129,6 +131,9 @@ function MailSettingsTab() { + + +