Browse Source

refactor(server): Split email loading & sending into separate classes

Michael Bromley 7 năm trước cách đây
mục cha
commit
fdbfb864ea

+ 83 - 0
server/src/email/email-sender.ts

@@ -0,0 +1,83 @@
+import { Injectable } from '@nestjs/common';
+import * as fs from 'fs-extra';
+import { createTransport } from 'nodemailer';
+import { default as Mail } from 'nodemailer/lib/mailer';
+import SMTPTransport from 'nodemailer/lib/smtp-transport';
+import * as path from 'path';
+import { normalizeString } from 'shared/normalize-string';
+import { assertNever } from 'shared/shared-utils';
+import { Stream } from 'stream';
+
+import { EmailTransportOptions } from '../config/email/email-transport-options';
+
+import { GeneratedEmailContext } from './email-context';
+
+export type StreamTransportInfo = {
+    envelope: {
+        from: string;
+        to: string[];
+    };
+    messageId: string;
+    message: Stream;
+};
+
+@Injectable()
+export class EmailSender {
+    async send(email: GeneratedEmailContext, options: EmailTransportOptions) {
+        let transporter: Mail;
+        switch (options.type) {
+            case 'none':
+                return;
+                break;
+            case 'file':
+                transporter = createTransport({
+                    streamTransport: true,
+                });
+                const result = await this.sendMail(email, transporter);
+                const fileName = normalizeString(
+                    `${new Date().toISOString()} ${result.envelope.to[0]} ${email.subject}`,
+                    '_',
+                );
+                const filePath = path.join(options.outputPath, fileName);
+                await this.writeToFile(filePath, result);
+                break;
+            case 'sendmail':
+                transporter = createTransport({
+                    sendmail: true,
+                    path: options.path,
+                });
+                await this.sendMail(email, transporter);
+                break;
+            case 'smtp':
+                transporter = createTransport({
+                    host: options.host,
+                    port: options.port,
+                    secure: options.secure,
+                    auth: options.auth.user,
+                } as SMTPTransport.Options);
+                await this.sendMail(email, transporter);
+                break;
+            default:
+                return assertNever(options);
+        }
+    }
+
+    private async sendMail(email: GeneratedEmailContext, transporter: Mail): Promise<any> {
+        return transporter.sendMail({
+            to: email.recipient,
+            subject: email.subject,
+            html: email.body,
+        });
+    }
+
+    private async writeToFile(filePath: string, info: StreamTransportInfo): Promise<string> {
+        const writeStream = fs.createWriteStream(filePath);
+        return new Promise<string>((resolve, reject) => {
+            writeStream.on('open', () => {
+                info.message.pipe(writeStream);
+                writeStream.on('close', resolve);
+                writeStream.on('error', reject);
+            });
+        });
+    }
+}

+ 49 - 4
server/src/email/email.module.ts

@@ -1,18 +1,63 @@
 import { Module, OnModuleInit } from '@nestjs/common';
+import * as fs from 'fs-extra';
 
 import { ConfigModule } from '../config/config.module';
+import { ConfigService } from '../config/config.service';
+import { EmailTypeConfig } from '../config/email/email-options';
+import { EventBus } from '../event-bus/event-bus';
 import { EventBusModule } from '../event-bus/event-bus.module';
+import { VendureEvent } from '../event-bus/vendure-event';
 
