import { Container, Service } from 'typedi'; import { existsSync } from 'fs'; import { readFile } from 'fs/promises'; import Handlebars from 'handlebars'; import { join as pathJoin } from 'path'; import { GlobalConfig } from '@n8n/config'; import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { UserRepository } from '@db/repositories/user.repository'; import { Logger } from '@/Logger'; import { UrlService } from '@/services/url.service'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { toError } from '@/utils'; import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces'; import { NodeMailer } from './NodeMailer'; import { EventService } from '@/events/event.service'; type Template = HandlebarsTemplateDelegate; type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared'; @Service() export class UserManagementMailer { readonly isEmailSetUp: boolean; readonly templateOverrides: GlobalConfig['userManagement']['emails']['template']; readonly templatesCache: Partial> = {}; readonly mailer: NodeMailer | undefined; constructor( globalConfig: GlobalConfig, private readonly logger: Logger, private readonly userRepository: UserRepository, private readonly urlService: UrlService, ) { const emailsConfig = globalConfig.userManagement.emails; this.isEmailSetUp = emailsConfig.mode === 'smtp' && emailsConfig.smtp.host !== ''; this.templateOverrides = emailsConfig.template; // Other implementations can be used in the future. if (this.isEmailSetUp) { this.mailer = Container.get(NodeMailer); } } async invite(inviteEmailData: InviteEmailData): Promise { if (!this.mailer) return { emailSent: false }; const template = await this.getTemplate('invite'); const result = await this.mailer.sendMail({ emailRecipients: inviteEmailData.email, subject: 'You have been invited to n8n', body: template(inviteEmailData), }); // If mailer does not exist it means mail has been disabled. // No error, just say no email was sent. return result ?? { emailSent: false }; } async passwordReset(passwordResetData: PasswordResetData): Promise { if (!this.mailer) return { emailSent: false }; const template = await this.getTemplate('passwordReset', 'passwordReset.html'); const result = await this.mailer.sendMail({ emailRecipients: passwordResetData.email, subject: 'n8n password reset', body: template(passwordResetData), }); // If mailer does not exist it means mail has been disabled. // No error, just say no email was sent. return result ?? { emailSent: false }; } async notifyWorkflowShared({ sharer, newShareeIds, workflow, }: { sharer: User; newShareeIds: string[]; workflow: WorkflowEntity; }): Promise { if (!this.mailer) return { emailSent: false }; const recipients = await this.userRepository.getEmailsByIds(newShareeIds); if (recipients.length === 0) return { emailSent: false }; const emailRecipients = recipients.map(({ email }) => email); const populateTemplate = await this.getTemplate('workflowShared', 'workflowShared.html'); const baseUrl = this.urlService.getInstanceBaseUrl(); try { const result = await this.mailer.sendMail({ emailRecipients, subject: `${sharer.firstName} has shared an n8n workflow with you`, body: populateTemplate({ workflowName: workflow.name, workflowUrl: `${baseUrl}/workflow/${workflow.id}`, }), }); if (!result) return { emailSent: false }; this.logger.info('Sent workflow shared email successfully', { sharerId: sharer.id }); Container.get(EventService).emit('user-transactional-email-sent', { userId: sharer.id, messageType: 'Workflow shared', publicApi: false, }); return result; } catch (e) { Container.get(EventService).emit('email-failed', { user: sharer, messageType: 'Workflow shared', publicApi: false, }); const error = toError(e); throw new InternalServerError(`Please contact your administrator: ${error.message}`); } } async notifyCredentialsShared({ sharer, newShareeIds, credentialsName, }: { sharer: User; newShareeIds: string[]; credentialsName: string; }): Promise { if (!this.mailer) return { emailSent: false }; const recipients = await this.userRepository.getEmailsByIds(newShareeIds); if (recipients.length === 0) return { emailSent: false }; const emailRecipients = recipients.map(({ email }) => email); const populateTemplate = await this.getTemplate('credentialsShared', 'credentialsShared.html'); const baseUrl = this.urlService.getInstanceBaseUrl(); try { const result = await this.mailer.sendMail({ emailRecipients, subject: `${sharer.firstName} has shared an n8n credential with you`, body: populateTemplate({ credentialsName, credentialsListUrl: `${baseUrl}/home/credentials`, }), }); if (!result) return { emailSent: false }; this.logger.info('Sent credentials shared email successfully', { sharerId: sharer.id }); Container.get(EventService).emit('user-transactional-email-sent', { userId: sharer.id, messageType: 'Credentials shared', publicApi: false, }); return result; } catch (e) { Container.get(EventService).emit('email-failed', { user: sharer, messageType: 'Credentials shared', publicApi: false, }); const error = toError(e); throw new InternalServerError(`Please contact your administrator: ${error.message}`); } } async getTemplate( templateName: TemplateName, defaultFilename = `${templateName}.html`, ): Promise