mirror of
https://github.com/docmost/docmost.git
synced 2025-11-12 21:02:37 +10:00
email integration
* Nest email module with smtp, postmark and console log drivers * react-email package
This commit is contained in:
@ -41,6 +41,7 @@
|
||||
"@nestjs/platform-socket.io": "^10.3.8",
|
||||
"@nestjs/serve-static": "^4.0.2",
|
||||
"@nestjs/websockets": "^10.3.8",
|
||||
"@react-email/render": "^0.0.13",
|
||||
"@types/pg": "^8.11.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bytes": "^3.1.2",
|
||||
@ -52,9 +53,11 @@
|
||||
"kysely-migration-cli": "^0.4.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"nestjs-kysely": "^0.1.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.5",
|
||||
"pg-tsquery": "^8.4.2",
|
||||
"postmark": "^4.0.2",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename-ts": "^1.0.2",
|
||||
@ -75,13 +78,14 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||
"@typescript-eslint/parser": "^7.8.0",
|
||||
"eslint": "^9.1.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/server/src/integrations/mail/drivers/index.ts
Normal file
3
apps/server/src/integrations/mail/drivers/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { SmtpDriver } from './smtp.driver';
|
||||
export { PostmarkDriver } from './postmark.driver';
|
||||
export { LogDriver } from './log.driver';
|
||||
@ -0,0 +1,5 @@
|
||||
import { MailMessage } from '../../interfaces/mail.message';
|
||||
|
||||
export interface MailDriver {
|
||||
sendMail(message: MailMessage): Promise<void>;
|
||||
}
|
||||
18
apps/server/src/integrations/mail/drivers/log.driver.ts
Normal file
18
apps/server/src/integrations/mail/drivers/log.driver.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
30
apps/server/src/integrations/mail/drivers/postmark.driver.ts
Normal file
30
apps/server/src/integrations/mail/drivers/postmark.driver.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
apps/server/src/integrations/mail/drivers/smtp.driver.ts
Normal file
32
apps/server/src/integrations/mail/drivers/smtp.driver.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
1
apps/server/src/integrations/mail/interfaces/index.ts
Normal file
1
apps/server/src/integrations/mail/interfaces/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './mail.interface';
|
||||
@ -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[];
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export interface MailMessage {
|
||||
from: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
}
|
||||
2
apps/server/src/integrations/mail/mail.constants.ts
Normal file
2
apps/server/src/integrations/mail/mail.constants.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const MAIL_DRIVER_TOKEN = 'MAIL_DRIVER_TOKEN';
|
||||
export const MAIL_CONFIG_TOKEN = 'MAIL_CONFIG_TOKEN';
|
||||
20
apps/server/src/integrations/mail/mail.module.ts
Normal file
20
apps/server/src/integrations/mail/mail.module.ts
Normal 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],
|
||||
};
|
||||
}
|
||||
}
|
||||
18
apps/server/src/integrations/mail/mail.service.ts
Normal file
18
apps/server/src/integrations/mail/mail.service.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
3
apps/server/src/integrations/mail/mail.utils.ts
Normal file
3
apps/server/src/integrations/mail/mail.utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const mailLogName = (driverName: string) => {
|
||||
return `Mail::${driverName}`;
|
||||
};
|
||||
67
apps/server/src/integrations/mail/providers/mail.provider.ts
Normal file
67
apps/server/src/integrations/mail/providers/mail.provider.ts
Normal 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],
|
||||
};
|
||||
@ -18,6 +18,7 @@
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"strict": true,
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"@docmost/db": ["./src/kysely"],
|
||||
"@docmost/db/*": ["./src/kysely/*"],
|
||||
|
||||
Reference in New Issue
Block a user