-import { TransactionalEmailService } from './transactional-email.service';
+import { EmailContext } from './email-context';
+import { EmailSender } from './email-sender';
+import { TemplateLoader } from './template-loader';
 
 @Module({
     imports: [ConfigModule, EventBusModule],
-    providers: [TransactionalEmailService],
+    providers: [TemplateLoader, EmailSender],
 })
 export class EmailModule implements OnModuleInit {
-    constructor(private transactionalEmailService: TransactionalEmailService) {}
+    constructor(
+        private configService: ConfigService,
+        private eventBus: EventBus,
+        private templateLoader: TemplateLoader,
+        private emailSender: EmailSender,
+    ) {}
 
     async onModuleInit() {
-        await this.transactionalEmailService.init();
+        await this.setupEventSubscribers();
+    }
+
+    async setupEventSubscribers() {
+        const { emailTypes } = this.configService.emailOptions;
+        for (const [type, config] of Object.entries(emailTypes)) {
+            this.eventBus.subscribe(config.triggerEvent, event => {
+                return this.handleEvent(type, config, event);
+            });
+        }
+        if (this.configService.emailOptions.transport.type === 'file') {
+            // ensure the configured directory exists before
+            // we attempt to write files to it
+            const emailPath = this.configService.emailOptions.transport.outputPath;
+            await fs.ensureDir(emailPath);
+        }
+    }
+
+    private async handleEvent(type: string, config: EmailTypeConfig<any>, event: VendureEvent) {
+        const { generator, transport } = this.configService.emailOptions;
+        const contextConfig = config.createContext(event);
+        if (contextConfig) {
+            const emailContext = new EmailContext({
+                ...contextConfig,
+                type,
+                event,
+            });
+            const { subject, body, templateContext } = await this.templateLoader.loadTemplate(
+                type,
+                emailContext,
+            );
+            const generatedEmailContext = await generator.generate(subject, body, templateContext);
+            await this.emailSender.send(generatedEmailContext, transport);
+        }
     }
 }

+ 41 - 0
server/src/email/template-loader.ts

@@ -0,0 +1,41 @@
+import { Injectable } from '@nestjs/common';
+import * as fs from 'fs-extra';
+
+import { ConfigService } from '../config/config.service';
+import { TemplateConfig } from '../config/email/email-options';
+
+import { EmailContext } from './email-context';
+
+/**
+ * Loads email templates according to the configured TemplateConfig values.
+ */
+@Injectable()
+export class TemplateLoader {
+    constructor(private configService: ConfigService) {}
+
+    async loadTemplate(
+        type: string,
+        context: EmailContext,
+    ): Promise<{ templateContext: any; subject: string; body: string }> {
+        const { subject, templateContext, templatePath } = this.getTemplateConfig(type, context);
+        const body = await fs.readFile(templatePath, 'utf-8');
+
+        return {
+            templateContext: templateContext(context),
+            subject,
+            body,
+        };
+    }
+
+    /**
+     * Returns the corresponding TemplateConfig based on the channelCode and languageCode of the
+     * EmailContext object.
+     */
+    private getTemplateConfig(type: string, context: EmailContext): TemplateConfig {
+        const { emailTypes } = this.configService.emailOptions;
+        const typeConfig = emailTypes[type].templates;
+        const channelConfig = typeConfig[context.channelCode] || typeConfig.defaultChannel;
+        const languageConfig = channelConfig[context.languageCode] || channelConfig.defaultLanguage;
+        return languageConfig;
+    }
+}

+ 0 - 142
server/src/email/transactional-email.service.ts

