mirror of
https://github.com/immich-app/immich.git
synced 2025-12-26 01:11:47 +03:00
* feat(server): add `react-mail` as mail template engine and `nodemailer` * feat(server): add `smtp` related configs to `SystemConfig` * feat(web): add page for SMTP settings * feat(server): add `react-email.adapter` This adapter render the React-Email into HTML and plain/text email. The output is set as the body of the email. * feat(server): add `MailRepository` and `MailService` Allow to use the NestJS-modules-mailer module to send SMTP emails. This is the base transport for the `NotificationRepository` * feat(server): register the job dispatcher and Job for async email This allows to queue email sending jobs for the `EmailService`. * feat(server): add `NotificationRepository` and `NotificationService` This act as a middleware to properly route the notification to the right transport. As POC I've only implemented a simple SMTP transport. * feat(server): add `welcome` email template * feat(server): add the first notification on `createUser` in `UserService` This trigger an event for the `NotificationRepository` that once processes by using the global config and per-user config will carry the payload to the right notification transport. * chore: clean up * chore: clean up web * fix: type errors" * fix package lock * fix mail sending, option to ignore certs * chore: open api * chore: clean up * remove unused import * feat: email feature flag * chore: remove unused interface * small styling --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
import { Inject, Injectable } from '@nestjs/common';
|
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
|
import { OnServerEvent } from 'src/decorators';
|
|
import { ServerAsyncEvent, ServerAsyncEventMap } from 'src/interfaces/event.interface';
|
|
import { IEmailJob, IJobRepository, INotifySignupJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|
import { EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface';
|
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
|
|
|
@Injectable()
|
|
export class NotificationService {
|
|
private configCore: SystemConfigCore;
|
|
|
|
constructor(
|
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
|
@Inject(INotificationRepository) private notificationRepository: INotificationRepository,
|
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
|
) {
|
|
this.logger.setContext(NotificationService.name);
|
|
this.configCore = SystemConfigCore.create(configRepository, logger);
|
|
}
|
|
|
|
init() {
|
|
// TODO
|
|
return Promise.resolve();
|
|
}
|
|
|
|
@OnServerEvent(ServerAsyncEvent.CONFIG_VALIDATE)
|
|
async onValidateConfig({ newConfig }: ServerAsyncEventMap[ServerAsyncEvent.CONFIG_VALIDATE]) {
|
|
try {
|
|
if (newConfig.notifications.smtp.enabled) {
|
|
await this.notificationRepository.verifySmtp(newConfig.notifications.smtp.transport);
|
|
}
|
|
} catch (error: Error | any) {
|
|
this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack);
|
|
throw new Error(`Invalid SMTP configuration: ${error}`);
|
|
}
|
|
}
|
|
|
|
async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
|
|
const user = await this.userRepository.get(id, { withDeleted: false });
|
|
if (!user) {
|
|
return JobStatus.SKIPPED;
|
|
}
|
|
|
|
const { server } = await this.configCore.getConfig();
|
|
const { html, text } = this.notificationRepository.renderEmail({
|
|
template: EmailTemplate.WELCOME,
|
|
data: {
|
|
baseUrl: server.externalDomain || 'http://localhost:2283',
|
|
displayName: user.name,
|
|
username: user.email,
|
|
password: tempPassword,
|
|
},
|
|
});
|
|
|
|
await this.jobRepository.queue({
|
|
name: JobName.SEND_EMAIL,
|
|
data: {
|
|
to: user.email,
|
|
subject: 'Welcome to Immich',
|
|
html,
|
|
text,
|
|
},
|
|
});
|
|
|
|
return JobStatus.SUCCESS;
|
|
}
|
|
|
|
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
|
const { notifications } = await this.configCore.getConfig();
|
|
if (!notifications.smtp.enabled) {
|
|
return JobStatus.SKIPPED;
|
|
}
|
|
|
|
const { to, subject, html, text: plain } = data;
|
|
const response = await this.notificationRepository.sendEmail({
|
|
to,
|
|
subject,
|
|
html,
|
|
text: plain,
|
|
from: notifications.smtp.from,
|
|
replyTo: notifications.smtp.replyTo || notifications.smtp.from,
|
|
smtp: notifications.smtp.transport,
|
|
});
|
|
|
|
if (!response) {
|
|
return JobStatus.FAILED;
|
|
}
|
|
|
|
this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`);
|
|
|
|
return JobStatus.SUCCESS;
|
|
}
|
|
}
|