Files
paperlessmanager/paperless-backend/src/user-settings/user-settings.service.ts
T
bjoernpoettker dad0136365
Build and Push Multi-Platform Images / build-and-push (push) Successful in 41s
chore: apply ESLint auto-fix across entire backend
Reformats code style (line breaks, indentation, type annotations)
without changing logic. Also includes minor feature additions bundled
in the same lint run (stats service, user-settings groups, agrarmonitor
polling improvements).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:02:02 +02:00

243 lines
7.9 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
import { Repository } from 'typeorm';
import * as nodemailer from 'nodemailer';
import * as crypto from 'crypto';
import { UserSettings } from '../database/entities/user-settings.entity';
const ALGORITHM = 'aes-256-gcm';
export interface UserSettingsDto {
smtpHost: string | null;
smtpPort: number | null;
smtpSecure: boolean;
smtpUser: string | null;
smtpPassSet: boolean;
smtpFrom: string | null;
smtpFromName: string | null;
mailSignatureHtml: string | null;
defaultLabelTemplateId: number | null;
emailRecipientHistory: string[] | null;
dailyDigestEnabled: boolean;
}
@Injectable()
export class UserSettingsService {
private readonly logger = new Logger(UserSettingsService.name);
private readonly encKey: Buffer | null;
constructor(
@InjectRepository(UserSettings)
private readonly repo: Repository<UserSettings>,
private readonly configService: ConfigService,
) {
const raw = this.configService.get<string>('SMTP_ENCRYPTION_KEY', '');
if (raw && raw.length === 64) {
this.encKey = Buffer.from(raw, 'hex');
} else {
this.encKey = null;
this.logger.warn(
'SMTP_ENCRYPTION_KEY fehlt oder hat falsche Länge (64 Hex-Zeichen = 32 Bytes erforderlich). Passwörter werden im Klartext gespeichert.',
);
}
}
private encrypt(plaintext: string): string {
if (!this.encKey) return plaintext;
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, this.encKey, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return `enc:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
}
private decrypt(stored: string): string {
if (!this.encKey || !stored.startsWith('enc:')) return stored;
const parts = stored.split(':');
if (parts.length !== 4) return stored;
const [, ivHex, authTagHex, encryptedHex] = parts;
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const encrypted = Buffer.from(encryptedHex, 'hex');
const decipher = crypto.createDecipheriv(ALGORITHM, this.encKey, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([
decipher.update(encrypted),
decipher.final(),
]).toString('utf8');
}
async getSettings(
userId: string,
email?: string,
preferredUsername?: string,
groups?: string[],
): Promise<UserSettingsDto> {
let entity = await this.repo.findOne({ where: { UserId: userId } });
if (email || preferredUsername || groups) {
if (!entity) {
entity = this.repo.create({ UserId: userId });
}
if (email) entity.UserEmail = email;
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
if (groups) entity.UserGroups = groups;
await this.repo.save(entity);
}
return this.toDto(entity);
}
async updateSettings(
userId: string,
data: {
smtpHost?: string | null;
smtpPort?: number | null;
smtpSecure?: boolean;
smtpUser?: string | null;
smtpPass?: string | null;
smtpFrom?: string | null;
smtpFromName?: string | null;
mailSignatureHtml?: string | null;
defaultLabelTemplateId?: number | null;
emailRecipientHistory?: string[] | null;
dailyDigestEnabled?: boolean;
},
email?: string,
preferredUsername?: string,
groups?: string[],
): Promise<UserSettingsDto> {
let entity = await this.repo.findOne({ where: { UserId: userId } });
if (!entity) {
entity = this.repo.create({ UserId: userId });
}
if (data.smtpHost !== undefined) entity.SmtpHost = data.smtpHost;
if (data.smtpPort !== undefined) entity.SmtpPort = data.smtpPort;
if (data.smtpSecure !== undefined) entity.SmtpSecure = data.smtpSecure;
if (data.smtpUser !== undefined) entity.SmtpUser = data.smtpUser;
if (
data.smtpPass !== undefined &&
data.smtpPass !== null &&
data.smtpPass !== ''
) {
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;
if (data.defaultLabelTemplateId !== undefined)
entity.DefaultLabelTemplateId = data.defaultLabelTemplateId;
if (data.emailRecipientHistory !== undefined)
entity.EmailRecipientHistory = data.emailRecipientHistory;
if (data.dailyDigestEnabled !== undefined)
entity.DailyDigestEnabled = data.dailyDigestEnabled;
if (email) entity.UserEmail = email;
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
if (groups) entity.UserGroups = groups;
await this.repo.save(entity);
return this.toDto(entity);
}
async testSmtp(config: {
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
}): Promise<{ ok: boolean; error?: string }> {
const transporter = nodemailer.createTransport({
host: config.host,
port: config.port,
secure: config.secure,
auth: { user: config.user, pass: config.pass },
});
try {
await transporter.verify();
return { ok: true };
} catch (err: any) {
return { ok: false, error: err.message };
}
}
async getSmtpConfig(userId: string): Promise<{
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
from: string;
} | 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,
};
}
async findAllDigestSubscribers(): Promise<UserSettings[]> {
return this.repo
.find({
where: { DailyDigestEnabled: true },
})
.then((rows) => rows.filter((r) => !!r.UserEmail));
}
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,
smtpPort: entity?.SmtpPort ?? null,
smtpSecure: entity?.SmtpSecure ?? false,
smtpUser: entity?.SmtpUser ?? null,
smtpPassSet: !!entity?.SmtpPass,
smtpFrom: entity?.SmtpFrom ?? null,
smtpFromName: entity?.SmtpFromName ?? null,
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
defaultLabelTemplateId: entity?.DefaultLabelTemplateId ?? null,
emailRecipientHistory: entity?.EmailRecipientHistory ?? null,
dailyDigestEnabled: entity?.DailyDigestEnabled ?? false,
};
}
}