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
Build and Push Multi-Platform Images / build-and-push (push) Successful in 34s
This commit is contained in:
@@ -27,6 +27,7 @@ services:
|
|||||||
- SMTP_USER=${SMTP_USER:-}
|
- SMTP_USER=${SMTP_USER:-}
|
||||||
- SMTP_PASS=${SMTP_PASS:-}
|
- SMTP_PASS=${SMTP_PASS:-}
|
||||||
- SMTP_FROM=${SMTP_FROM:-paperless@localhost}
|
- SMTP_FROM=${SMTP_FROM:-paperless@localhost}
|
||||||
|
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-}
|
||||||
- SMTP_ENCRYPTION_KEY=${SMTP_ENCRYPTION_KEY:-}
|
- SMTP_ENCRYPTION_KEY=${SMTP_ENCRYPTION_KEY:-}
|
||||||
- POSTPROCESSING_ERROR_TAG=${POSTPROCESSING_ERROR_TAG:-0}
|
- POSTPROCESSING_ERROR_TAG=${POSTPROCESSING_ERROR_TAG:-0}
|
||||||
- MANUELL_BEARBEITEN_TAG=${MANUELL_BEARBEITEN_TAG:-6}
|
- MANUELL_BEARBEITEN_TAG=${MANUELL_BEARBEITEN_TAG:-6}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export class UserSettings {
|
|||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
SmtpFrom!: string | null;
|
SmtpFrom!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
SmtpFromName!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
MailSignatureHtml!: string | null;
|
MailSignatureHtml!: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,11 +203,14 @@ export class InboxController {
|
|||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
segments: { pages: number[]; filename: string }[];
|
segments: { pages: number[]; filename: string }[];
|
||||||
|
sender?: string;
|
||||||
},
|
},
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const preferredUsername: string | null = req.user?.preferredUsername ?? null;
|
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, {
|
await this.inboxService.sendAsEmail(id, preferredUsername, {
|
||||||
...body,
|
...body,
|
||||||
smtpOverride: smtpOverride ?? undefined,
|
smtpOverride: smtpOverride ?? undefined,
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export class MailService {
|
|||||||
smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string };
|
smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string };
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
let transporter = this.transporter;
|
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) {
|
if (options.smtpOverride) {
|
||||||
const o = options.smtpOverride;
|
const o = options.smtpOverride;
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ export class UserSettingsController {
|
|||||||
return this.userSettingsService.updateSettings(req.user.userId, body);
|
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')
|
@Post('test-smtp')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
async testSmtp(@Body() body: { host: string; port: number; secure: boolean; user: string; pass: string }) {
|
async testSmtp(@Body() body: { host: string; port: number; secure: boolean; user: string; pass: string }) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface UserSettingsDto {
|
|||||||
smtpUser: string | null;
|
smtpUser: string | null;
|
||||||
smtpPassSet: boolean;
|
smtpPassSet: boolean;
|
||||||
smtpFrom: string | null;
|
smtpFrom: string | null;
|
||||||
|
smtpFromName: string | null;
|
||||||
mailSignatureHtml: string | null;
|
mailSignatureHtml: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +76,7 @@ export class UserSettingsService {
|
|||||||
smtpUser?: string | null;
|
smtpUser?: string | null;
|
||||||
smtpPass?: string | null;
|
smtpPass?: string | null;
|
||||||
smtpFrom?: string | null;
|
smtpFrom?: string | null;
|
||||||
|
smtpFromName?: string | null;
|
||||||
mailSignatureHtml?: string | null;
|
mailSignatureHtml?: string | null;
|
||||||
},
|
},
|
||||||
): Promise<UserSettingsDto> {
|
): Promise<UserSettingsDto> {
|
||||||
@@ -91,6 +93,7 @@ export class UserSettingsService {
|
|||||||
entity.SmtpPass = this.encrypt(data.smtpPass);
|
entity.SmtpPass = this.encrypt(data.smtpPass);
|
||||||
}
|
}
|
||||||
if (data.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom;
|
if (data.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom;
|
||||||
|
if (data.smtpFromName !== undefined) entity.SmtpFromName = data.smtpFromName;
|
||||||
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
||||||
|
|
||||||
await this.repo.save(entity);
|
await this.repo.save(entity);
|
||||||
@@ -123,16 +126,34 @@ export class UserSettingsService {
|
|||||||
} | null> {
|
} | null> {
|
||||||
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||||
if (!entity?.SmtpHost || !entity?.SmtpPass) return null;
|
if (!entity?.SmtpHost || !entity?.SmtpPass) return null;
|
||||||
|
const fromEmail = entity.SmtpFrom ?? entity.SmtpUser ?? '';
|
||||||
|
const from = entity.SmtpFromName ? `"${entity.SmtpFromName}" <${fromEmail}>` : fromEmail;
|
||||||
return {
|
return {
|
||||||
host: entity.SmtpHost,
|
host: entity.SmtpHost,
|
||||||
port: entity.SmtpPort ?? 587,
|
port: entity.SmtpPort ?? 587,
|
||||||
secure: entity.SmtpSecure,
|
secure: entity.SmtpSecure,
|
||||||
user: entity.SmtpUser ?? '',
|
user: entity.SmtpUser ?? '',
|
||||||
pass: this.decrypt(entity.SmtpPass),
|
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 {
|
private toDto(entity: UserSettings | null): UserSettingsDto {
|
||||||
return {
|
return {
|
||||||
smtpHost: entity?.SmtpHost ?? null,
|
smtpHost: entity?.SmtpHost ?? null,
|
||||||
@@ -141,6 +162,7 @@ export class UserSettingsService {
|
|||||||
smtpUser: entity?.SmtpUser ?? null,
|
smtpUser: entity?.SmtpUser ?? null,
|
||||||
smtpPassSet: !!(entity?.SmtpPass),
|
smtpPassSet: !!(entity?.SmtpPass),
|
||||||
smtpFrom: entity?.SmtpFrom ?? null,
|
smtpFrom: entity?.SmtpFrom ?? null,
|
||||||
|
smtpFromName: entity?.SmtpFromName ?? null,
|
||||||
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
|
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export const inboxApi = {
|
|||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
segments: { pages: number[]; filename: string }[];
|
segments: { pages: number[]; filename: string }[];
|
||||||
|
sender?: string;
|
||||||
},
|
},
|
||||||
) =>
|
) =>
|
||||||
api.post(`/api/inbox/${encodeURIComponent(id)}/send-email`, body).then(() => {}),
|
api.post(`/api/inbox/${encodeURIComponent(id)}/send-email`, body).then(() => {}),
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ export interface UserSettingsData {
|
|||||||
smtpUser: string | null;
|
smtpUser: string | null;
|
||||||
smtpPassSet: boolean;
|
smtpPassSet: boolean;
|
||||||
smtpFrom: string | null;
|
smtpFrom: string | null;
|
||||||
|
smtpFromName: string | null;
|
||||||
mailSignatureHtml: string | null;
|
mailSignatureHtml: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SenderOption {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const userSettingsApi = {
|
export const userSettingsApi = {
|
||||||
get: () => api.get<UserSettingsData>('/api/user-settings').then((r) => r.data),
|
get: () => api.get<UserSettingsData>('/api/user-settings').then((r) => r.data),
|
||||||
|
|
||||||
@@ -20,4 +26,7 @@ export const userSettingsApi = {
|
|||||||
api
|
api
|
||||||
.post<{ ok: boolean; error?: string }>('/api/user-settings/test-smtp', cfg)
|
.post<{ ok: boolean; error?: string }>('/api/user-settings/test-smtp', cfg)
|
||||||
.then((r) => r.data),
|
.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 { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
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 type { MenuProps } from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
@@ -26,7 +26,7 @@ import StarterKit from '@tiptap/starter-kit';
|
|||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
||||||
import { paperlessApi } from '../api/paperless';
|
import { paperlessApi } from '../api/paperless';
|
||||||
import { userSettingsApi } from '../api/userSettings';
|
import { userSettingsApi, type SenderOption } from '../api/userSettings';
|
||||||
|
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
const ZOOM_MAX = 3;
|
const ZOOM_MAX = 3;
|
||||||
@@ -670,6 +670,8 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [filenames, setFilenames] = useState<string[]>([]);
|
const [filenames, setFilenames] = useState<string[]>([]);
|
||||||
|
const [senders, setSenders] = useState<SenderOption[]>([]);
|
||||||
|
const [selectedSender, setSelectedSender] = useState<string>('default');
|
||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [StarterKit, Underline],
|
extensions: [StarterKit, Underline],
|
||||||
@@ -690,6 +692,10 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
editor?.commands.clearContent();
|
editor?.commands.clearContent();
|
||||||
});
|
});
|
||||||
|
userSettingsApi.getSenders().then((s) => {
|
||||||
|
setSenders(s);
|
||||||
|
setSelectedSender(s.length > 1 ? 'user' : 'default');
|
||||||
|
}).catch(() => setSenders([]));
|
||||||
}, [open, documents, fileName, form, editor]);
|
}, [open, documents, fileName, form, editor]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
@@ -705,6 +711,7 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
pages: doc.pages,
|
pages: doc.pages,
|
||||||
filename: filenames[i] || fileName,
|
filename: filenames[i] || fileName,
|
||||||
})),
|
})),
|
||||||
|
sender: senders.length > 1 ? selectedSender : undefined,
|
||||||
});
|
});
|
||||||
message.success('E-Mail wurde gesendet');
|
message.success('E-Mail wurde gesendet');
|
||||||
onClose();
|
onClose();
|
||||||
@@ -729,6 +736,15 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
width={580}
|
width={580}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
<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' }]}>
|
<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" />
|
<Input placeholder="empfaenger@beispiel.de" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function MailSettingsTab() {
|
|||||||
smtpSecure: data.smtpSecure,
|
smtpSecure: data.smtpSecure,
|
||||||
smtpUser: data.smtpUser ?? '',
|
smtpUser: data.smtpUser ?? '',
|
||||||
smtpFrom: data.smtpFrom ?? '',
|
smtpFrom: data.smtpFrom ?? '',
|
||||||
|
smtpFromName: data.smtpFromName ?? '',
|
||||||
});
|
});
|
||||||
setPassSet(data.smtpPassSet);
|
setPassSet(data.smtpPassSet);
|
||||||
if (data.mailSignatureHtml) {
|
if (data.mailSignatureHtml) {
|
||||||
@@ -72,6 +73,7 @@ function MailSettingsTab() {
|
|||||||
smtpUser: values.smtpUser || null,
|
smtpUser: values.smtpUser || null,
|
||||||
smtpPass: values.smtpPass || undefined,
|
smtpPass: values.smtpPass || undefined,
|
||||||
smtpFrom: values.smtpFrom || null,
|
smtpFrom: values.smtpFrom || null,
|
||||||
|
smtpFromName: values.smtpFromName || null,
|
||||||
mailSignatureHtml: editor?.getHTML() ?? null,
|
mailSignatureHtml: editor?.getHTML() ?? null,
|
||||||
});
|
});
|
||||||
setPassSet(updated.smtpPassSet);
|
setPassSet(updated.smtpPassSet);
|
||||||
@@ -129,6 +131,9 @@ function MailSettingsTab() {
|
|||||||
<Form.Item name="smtpFrom" label="Absender-Adresse (From)">
|
<Form.Item name="smtpFrom" label="Absender-Adresse (From)">
|
||||||
<Input placeholder="mein.name@beispiel.de" />
|
<Input placeholder="mein.name@beispiel.de" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="smtpFromName" label="Absendername (Anzeigename)">
|
||||||
|
<Input placeholder="Max Mustermann" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Signatur">
|
<Form.Item label="Signatur">
|
||||||
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>
|
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user