feat: add configurable sender name and allow users to choose between system and personal SMTP accounts when sending emails
Build and Push Multi-Platform Images / build-and-push (push) Successful in 34s

This commit is contained in:
2026-05-06 20:49:09 +02:00
parent 8212f733ab
commit d19fd266c7
10 changed files with 72 additions and 5 deletions
+1
View File
@@ -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}
@@ -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;
}
@@ -203,11 +203,14 @@ export class InboxController {
body: string;
html?: string;
segments: { pages: number[]; filename: string }[];
sender?: string;
},
@Request() req: any,
): Promise<void> {
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,
@@ -28,7 +28,9 @@ export class MailService {
smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string };
}): Promise<void> {
let transporter = this.transporter;
let from = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
const globalFromEmail = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
const globalFromName = this.configService.get<string>('SMTP_FROM_NAME', '');
let from = globalFromName ? `"${globalFromName}" <${globalFromEmail}>` : globalFromEmail;
if (options.smtpOverride) {
const o = options.smtpOverride;
@@ -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 }) {
@@ -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<UserSettingsDto> {
@@ -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<string>('SMTP_FROM', 'paperless@localhost');
const defaultName = this.configService.get<string>('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,
};
}
+1
View File
@@ -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(() => {}),
@@ -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<UserSettingsData>('/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<SenderOption[]>('/api/user-settings/senders').then((r) => r.data),
};
@@ -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<string[]>([]);
const [senders, setSenders] = useState<SenderOption[]>([]);
const [selectedSender, setSelectedSender] = useState<string>('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}
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
{senders.length > 1 && (
<Form.Item label="Absender">
<Select
value={selectedSender}
onChange={setSelectedSender}
options={senders.map((s) => ({ value: s.id, label: s.label }))}
/>
</Form.Item>
)}
<Form.Item name="to" label="Empfänger" rules={[{ required: true, message: 'Bitte Empfänger angeben' }, { type: 'email', message: 'Ungültige E-Mail-Adresse' }]}>
<Input placeholder="empfaenger@beispiel.de" />
</Form.Item>
@@ -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() {
<Form.Item name="smtpFrom" label="Absender-Adresse (From)">
<Input placeholder="mein.name@beispiel.de" />
</Form.Item>
<Form.Item name="smtpFromName" label="Absendername (Anzeigename)">
<Input placeholder="Max Mustermann" />
</Form.Item>
<Form.Item label="Signatur">
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>