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, private readonly configService: ConfigService, ) { const raw = this.configService.get('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 { 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 { 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 { 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( '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, 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, }; } }