email integration

* Nest email module with smtp, postmark and console log drivers
* react-email package
This commit is contained in:
Philipinho
2024-05-02 03:12:40 +01:00
parent 48be0c21ae
commit 4c573b9bc2
26 changed files with 2685 additions and 446 deletions

View File

@ -17,6 +17,7 @@ import { SpaceModule } from './space/space.module';
import { GroupModule } from './group/group.module';
import { CaslModule } from './casl/casl.module';
import { DomainMiddleware } from '../middlewares/domain.middleware';
import { MailModule } from '../integrations/mail/mail.module';
@Module({
imports: [
@ -27,6 +28,9 @@ import { DomainMiddleware } from '../middlewares/domain.middleware';
StorageModule.forRootAsync({
imports: [EnvironmentModule],
}),
MailModule.forRootAsync({
imports: [EnvironmentModule],
}),
AttachmentModule,
CommentModule,
SearchModule,

View File

@ -70,4 +70,36 @@ export class EnvironmentService {
isSelfHosted(): boolean {
return !this.isCloud();
}
getMailDriver(): string {
return this.configService.get<string>('MAIL_DRIVER', 'log');
}
getMailHost(): string {
return this.configService.get<string>('MAIL_HOST', '127.0.0.1');
}
getMailPort(): number {
return this.configService.get<number>('MAIL_PORT');
}
getMailUsername(): string {
return this.configService.get<string>('MAIL_USERNAME');
}
getMailPassword(): string {
return this.configService.get<string>('MAIL_PASSWORD');
}
getMailFromAddress(): string {
return this.configService.get<string>('MAIL_FROM_ADDRESS');
}
getMailFromName(): string {
return this.configService.get<string>('MAIL_FROM_NAME');
}
getPostmarkToken(): string {
return this.configService.get<string>('POSTMARK_TOKEN');
}
}

View File

@ -0,0 +1,3 @@
export { SmtpDriver } from './smtp.driver';
export { PostmarkDriver } from './postmark.driver';
export { LogDriver } from './log.driver';

View File

@ -0,0 +1,5 @@
import { MailMessage } from '../../interfaces/mail.message';
export interface MailDriver {
sendMail(message: MailMessage): Promise<void>;
}

View File

@ -0,0 +1,18 @@
import { MailDriver } from './interfaces/mail-driver.interface';
import { Logger } from '@nestjs/common';
import { MailMessage } from '../interfaces/mail.message';
import { mailLogName } from '../mail.utils';
export class LogDriver implements MailDriver {
private readonly logger = new Logger(mailLogName(LogDriver.name));
async sendMail(message: MailMessage): Promise<void> {
const mailLog = {
to: message.to,
subject: message.subject,
text: message.text,
};
this.logger.log(`Logged mail: ${JSON.stringify(mailLog)}`);
}
}

View File

@ -0,0 +1,30 @@
import { MailDriver } from './interfaces/mail-driver.interface';
import { PostmarkConfig } from '../interfaces';
import { ServerClient } from 'postmark';
import { MailMessage } from '../interfaces/mail.message';
import { Logger } from '@nestjs/common';
import { mailLogName } from '../mail.utils';
export class PostmarkDriver implements MailDriver {
private readonly logger = new Logger(mailLogName(PostmarkDriver.name));
private readonly postmarkClient: ServerClient;
constructor(config: PostmarkConfig) {
this.postmarkClient = new ServerClient(config.postmarkToken);
}
async sendMail(message: MailMessage): Promise<void> {
try {
await this.postmarkClient.sendEmail({
From: message.from,
To: message.to,
Subject: message.subject,
TextBody: message.text,
HtmlBody: message.html,
});
this.logger.debug(`Sent mail to ${message.to}`);
} catch (err) {
this.logger.warn(`Failed to send mail to ${message.to}: ${err}`);
}
}
}

View File

@ -0,0 +1,32 @@
import { MailDriver } from './interfaces/mail-driver.interface';
import { SMTPConfig } from '../interfaces';
import { Transporter } from 'nodemailer';
import * as nodemailer from 'nodemailer';
import { MailMessage } from '../interfaces/mail.message';
import { Logger } from '@nestjs/common';
import { mailLogName } from '../mail.utils';
export class SmtpDriver implements MailDriver {
private readonly logger = new Logger(mailLogName(SmtpDriver.name));
private readonly transporter: Transporter;
constructor(config: SMTPConfig) {
this.transporter = nodemailer.createTransport(config);
}
async sendMail(message: MailMessage): Promise<void> {
try {
await this.transporter.sendMail({
from: message.from,
to: message.to,
subject: message.subject,
text: message.text,
html: message.html,
});
this.logger.debug(`Sent mail to ${message.to}`);
} catch (err) {
this.logger.warn(`Failed to send mail to ${message.to}: ${err}`);
}
}
}

View File

@ -0,0 +1 @@
export * from './mail.interface';

View File

@ -0,0 +1,30 @@
import SMTPTransport from 'nodemailer/lib/smtp-transport';
export enum MailOption {
SMTP = 'smtp',
Postmark = 'postmark',
Log = 'log',
}
export type MailConfig =
| { driver: MailOption.SMTP; config: SMTPConfig }
| { driver: MailOption.Postmark; config: PostmarkConfig }
| { driver: MailOption.Log; config: LogConfig };
export interface SMTPConfig extends SMTPTransport.Options {}
export interface PostmarkConfig {
postmarkToken: string;
}
export interface LogConfig {}
export interface MailOptions {
mail: MailConfig;
}
export interface MailOptionsFactory {
createMailOptions(): Promise<MailConfig> | MailConfig;
}
export interface MailModuleOptions {
imports?: any[];
}

View File

@ -0,0 +1,7 @@
export interface MailMessage {
from: string;
to: string;
subject: string;
text?: string;
html?: string;
}

View File

@ -0,0 +1,2 @@
export const MAIL_DRIVER_TOKEN = 'MAIL_DRIVER_TOKEN';
export const MAIL_CONFIG_TOKEN = 'MAIL_CONFIG_TOKEN';

View File

@ -0,0 +1,20 @@
import { DynamicModule, Global, Module } from '@nestjs/common';
import {
mailDriverConfigProvider,
mailDriverProvider,
} from './providers/mail.provider';
import { MailModuleOptions } from './interfaces';
import { MailService } from './mail.service';
@Global()
@Module({})
export class MailModule {
static forRootAsync(options: MailModuleOptions): DynamicModule {
return {
module: MailModule,
imports: options.imports || [],
providers: [mailDriverConfigProvider, mailDriverProvider, MailService],
exports: [MailService],
};
}
}

View File

@ -0,0 +1,18 @@
import { Inject, Injectable } from '@nestjs/common';
import { MAIL_DRIVER_TOKEN } from './mail.constants';
import { MailDriver } from './drivers/interfaces/mail-driver.interface';
import { MailMessage } from './interfaces/mail.message';
import { EnvironmentService } from '../environment/environment.service';
@Injectable()
export class MailService {
constructor(
@Inject(MAIL_DRIVER_TOKEN) private mailDriver: MailDriver,
private readonly environmentService: EnvironmentService,
) {}
async sendMail(message: Omit<MailMessage, 'from'>): Promise<void> {
const sender = `${this.environmentService.getMailFromName()} <${this.environmentService.getMailFromAddress()}> `;
await this.mailDriver.sendMail({ from: sender, ...message });
}
}

View File

@ -0,0 +1,3 @@
export const mailLogName = (driverName: string) => {
return `Mail::${driverName}`;
};

View File

@ -0,0 +1,67 @@
import { EnvironmentService } from '../../environment/environment.service';
import { MailOption, PostmarkConfig, SMTPConfig } from '../interfaces';
import { SmtpDriver, PostmarkDriver, LogDriver } from '../drivers';
import { MailDriver } from '../drivers/interfaces/mail-driver.interface';
import { MailConfig } from '../interfaces';
import { MAIL_CONFIG_TOKEN, MAIL_DRIVER_TOKEN } from '../mail.constants';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
function createMailDriver(mail: MailConfig): MailDriver {
switch (mail.driver) {
case MailOption.SMTP:
return new SmtpDriver(mail.config as SMTPConfig);
case MailOption.Postmark:
return new PostmarkDriver(mail.config as PostmarkConfig);
case MailOption.Log:
return new LogDriver();
default:
throw new Error(`Unknown mail driver`);
}
}
export const mailDriverConfigProvider = {
provide: MAIL_CONFIG_TOKEN,
useFactory: async (environmentService: EnvironmentService) => {
const driver = environmentService.getMailDriver().toLocaleLowerCase();
if (driver === MailOption.SMTP) {
return {
driver,
config: {
host: environmentService.getMailHost(),
port: environmentService.getMailPort(),
connectionTimeout: 30 * 1000, // 30 seconds
auth: {
user: environmentService.getMailUsername(),
pass: environmentService.getMailPassword(),
},
} as SMTPTransport.Options,
};
}
if (driver === MailOption.Postmark) {
return {
driver,
config: {
postmarkToken: environmentService.getPostmarkToken(),
} as PostmarkConfig,
};
}
if (driver === MailOption.Log) {
return {
driver,
};
}
throw new Error(`Unknown mail driver: ${driver}`);
},
inject: [EnvironmentService],
};
export const mailDriverProvider = {
provide: MAIL_DRIVER_TOKEN,
useFactory: (config: MailConfig) => createMailDriver(config),
inject: [MAIL_CONFIG_TOKEN],
};