@@ -1,142 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import * as fs from 'fs-extra';
-import { createTransport } from 'nodemailer';
-import { default as Mail } from 'nodemailer/lib/mailer';
-import SMTPTransport from 'nodemailer/lib/smtp-transport';
-import * as path from 'path';
-import { normalizeString } from 'shared/normalize-string';
-import { assertNever } from 'shared/shared-utils';
-import * as Stream from 'stream';
-
-import { ConfigService } from '../config/config.service';
-import { EmailTypeConfig, TemplateConfig } from '../config/email/email-options';
-import { FileTransportOptions } from '../config/email/email-transport-options';
-import { EventBus } from '../event-bus/event-bus';
-import { VendureEvent } from '../event-bus/vendure-event';
-
-import { EmailContext, GeneratedEmailContext } from './email-context';
-
-export type StreamTransportInfo = {
-    envelope: {
-        from: string;
-        to: string[];
-    };
-    messageId: string;
-    message: Stream;
-};
-
-@Injectable()
-export class TransactionalEmailService {
-    constructor(private configService: ConfigService, private eventBus: EventBus) {}
-
-    async init() {
-        const { emailTypes } = this.configService.emailOptions;
-        for (const [type, config] of Object.entries(emailTypes)) {
-            this.eventBus.subscribe(config.triggerEvent, event => {
-                return this.handleEvent(type, config, event);
-            });
-        }
-        if (this.configService.emailOptions.transport.type === 'file') {
-            // ensure the configured directory exists before
-            // we attempt to write files to it
-            const emailPath = this.configService.emailOptions.transport.outputPath;
-            await fs.ensureDir(emailPath);
-        }
-    }
-
-    private async handleEvent(type: string, config: EmailTypeConfig<any>, event: VendureEvent) {
-        const { generator } = this.configService.emailOptions;
-        const contextConfig = config.createContext(event);
-        if (contextConfig) {
-            const emailContext = new EmailContext({
-                ...contextConfig,
-                type,
-                event,
-            });
-            const templateConfig = this.getTemplateConfig(type, emailContext);
-            const templateContents = await this.loadTemplateContents(templateConfig.templatePath);
-            const templateContext = templateConfig.templateContext(emailContext);
-            const generatedEmailContext = await generator.generate(
-                templateConfig.subject,
-                templateContents,
-                templateContext,
-            );
-            await this.send(generatedEmailContext);
-        }
-    }
-
-    /**
-     * Returns the corresponding TemplateConfig based on the channelCode and languageCode of the
-     * EmailContext object.
-     */
-    private getTemplateConfig(type: string, context: EmailContext): TemplateConfig {
-        const { emailTypes } = this.configService.emailOptions;
-        const typeConfig = emailTypes[type].templates;
-        const channelConfig = typeConfig[context.channelCode] || typeConfig.defaultChannel;
-        const languageConfig = channelConfig[context.languageCode] || channelConfig.defaultLanguage;
-        return languageConfig;
-    }
-
-    private loadTemplateContents(filePath: string): Promise<string> {
-        return fs.readFile(filePath, 'utf-8');
-    }
-
-    private async send(email: GeneratedEmailContext) {
-        const { transport } = this.configService.emailOptions;
-        let transporter: Mail;
-        switch (transport.type) {
-            case 'none':
-                return;
-                break;
-            case 'file':
-                transporter = createTransport({
-                    streamTransport: true,
-                });
-                const result = await this.sendMail(email, transporter);
-                const fileName = normalizeString(
-                    `${new Date().toISOString()} ${result.envelope.to[0]} ${email.subject}`,
-                    '_',
-                );
-                const filePath = path.join(transport.outputPath, fileName);
-                await this.writeToFile(filePath, result);
-                break;
-            case 'sendmail':
-                transporter = createTransport({
-                    sendmail: true,
-                    path: transport.path,
-                });
-                await this.sendMail(email, transporter);
-                break;
-            case 'smtp':
-                transporter = createTransport({
-                    host: transport.host,
-                    port: transport.port,
-                    secure: transport.secure,
-                    auth: transport.auth.user,
-                } as SMTPTransport.Options);
-                await this.sendMail(email, transporter);
-                break;
-            default:
-                return assertNever(transport);
-        }
-    }
-
-    private async sendMail(email: GeneratedEmailContext, transporter: Mail): Promise<any> {
-        return transporter.sendMail({
-            to: email.recipient,
-            subject: email.subject,
-            html: email.body,
-        });
-    }
-
-    private async writeToFile(filePath: string, info: StreamTransportInfo): Promise<string> {
-        const writeStream = fs.createWriteStream(filePath);
-        return new Promise<string>((resolve, reject) => {
-            writeStream.on('open', () => {
-                info.message.pipe(writeStream);
-                writeStream.on('close', resolve);
-                writeStream.on('error', reject);
-            });
-        });
-    }
-}