Sfoglia il codice sorgente

feat(email-plugin): Simplify email config API

Relates to #88
Michael Bromley 6 anni fa
parent
commit
d35420a47e

+ 2 - 1
packages/dev-server/dev-config.ts

@@ -6,7 +6,7 @@ import {
     examplePaymentHandler,
     VendureConfig,
 } from '@vendure/core';
-import { EmailPlugin } from '@vendure/email-plugin';
+import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import path from 'path';
 
 /**
@@ -58,6 +58,7 @@ export const devConfig: VendureConfig = {
         new DefaultSearchPlugin(),
         new EmailPlugin({
             devMode: true,
+            handlers: defaultEmailHandlers,
             templatePath: path.join(__dirname, '../email-plugin/templates'),
             outputPath: path.join(__dirname, 'test-emails'),
         }),

+ 1 - 2
packages/email-plugin/index.ts

@@ -1,5 +1,4 @@
-export * from './src/default-email-types';
-export * from './src/email-context';
+export * from './src/default-email-handlers';
 export * from './src/email-sender';
 export * from './src/handlebars-mjml-generator';
 export * from './src/noop-email-generator';

+ 1 - 0
packages/email-plugin/package.json

@@ -6,6 +6,7 @@
   "types": "lib/index.d.ts",
   "files": ["lib/**/*", "templates/**/*"],
   "scripts": {
+    "watch": "tsc -p ./tsconfig.build.json --watch",
     "build": "rimraf lib && tsc -p ./tsconfig.build.json"
   },
   "publishConfig": {

+ 41 - 0
packages/email-plugin/src/default-email-handlers.ts

@@ -0,0 +1,41 @@
+/* tslint:disable:no-non-null-assertion */
+import { AccountRegistrationEvent, OrderStateTransitionEvent, PasswordResetEvent } from '@vendure/core';
+
+import { EmailEventListener } from './event-listener';
+
+export const orderConfirmationHandler = new EmailEventListener('order-confirmation')
+    .on(OrderStateTransitionEvent)
+    .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
+    .configure({
+        setRecipient: event => event.order.customer!.emailAddress,
+        subject: `Order confirmation for #{{ order.code }}`,
+        templateVars: event => ({ order: event.order }),
+    });
+
+export const emailVerificationHandler = new EmailEventListener('email-verification')
+    .on(AccountRegistrationEvent)
+    .configure({
+        setRecipient: event => event.user.identifier,
+        subject: `Please verify your email address`,
+        templateVars: event => ({
+            user: event.user,
+            verifyUrl: 'verify',
+        }),
+    });
+
+export const passwordResetHandler = new EmailEventListener('password-reset')
+    .on(PasswordResetEvent)
+    .configure({
+        setRecipient: event => event.user.identifier,
+        subject: `Forgotten password reset`,
+        templateVars: event => ({
+            user: event.user,
+            passwordResetUrl: 'reset-password',
+        }),
+    });
+
+export const defaultEmailHandlers = [
+    orderConfirmationHandler,
+    emailVerificationHandler,
+    passwordResetHandler,
+];

+ 0 - 81
packages/email-plugin/src/default-email-types.ts

@@ -1,81 +0,0 @@
-import { AccountRegistrationEvent, OrderStateTransitionEvent, PasswordResetEvent } from '@vendure/core';
-
-import { configEmailType, EmailTypes } from './types';
-
-/**
- * @description
- * The possible types of email which are configured by default. These define the keys of the
- * {@link EmailTypes} object.
- *
- * @docsCategory EmailPlugin
- */
-export type DefaultEmailType = 'order-confirmation' | 'email-verification' | 'password-reset';
-
-export const defaultEmailTypes: EmailTypes<DefaultEmailType> = {
-    'order-confirmation': configEmailType({
-        triggerEvent: OrderStateTransitionEvent,
-        createContext: e => {
-            const customer = e.order.customer;
-            if (customer && e.toState === 'PaymentSettled') {
-                return {
-                    recipient: customer.emailAddress,
-                    languageCode: e.ctx.languageCode,
-                    channelCode: e.ctx.channel.code,
-                };
-            }
-        },
-        templates: {
-            defaultChannel: {
-                defaultLanguage: {
-                    templateContext: emailContext => ({ order: emailContext.event.order }),
-                    subject: `Order confirmation for #{{ order.code }}`,
-                    templatePath: 'order-confirmation/order-confirmation.hbs',
-                },
-            },
-        },
-    }),
-    'email-verification': configEmailType({
-        triggerEvent: AccountRegistrationEvent,
-        createContext: e => {
-            return {
-                recipient: e.user.identifier,
-                languageCode: e.ctx.languageCode,
-                channelCode: e.ctx.channel.code,
-            };
-        },
-        templates: {
-            defaultChannel: {
-                defaultLanguage: {
-                    templateContext: emailContext => ({
-                        user: emailContext.event.user,
-                        verifyUrl: emailContext.templateVars.shopUrl + 'verify',
-                    }),
-                    subject: `Please verify your email address`,
-                    templatePath: 'email-verification/email-verification.hbs',
-                },
-            },
-        },
-    }),
-    'password-reset': configEmailType({
-        triggerEvent: PasswordResetEvent,
-        createContext: e => {
-            return {
-                recipient: e.user.identifier,
-                languageCode: e.ctx.languageCode,
-                channelCode: e.ctx.channel.code,
-            };
-        },
-        templates: {
-            defaultChannel: {
-                defaultLanguage: {
-                    templateContext: emailContext => ({
-                        user: emailContext.event.user,
-                        passwordResetUrl: emailContext.templateVars.shopUrl + 'reset-password',
-                    }),
-                    subject: `Forgotten password reset`,
-                    templatePath: 'password-reset/password-reset.hbs',
-                },
-            },
-        },
-    }),
-};

+ 0 - 82
packages/email-plugin/src/email-context.ts

@@ -1,82 +0,0 @@
-import { LanguageCode } from '@vendure/common/lib/generated-types';
-import { VendureEvent } from '@vendure/core';
-
-/**
- * @description
- * The EmailContext contains all the neccesary data required to generate an email.
- * It is used in the `templateContext` method of the {@link TemplateConfig} object
- * to define which data get passed to the email template engine for interpolation.
- *
- * @docsCategory EmailPlugin
- */
-export class EmailContext<T extends string = any, E extends VendureEvent = any> {
-    /**
-     * @description
-     * A string defining the type of email, e.g. "order-confirmation". See {@link DefaultEmailType}
-     * for the default types available.
-     */
-    public readonly type: T;
-    /**
-     * @description
-     * The email address of the email recipient.
-     */
-    public readonly recipient: string;
-    /**
-     * @description
-     * The {@link VendureEvent} which triggered this email
-     */
-    public readonly event: E;
-    /**
-     * @description
-     * The code of the active language when the even was fired.
-     */
-    public readonly languageCode: LanguageCode;
-    /**
-     * @description
-     * The code of the Channel from which the event triggering the email was
-     * fired.
-     */
-    public readonly channelCode: string;
-    /**
-     * @description
-     * The variables defined in the {@link EmailOptions} which can be used in
-     * the email templates.
-     */
-    public readonly templateVars: { [name: string]: any };
-
-    constructor(options: {
-        type: T;
-        languageCode: LanguageCode;
-        channelCode: string;
-        recipient: string;
-        event: E;
-        templateVars: { [name: string]: any };
-    }) {
-        const { type, recipient, event, languageCode, channelCode, templateVars } = options;
-        this.type = type;
-        this.languageCode = languageCode;
-        this.channelCode = channelCode;
-        this.recipient = recipient;
-        this.event = event;
-        this.templateVars = templateVars;
-    }
-}
-
-/**
- *
- *
- * @docsCateogry EmailPlugin
- */
-export class GeneratedEmailContext<T extends string = any, E extends VendureEvent = any> extends EmailContext<
-    T,
-    E
-> {
-    public readonly subject: string;
-    public readonly body: string;
-
-    constructor(context: EmailContext<T, E>, subject: string, body: string) {
-        super(context);
-        this.subject = subject;
-        this.body = body;
-    }
-}

+ 8 - 6
packages/email-plugin/src/email-sender.ts

@@ -7,8 +7,7 @@ import SMTPTransport from 'nodemailer/lib/smtp-transport';
 import path from 'path';
 import { Stream } from 'stream';
 
-import { GeneratedEmailContext } from './email-context';
-import { EmailTransportOptions } from './types';
+import { EmailDetails, EmailTransportOptions } from './types';
 
 export type StreamTransportInfo = {
     envelope: {
@@ -19,8 +18,11 @@ export type StreamTransportInfo = {
     message: Stream;
 };
 
+/**
+ * Uses the configured transport to send the generated email.
+ */
 export class EmailSender {
-    async send(email: GeneratedEmailContext, options: EmailTransportOptions) {
+    async send(email: EmailDetails, options: EmailTransportOptions) {
         let transporter: Mail;
         switch (options.type) {
             case 'none':
@@ -62,7 +64,7 @@ export class EmailSender {
         }
     }
 
-    private async sendMail(email: GeneratedEmailContext, transporter: Mail): Promise<any> {
+    private async sendMail(email: EmailDetails, transporter: Mail): Promise<any> {
         return transporter.sendMail({
             to: email.recipient,
             subject: email.subject,
@@ -70,7 +72,7 @@ export class EmailSender {
         });
     }
 
-    private async sendFileHtml(email: GeneratedEmailContext, pathWithoutExt: string) {
+    private async sendFileHtml(email: EmailDetails, pathWithoutExt: string) {
         const content = `<html lang="en">
             <head>
                 <title>${email.subject}</title>
@@ -111,7 +113,7 @@ export class EmailSender {
         await fs.writeFile(pathWithoutExt + '.html', content);
     }
 
-    private async sendFileRaw(email: GeneratedEmailContext, pathWithoutExt: string) {
+    private async sendFileRaw(email: EmailDetails, pathWithoutExt: string) {
         const transporter = createTransport({
             streamTransport: true,
             buffer: true,

+ 79 - 0
packages/email-plugin/src/event-listener.ts

@@ -0,0 +1,79 @@
+import { Type } from '@vendure/common/lib/shared-types';
+import { LanguageCode } from '@vendure/common/src/generated-types';
+
+import { EventWithContext } from './types';
+
+export interface EmailEventHandlerConfig<T extends string, Event extends EventWithContext> {
+    channelCode?: string;
+    languageCode?: LanguageCode;
+    setRecipient: (event: Event) => string;
+    subject: string;
+    templateVars: (event: Event) => { [key: string]: any; };
+}
+
+export class EmailEventListener<T extends string> {
+    public type: T;
+    constructor(type: T) {
+        this.type = type;
+    }
+
+    on<Event extends EventWithContext>(event: Type<Event>) {
+        return new EmailEventHandler<T, Event>(this, event);
+    }
+}
+
+export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
+    private filterFns: Array<(event: Event) => boolean> = [];
+    private configurations: Array<EmailEventHandlerConfig<T, Event>> = [];
+
+    constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
+
+    get type(): T {
+        return this.listener.type;
+    }
+
+    filter(filterFn: (event: Event) => boolean) {
+        this.filterFns.push(filterFn);
+        return this;
+    }
+
+    configure(templateConfig: EmailEventHandlerConfig<T, Event>) {
+        this.configurations.push(templateConfig);
+        return this;
+    }
+
+    handle(event: Event) {
+        for (const filterFn of this.filterFns) {
+            if (!filterFn(event)) {
+                return;
+            }
+        }
+        const { ctx } = event;
+        const configuration = this.getBestConfiguration(ctx.channel.code, ctx.languageCode);
+        const recipient = configuration.setRecipient(event);
+        const templateVars = configuration.templateVars(event);
+        return {
+            recipient,
+            templateVars,
+            subject: configuration.subject,
+        };
+    }
+
+    private getBestConfiguration(channelCode: string, languageCode: LanguageCode) {
+        if ( this.configurations.length === 0) {
+            throw new Error(`This handler has not yet been configured`);
+        }
+        if (this.configurations.length === 1) {
+            return this.configurations[0];
+        }
+        const exactMatch = this.configurations.find(c => c.channelCode === channelCode && c.languageCode === languageCode);
+        if (exactMatch) {
+            return exactMatch;
+        }
+        const channelMatch = this.configurations.find(c => c.channelCode === channelCode && c.languageCode === undefined);
+        if (channelMatch) {
+            return channelMatch;
+        }
+        return this.configurations[0];
+    }
+}

+ 6 - 14
packages/email-plugin/src/handlebars-mjml-generator.ts

@@ -1,25 +1,18 @@
-import { ConfigService, InternalServerError } from '@vendure/core';
 import dateFormat from 'dateformat';
 import fs from 'fs-extra';
 import Handlebars from 'handlebars';
 import mjml2html from 'mjml';
 import path from 'path';
 
-import { EmailContext, GeneratedEmailContext } from './email-context';
-import { EmailGenerator, EmailOptions } from './types';
+import { EmailGenerator, EmailPluginDevModeOptions, EmailPluginOptions } from './types';
 
 /**
  * Uses Handlebars (https://handlebarsjs.com/) to output MJML (https://mjml.io) which is then
  * compiled down to responsive email HTML.
  */
 export class HandlebarsMjmlGenerator implements EmailGenerator {
-    onInit(options: EmailOptions<any>) {
-        if (!options.emailTemplatePath) {
-            throw new InternalServerError(
-                `When using the HandlebarsMjmlGenerator, the emailTemplatePath config option must be set`,
-            );
-        }
-        const partialsPath = path.join(options.emailTemplatePath, 'partials');
+    onInit(options: EmailPluginOptions | EmailPluginDevModeOptions) {
+        const partialsPath = path.join(options.templatePath, 'partials');
         this.registerPartials(partialsPath);
         this.registerHelpers();
     }
@@ -28,14 +21,13 @@ export class HandlebarsMjmlGenerator implements EmailGenerator {
         subject: string,
         template: string,
         templateContext: any,
-        emailContext: EmailContext,
-    ): GeneratedEmailContext {
+    ) {
         const compiledTemplate = Handlebars.compile(template);
         const compiledSubject = Handlebars.compile(subject);
         const subjectResult = compiledSubject(templateContext);
         const mjml = compiledTemplate(templateContext);
-        const bodyResult = mjml2html(mjml);
-        return new GeneratedEmailContext(emailContext, subjectResult, bodyResult.html);
+        const body = mjml2html(mjml).html;
+        return { subject, body };
     }
 
     private registerPartials(partialsPath: string) {

+ 4 - 6
packages/email-plugin/src/noop-email-generator.ts

@@ -1,4 +1,3 @@
-import { EmailContext, GeneratedEmailContext } from './email-context';
 import { EmailGenerator } from './types';
 
 /**
@@ -7,10 +6,9 @@ import { EmailGenerator } from './types';
 export class NoopEmailGenerator implements EmailGenerator {
     generate(
         subject: string,
-        template: string,
-        templateContext: any,
-        context: EmailContext,
-    ): GeneratedEmailContext {
-        return new GeneratedEmailContext(context, subject, template);
+        body: string,
+        templateVars: any,
+    ) {
+        return { subject, body };
     }
 }

+ 34 - 49
packages/email-plugin/src/plugin.ts

@@ -1,13 +1,11 @@
-import { EventBus, InternalServerError, Type, VendureConfig, VendureEvent, VendurePlugin } from '@vendure/core';
+import { EventBus, InternalServerError, Type, VendurePlugin } from '@vendure/core';
 import fs from 'fs-extra';
-import path from 'path';
 
-import { DefaultEmailType, defaultEmailTypes } from './default-email-types';
-import { EmailContext } from './email-context';
 import { EmailSender } from './email-sender';
+import { EmailEventHandler } from './event-listener';
 import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
 import { TemplateLoader } from './template-loader';
-import { EmailOptions, EmailPluginDevModeOptions, EmailPluginOptions, EmailTransportOptions, EmailTypeConfig } from './types';
+import { EmailPluginDevModeOptions, EmailPluginOptions, EmailTransportOptions, EventWithContext } from './types';
 
 /**
  * @description
@@ -89,7 +87,8 @@ import { EmailOptions, EmailPluginDevModeOptions, EmailPluginOptions, EmailTrans
  * ## Dev mode
  *
  * For development, the `transport` option can be replaced by `devMode: true`. Doing so configures Vendure to use the
- * [file transport]({{}}) and outputs emails as rendered HTML files in a directory named "test-emails" which is located adjacent to the directory configured in the `templatePath`.
+ * [file transport]({{}}) and outputs emails as rendered HTML files in a directory named "test-emails" which is located adjacent to the directory
+ * configured in the `templatePath`.
  *
  * ```ts
  * new EmailPlugin({
@@ -101,17 +100,15 @@ import { EmailOptions, EmailPluginDevModeOptions, EmailPluginOptions, EmailTrans
  * @docsCategory EmailPlugin
  */
 export class EmailPlugin implements VendurePlugin {
-    private readonly templatePath: string;
     private readonly transport: EmailTransportOptions;
-    private readonly templateVars: { [name: string]: any };
+    private readonly options: EmailPluginOptions | EmailPluginDevModeOptions;
     private eventBus: EventBus;
     private templateLoader: TemplateLoader;
     private emailSender: EmailSender;
-    private readonly emailOptions: EmailOptions<DefaultEmailType>;
+    private generator: HandlebarsMjmlGenerator;
 
     constructor(options: EmailPluginOptions | EmailPluginDevModeOptions) {
-        this.templatePath = options.templatePath;
-        this.templateVars = options.templateVars || {};
+        this.options = options;
         if (isDevModeOptions(options)) {
             this.transport = {
                 type: 'file',
@@ -126,64 +123,52 @@ export class EmailPlugin implements VendurePlugin {
             }
             this.transport = options.transport;
         }
-        this.emailOptions = {
-            emailTemplatePath: this.templatePath,
-            emailTypes: defaultEmailTypes,
-            generator: new HandlebarsMjmlGenerator(),
-            transport: this.transport,
-            templateVars: this.templateVars,
-        };
     }
 
     async onBootstrap(inject: <T>(type: Type<T>) => T): Promise<void> {
         this.eventBus = inject(EventBus);
-        this.templateLoader = new TemplateLoader(this.emailOptions);
+        this.templateLoader = new TemplateLoader(this.options.templatePath);
         this.emailSender = new EmailSender();
+        this.generator = new HandlebarsMjmlGenerator();
 
         await this.setupEventSubscribers();
-        const { generator } = this.emailOptions;
-        if (generator.onInit) {
-            await generator.onInit.call(generator, this.emailOptions);
+        if (this.generator.onInit) {
+            await this.generator.onInit.call(this.generator, this.options);
         }
     }
 
     async setupEventSubscribers() {
-        const { emailTypes } = this.emailOptions;
-        for (const [type, config] of Object.entries(emailTypes)) {
-            this.eventBus.subscribe(config.triggerEvent, event => {
-                return this.handleEvent(type, config, event);
+        for (const handler of this.options.handlers) {
+            this.eventBus.subscribe(handler.event, event => {
+                return this.handleEvent(handler, event);
             });
         }
-        if (this.emailOptions.transport.type === 'file') {
+        if (this.transport.type === 'file') {
             // ensure the configured directory exists before
             // we attempt to write files to it
-            const emailPath = this.emailOptions.transport.outputPath;
+            const emailPath = this.transport.outputPath;
             await fs.ensureDir(emailPath);
         }
     }
 
-    private async handleEvent(type: string, config: EmailTypeConfig<any>, event: VendureEvent) {
-        const { generator, transport, templateVars } = this.emailOptions;
-        const contextConfig = config.createContext(event);
-        if (contextConfig) {
-            const emailContext = new EmailContext({
-                ...contextConfig,
-                type,
-                event,
-                templateVars: templateVars || {},
-            });
-            const { subject, body, templateContext } = await this.templateLoader.loadTemplate(
-                type,
-                emailContext,
-            );
-            const generatedEmailContext = await generator.generate(
-                subject,
-                body,
-                templateContext,
-                emailContext,
-            );
-            await this.emailSender.send(generatedEmailContext, transport);
+    private async handleEvent(handler: EmailEventHandler, event: EventWithContext) {
+        const { type } = handler;
+        const result = handler.handle(event);
+        if (!result) {
+            return;
         }
+        const bodySource = await this.templateLoader.loadTemplate(
+            type,
+            event.ctx.channel.code,
+            event.ctx.languageCode,
+        );
+        const generated = await this.generator.generate(
+            result.subject,
+            bodySource,
+            result.templateVars,
+        );
+        const emailDetails = { ...generated, recipient: result.recipient };
+        await this.emailSender.send(emailDetails, this.transport);
     }
 }
 

+ 9 - 27
packages/email-plugin/src/template-loader.ts

@@ -1,39 +1,21 @@
+import { LanguageCode } from '@vendure/common/src/generated-types';
 import fs from 'fs-extra';
 import path from 'path';
 
-import { EmailContext } from './email-context';
-import { EmailOptions, TemplateConfig } from './types';
-
 /**
  * Loads email templates according to the configured TemplateConfig values.
  */
 export class TemplateLoader {
-    constructor(private options: EmailOptions<any>) {}
+    constructor(private templatePath: string) {}
 
     async loadTemplate(
         type: string,
-        context: EmailContext,
-    ): Promise<{ templateContext: any; subject: string; body: string }> {
-        const { subject, templateContext, templatePath } = this.getTemplateConfig(type, context);
-        const { emailTemplatePath } = this.options;
-        const body = await fs.readFile(path.join(emailTemplatePath, 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.options;
-        const typeConfig = emailTypes[type].templates;
-        const channelConfig = typeConfig[context.channelCode] || typeConfig.defaultChannel;
-        const languageConfig = channelConfig[context.languageCode] || channelConfig.defaultLanguage;
-        return languageConfig;
+        channelCode: string,
+        languageCode: LanguageCode,
+    ): Promise<string> {
+        // TODO: logic to select other files based on channel / language
+        const templatePath = path.join(this.templatePath, type, 'body.hbs');
+        const body = await fs.readFile(path.join(this.templatePath, type, 'body.hbs'), 'utf-8');
+        return body;
     }
 }

+ 21 - 180
packages/email-plugin/src/types.ts

@@ -1,55 +1,11 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { Omit } from '@vendure/common/lib/omit';
 import { Type } from '@vendure/common/lib/shared-types';
-import { ConfigService, VendureEvent } from '@vendure/core';
+import { RequestContext, VendureEvent } from '@vendure/core';
 
-import { EmailContext, GeneratedEmailContext } from './email-context';
+import { EmailEventHandler } from './event-listener';
 
-/**
- * Defines how transactional emails (account verification, order confirmation etc) are generated and sent.
- *
- * {{% alert %}}
- * It is usually not recommended to configure these yourself.
- * You should use the `DefaultEmailPlugin`.
- * {{% /alert %}}
- */
-export interface EmailOptions<EmailType extends string> {
-    /**
-     * @description
-     * Path to the email template files.
-     *
-     * @default __dirname
-     */
-    emailTemplatePath: string;
-    /**
-     * @description
-     * Configuration for the creation and templating of each email type
-     *
-     * @default {}
-     */
-    emailTypes: EmailTypes<EmailType>;
-    /**
-     * @description
-     * The EmailGenerator uses the EmailContext and template to generate the email body
-     *
-     * @default NoopEmailGenerator
-     */
-    generator: EmailGenerator;
-    /**
-     * @description
-     * Configuration for the transport (email sending) method
-     *
-     * @default NoopTransportOptions
-     */
-    transport: EmailTransportOptions;
-    /**
-     * @description
-     * An object containing any extra variables for use in email templates. For example,
-     * the storefront URL could be defined here for use in password reset emails.
-     *
-     * @default {}
-     */
-    templateVars?: { [name: string]: any };
-}
+export type EventWithContext = VendureEvent & { ctx: RequestContext; };
 
 /**
  * @description
@@ -71,9 +27,10 @@ export interface EmailPluginOptions {
     transport: EmailTransportOptions;
     /**
      * @description
-     * Variables for use in email templates
+     * An array of {@link EmailEventHandler}s which define which Vendure events will trigger
+     * emails, and how those emails are generated.
      */
-    templateVars: { [name: string]: any };
+    handlers: EmailEventHandler[];
 }
 
 /**
@@ -82,11 +39,9 @@ export interface EmailPluginOptions {
  *
  * @docsCategory EmailPlugin
  */
-export interface EmailPluginDevModeOptions {
-    templatePath: string;
+export interface EmailPluginDevModeOptions extends Omit<EmailPluginOptions, 'transport'> {
     outputPath: string;
     devMode: true;
-    templateVars?: { [name: string]: any };
 }
 
 export interface SMTPCredentials {
@@ -189,6 +144,15 @@ export interface NoopTransportOptions {
     type: 'none';
 }
 
+/**
+ * The final, generated email details to be sent.
+ */
+export interface EmailDetails {
+    recipient: string;
+    subject: string;
+    body: string;
+}
+
 /**
  * @description
  * Forwards the raw GeneratedEmailContext object to a provided callback, for use in testing.
@@ -201,7 +165,7 @@ export interface TestingTransportOptions {
      * @description
      * Callback to be invoked when an email would be sent.
      */
-    onSend: (context: GeneratedEmailContext) => void;
+    onSend: (details: EmailDetails) => void;
 }
 
 /**
@@ -217,128 +181,6 @@ export type EmailTransportOptions =
     | NoopTransportOptions
     | TestingTransportOptions;
 
-/**
- * @description
- * This object defines the template location and context data used for interpolation
- * of an email for a particular language of a particular channel.
- *
- * @docsCategory EmailPlugin
- */
-export type TemplateConfig<C = any, R = any> = {
-    /**
-     * @description
-     * A function which uses the {@link EmailContext} to provide a context object for the
-     * template engine. That is, the templates will have access to the object
-     * returned by this function.
-     */
-    templateContext: (emailContext: C) => R;
-    /**
-     * @description
-     * The subject line for the email.
-     */
-    subject: string;
-    /**
-     * @description
-     * The path to the template file for the body of the email.
-     */
-    templatePath: string;
-};
-
-export type TemplateByLanguage<C = any> = { defaultLanguage: TemplateConfig<C> } & {
-    [languageCode: string]: TemplateConfig<C>;
-};
-
-export type TemplateByChannel<C = any> = { defaultChannel: TemplateByLanguage<C> } & {
-    [channelCode: string]: TemplateByLanguage<C>;
-};
-
-export type CreateContextResult = {
-    recipient: string;
-    languageCode: LanguageCode;
-    channelCode: string;
-};
-
-/**
- * @description
- * An object which configures an particular type of transactional email.
- *
- * @docsCategory EmailPlugin
- */
-export type EmailTypeConfig<T extends string, E extends VendureEvent = any> = {
-    /**
-     * @description
-     * Specifies the {@link VendureEvent} which triggers this type of email.
-     */
-    triggerEvent: Type<E>;
-    /**
-     * @description
-     * A function which creates a context object for the email, specifying the recipient
-     * email address, the languageCode of the email and the current Channel.
-     *
-     * A return value of `undefined` means that no email will be generated and sent.
-     */
-    createContext: (event: E) => CreateContextResult | undefined;
-    /**
-     * @description
-     * An object which describes how to resolve the template for the email depending on
-     * the current Channel and LanguageCode.
-     */
-    templates: TemplateByChannel<EmailContext<T, E>>;
-};
-
-/**
- * @description
- * An object describing each possible type of transactional email, plus which events
- * trigger those emails, as well as the location of the templates to handle each
- * email type. Search the repo for the `default-email-types.ts` file for an example of how
- * the email types are defined.
- *
- * When defining an email type, the helper function `configEmailType` may be used to
- * provide better type-safety.
- *
- * @example
- * ```ts
- * export const defaultEmailTypes: EmailTypes<DefaultEmailType> = {
- *  'order-confirmation': configEmailType({
- *    triggerEvent: OrderStateTransitionEvent,
- *    createContext: e => {
- *      const customer = e.order.customer;
- *      if (customer && e.toState === 'PaymentSettled') {
- *        return {
- *          recipient: customer.emailAddress,
- *          languageCode: e.ctx.languageCode,
- *          channelCode: e.ctx.channel.code,
- *        };
- *      }
- *    },
- *    templates: {
- *      defaultChannel: {
- *        defaultLanguage: {
- *          templateContext: emailContext => ({ order: emailContext.event.order }),
- *          subject: `Order confirmation for #{{ order.code }}`,
- *          templatePath: 'order-confirmation/order-confirmation.hbs',
- *        },
- *        de: {
- *          // config for German-language templates
- *        }
- *      },
- *      'other-channel-code': {
- *        // config for a different Channel
- *      }
- *    },
- *  }),
- * ```
- *
- * @docsCategory EmailPlugin
- */
-export type EmailTypes<T extends string> = { [emailType in T]: EmailTypeConfig<T> };
-
-export function configEmailType<T extends string, E extends VendureEvent = VendureEvent>(
-    config: EmailTypeConfig<T, E>,
-) {
-    return config;
-}
-
 /**
  * @description
  * The EmailGenerator uses the {@link EmailContext} and template to generate the email body
@@ -350,7 +192,7 @@ export interface EmailGenerator<T extends string = any, E extends VendureEvent =
      * @description
      * Any neccesary setup can be performed here.
      */
-    onInit?(options: EmailOptions<any>): void | Promise<void>;
+    onInit?(options: EmailPluginOptions): void | Promise<void>;
 
     /**
      * @description
@@ -360,7 +202,6 @@ export interface EmailGenerator<T extends string = any, E extends VendureEvent =
     generate(
         subject: string,
         body: string,
-        templateContext: any,
-        emailContext: EmailContext<T, E>,
-    ): GeneratedEmailContext<T, E> | Promise<GeneratedEmailContext<T, E>>;
+        templateVars: { [key: string]: any; },
+    ): Omit<EmailDetails, 'recipient'>;
 }

+ 0 - 0
packages/email-plugin/templates/email-verification/email-verification.hbs → packages/email-plugin/templates/email-verification/body.hbs


+ 0 - 0
packages/email-plugin/templates/order-confirmation/order-confirmation.hbs → packages/email-plugin/templates/order-confirmation/body.hbs


+ 0 - 0
packages/email-plugin/templates/password-reset/password-reset.hbs → packages/email-plugin/templates/password-reset/body.hbs