mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-10 04:22:00 +10:00
email integration
* Nest email module with smtp, postmark and console log drivers * react-email package
This commit is contained in:
14
.env.example
14
.env.example
@ -21,3 +21,17 @@ AWS_S3_BUCKET=
|
||||
AWS_S3_ENDPOINT=
|
||||
AWS_S3_URL=
|
||||
AWS_S3_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
|
||||
# EMAIL drivers: smtp / postmark / log
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_FROM_ADDRESS=hello@example.com
|
||||
MAIL_FROM_NAME=
|
||||
|
||||
# for postmark driver
|
||||
POSTMARK_TOKEN=
|
||||
|
||||
|
||||
@ -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/*"],
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
"editor-ext:editor-ext": "nx run editor-ext:build",
|
||||
"client:dev": "nx run client:dev",
|
||||
"server:dev": "nx run server:start:dev",
|
||||
"server:start": "nx run server:start:prod"
|
||||
"server:start": "nx run server:start:prod",
|
||||
"email:dev": "nx run @docmost/transactional:dev"
|
||||
|
||||
},
|
||||
"dependencies": {
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
|
||||
16
packages/transactional/.gitignore
vendored
Normal file
16
packages/transactional/.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
.env
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
53
packages/transactional/css/styles.ts
Normal file
53
packages/transactional/css/styles.ts
Normal file
@ -0,0 +1,53 @@
|
||||
export const fontFamily = "HelveticaNeue,Helvetica,Arial,sans-serif";
|
||||
|
||||
export const main = {
|
||||
backgroundColor: "#edf2f7",
|
||||
fontFamily,
|
||||
};
|
||||
|
||||
export const container = {
|
||||
maxWidth: "580px",
|
||||
margin: "30px auto",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#e8e5ef",
|
||||
borderRadius: "2px",
|
||||
borderWidth: "1px",
|
||||
boxShadow: "0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015)",
|
||||
};
|
||||
|
||||
export const content = {
|
||||
padding: "5px 20px 10px 20px",
|
||||
};
|
||||
|
||||
export const paragraph = {
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
color: "#333",
|
||||
lineHeight: 1.5,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
export const h1 = {
|
||||
color: "#333",
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
padding: "0",
|
||||
};
|
||||
|
||||
export const logo = {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alingItems: "center",
|
||||
padding: 4,
|
||||
};
|
||||
|
||||
export const link = {
|
||||
textDecoration: "underline",
|
||||
};
|
||||
|
||||
export const footer = {
|
||||
maxWidth: "580px",
|
||||
margin: "0 auto",
|
||||
};
|
||||
26
packages/transactional/emails/test-email.tsx
Normal file
26
packages/transactional/emails/test-email.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { Container, Section, Text } from "@react-email/components";
|
||||
import * as React from "react";
|
||||
import { container, content, paragraph } from "../css/styles";
|
||||
import { MailBody, MailFooter } from "../partials/partials";
|
||||
|
||||
interface WelcomeEmailProps {
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export const TestEmail = ({ username }: WelcomeEmailProps) => {
|
||||
return (
|
||||
<MailBody>
|
||||
<Container style={container}>
|
||||
<Section style={content}>
|
||||
<Text style={paragraph}>Hi {username},</Text>
|
||||
<Text style={paragraph}>
|
||||
This is a test email. Make sure to read it.
|
||||
</Text>
|
||||
</Section>
|
||||
</Container>
|
||||
<MailFooter />
|
||||
</MailBody>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestEmail;
|
||||
10
packages/transactional/package.json
Normal file
10
packages/transactional/package.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@docmost/transactional",
|
||||
"scripts": {
|
||||
"dev": "email dev -p 5019"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "0.0.17",
|
||||
"react-email": "^2.1.2"
|
||||
}
|
||||
}
|
||||
43
packages/transactional/partials/partials.tsx
Normal file
43
packages/transactional/partials/partials.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { footer, h1, logo, main } from "../css/styles";
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
|
||||
interface MailBodyProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function MailBody({ children }: MailBodyProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Body style={main}>{children}</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
export function MailHeader() {
|
||||
return (
|
||||
<Section style={logo}>
|
||||
<Heading style={h1}>logo/text</Heading>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export function MailFooter() {
|
||||
return (
|
||||
<Section style={footer}>
|
||||
<Row>
|
||||
<Text style={{ textAlign: "center", color: "#706a7b" }}>
|
||||
© {new Date().getFullYear()}, All Rights Reserved <br />
|
||||
</Text>
|
||||
</Row>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
6
packages/transactional/utils/utils.ts
Normal file
6
packages/transactional/utils/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const formatDate = (date: Date) => {
|
||||
new Intl.DateTimeFormat("en", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium",
|
||||
}).format(date);
|
||||
};
|
||||
2680
pnpm-lock.yaml
generated
2680
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user