Răsfoiți Sursa

Merge branch 'major' of github.com:vendure-ecommerce/vendure into major

Michael Bromley 2 ani în urmă
părinte
comite
3ca0c901df

+ 15 - 1
packages/email-plugin/src/common.ts

@@ -1,7 +1,21 @@
-import { EmailPluginDevModeOptions, EmailPluginOptions } from './types';
+import { Injector, RequestContext } from '@vendure/core';
+
+import { EmailPluginDevModeOptions, EmailPluginOptions, EmailTransportOptions } from './types';
 
 export function isDevModeOptions(
     input: EmailPluginOptions | EmailPluginDevModeOptions,
 ): input is EmailPluginDevModeOptions {
     return (input as EmailPluginDevModeOptions).devMode === true;
 }
+
+export async function resolveTransportSettings(
+    options: EmailPluginOptions,
+    injector: Injector,
+    ctx?: RequestContext
+): Promise<EmailTransportOptions> {
+    if (typeof options.transport === 'function') {
+        return options.transport(injector, ctx);
+    } else {
+        return options.transport;
+    }
+}

+ 49 - 13
packages/email-plugin/src/email-processor.ts

@@ -1,16 +1,24 @@
 import { Inject, Injectable } from '@nestjs/common';
-import { InternalServerError, Logger } from '@vendure/core';
+import { ModuleRef } from '@nestjs/core';
+import { Ctx, Injector, InternalServerError, Logger, RequestContext } from '@vendure/core';
 import fs from 'fs-extra';
 
 import { deserializeAttachments } from './attachment-utils';
-import { isDevModeOptions } from './common';
+import { isDevModeOptions, resolveTransportSettings } from './common';
 import { EMAIL_PLUGIN_OPTIONS, loggerCtx } from './constants';
 import { EmailGenerator } from './email-generator';
 import { EmailSender } from './email-sender';
 import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
 import { NodemailerEmailSender } from './nodemailer-email-sender';
-import { TemplateLoader } from './template-loader';
-import { EmailDetails, EmailPluginOptions, EmailTransportOptions, IntermediateEmailDetails } from './types';
+import { FileBasedTemplateLoader } from './template-loader';
+import {
+    EmailDetails,
+    EmailPluginOptions,
+    EmailTransportOptions,
+    InitializedEmailPluginOptions,
+    IntermediateEmailDetails,
+    TemplateLoader,
+} from './types';
 
 /**
  * This class combines the template loading, generation, and email sending - the actual "work" of
@@ -19,15 +27,21 @@ import { EmailDetails, EmailPluginOptions, EmailTransportOptions, IntermediateEm
  */
 @Injectable()
 export class EmailProcessor {
-    protected templateLoader: TemplateLoader;
     protected emailSender: EmailSender;
     protected generator: EmailGenerator;
-    protected transport: EmailTransportOptions;
+    protected transport:
+        | EmailTransportOptions
+        | ((
+              injector?: Injector,
+              ctx?: RequestContext,
+          ) => EmailTransportOptions | Promise<EmailTransportOptions>);
 
-    constructor(@Inject(EMAIL_PLUGIN_OPTIONS) protected options: EmailPluginOptions) {}
+    constructor(
+        @Inject(EMAIL_PLUGIN_OPTIONS) protected options: InitializedEmailPluginOptions,
+        private moduleRef: ModuleRef,
+    ) {}
 
     async init() {
-        this.templateLoader = new TemplateLoader(this.options.templatePath);
         this.emailSender = this.options.emailSender ? this.options.emailSender : new NodemailerEmailSender();
         this.generator = this.options.emailGenerator
             ? this.options.emailGenerator
@@ -44,22 +58,31 @@ export class EmailProcessor {
         } else {
             if (!this.options.transport) {
                 throw new InternalServerError(
-                    'When devMode is not set to true, the \'transport\' property must be set.',
+                    "When devMode is not set to true, the 'transport' property must be set.",
                 );
             }
             this.transport = this.options.transport;
         }
-        if (this.transport.type === 'file') {
+        const transport = await this.getTransportSettings();
+        if (transport.type === 'file') {
             // ensure the configured directory exists before
             // we attempt to write files to it
-            const emailPath = this.transport.outputPath;
+            const emailPath = transport.outputPath;
             await fs.ensureDir(emailPath);
         }
     }
 
     async process(data: IntermediateEmailDetails) {
         try {
-            const bodySource = await this.templateLoader.loadTemplate(data.type, data.templateFile);
+            const ctx = RequestContext.deserialize(data.ctx);
+            const bodySource = await this.options.templateLoader.loadTemplate(
+                new Injector(this.moduleRef),
+                ctx,
+                {
+                    templateName: data.templateFile,
+                    type: data.type,
+                },
+            );
             const generated = this.generator.generate(data.from, data.subject, bodySource, data.templateVars);
             const emailDetails: EmailDetails = {
                 ...generated,
@@ -69,7 +92,8 @@ export class EmailProcessor {
                 bcc: data.bcc,
                 replyTo: data.replyTo,
             };
-            await this.emailSender.send(emailDetails, this.transport);
+            const transportSettings = await this.getTransportSettings(ctx);
+            await this.emailSender.send(emailDetails, transportSettings);
             return true;
         } catch (err: unknown) {
             if (err instanceof Error) {
@@ -80,4 +104,16 @@ export class EmailProcessor {
             throw err;
         }
     }
+
+    async getTransportSettings(ctx?: RequestContext): Promise<EmailTransportOptions> {
+        if (isDevModeOptions(this.options)) {
+            return {
+                type: 'file',
+                raw: false,
+                outputPath: this.options.outputPath,
+            };
+        } else {
+            return resolveTransportSettings(this.options, new Injector(this.moduleRef), ctx);
+        }
+    }
 }

+ 11 - 7
packages/email-plugin/src/event-handler.ts

@@ -144,7 +144,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     };
     private _mockEvent: Omit<Event, 'ctx' | 'data'> | undefined;
 
-    constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
+    constructor(public listener: EmailEventListener<T>, public event: Type<Event>) { }
 
     /** @internal */
     get type(): T {
@@ -268,6 +268,9 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
      * @description
      * Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
      * templates for channels or languageCodes other than the default.
+     * 
+     * @deprecated Define a custom TemplateLoader on plugin initalization to define templates based on the RequestContext.
+     * E.g. `EmailPlugin.init({ templateLoader: new CustomTemplateLoader() })`
      */
     addTemplate(config: EmailTemplateConfig): EmailEventHandler<T, Event> {
         this.configurations.push(config);
@@ -346,14 +349,14 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         }
         if (!this.setRecipientFn) {
             throw new Error(
-                'No setRecipientFn has been defined. ' +
-                    `Remember to call ".setRecipient()" when setting up the EmailEventHandler for ${this.type}`,
+                `No setRecipientFn has been defined. ` +
+                `Remember to call ".setRecipient()" when setting up the EmailEventHandler for ${this.type}`,
             );
         }
         if (this.from === undefined) {
             throw new Error(
-                'No from field has been defined. ' +
-                    `Remember to call ".setFrom()" when setting up the EmailEventHandler for ${this.type}`,
+                `No from field has been defined. ` +
+                `Remember to call ".setFrom()" when setting up the EmailEventHandler for ${this.type}`,
             );
         }
         const { ctx } = event;
@@ -362,8 +365,8 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         const subject = configuration ? configuration.subject : this.defaultSubject;
         if (subject == null) {
             throw new Error(
-                'No subject field has been defined. ' +
-                    `Remember to call ".setSubject()" when setting up the EmailEventHandler for ${this.type}`,
+                `No subject field has been defined. ` +
+                `Remember to call ".setSubject()" when setting up the EmailEventHandler for ${this.type}`,
             );
         }
         const recipient = this.setRecipientFn(event);
@@ -377,6 +380,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         const attachments = await serializeAttachments(attachmentsArray);
         const optionalAddressFields = (await this.setOptionalAddressFieldsFn?.(event)) ?? {};
         return {
+            ctx: event.ctx.serialize(),
             type: this.type,
             recipient,
             from: this.from,

+ 11 - 12
packages/email-plugin/src/handlebars-mjml-generator.ts

@@ -5,7 +5,12 @@ import mjml2html from 'mjml';
 import path from 'path';
 
 import { EmailGenerator } from './email-generator';
-import { EmailPluginDevModeOptions, EmailPluginOptions } from './types';
+import {
+    EmailPluginDevModeOptions,
+    EmailPluginOptions,
+    InitializedEmailPluginOptions,
+    Partial,
+} from './types';
 
 /**
  * @description
@@ -16,9 +21,11 @@ import { EmailPluginDevModeOptions, EmailPluginOptions } from './types';
  * @docsPage EmailGenerator
  */
 export class HandlebarsMjmlGenerator implements EmailGenerator {
-    onInit(options: EmailPluginOptions | EmailPluginDevModeOptions) {
-        const partialsPath = path.join(options.templatePath, 'partials');
-        this.registerPartials(partialsPath);
+    async onInit(options: InitializedEmailPluginOptions) {
+        if (options.templateLoader.loadPartials) {
+            const partials = await options.templateLoader.loadPartials();
+            partials.forEach(({ name, content }) => Handlebars.registerPartial(name, content));
+        }
         this.registerHelpers();
     }
 
@@ -38,14 +45,6 @@ export class HandlebarsMjmlGenerator implements EmailGenerator {
         return { from: fromResult, subject: subjectResult, body };
     }
 
-    private registerPartials(partialsPath: string) {
-        const partialsFiles = fs.readdirSync(partialsPath);
-        for (const partialFile of partialsFiles) {
-            const partialContent = fs.readFileSync(path.join(partialsPath, partialFile), 'utf-8');
-            Handlebars.registerPartial(path.basename(partialFile, '.hbs'), partialContent);
-        }
-    }
-
     private registerHelpers() {
         Handlebars.registerHelper('formatDate', (date: Date | undefined, format: string | object) => {
             if (!date) {

+ 1 - 1
packages/email-plugin/src/mock-events.ts

@@ -51,7 +51,7 @@ export const mockOrderStateTransitionEvent = new OrderStateTransitionEvent(
                     {
                         adjustmentSource: 'Promotion:1',
                         type: AdjustmentType.PROMOTION,
-                        amount: -1000,
+                        amount: -1000 as any,
                         description: '$10 off computer equipment',
                     },
                 ],

+ 52 - 1
packages/email-plugin/src/plugin.spec.ts

@@ -3,9 +3,12 @@ import { Test, TestingModule } from '@nestjs/testing';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import {
+    DefaultLogger,
     EventBus,
+    Injector,
     LanguageCode,
     Logger,
+    LogLevel,
     Order,
     OrderStateTransitionEvent,
     PluginCommonModule,
@@ -18,8 +21,8 @@ import { createReadStream, readFileSync } from 'fs';
 import path from 'path';
 import { Readable } from 'stream';
 import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
-
 import { orderConfirmationHandler } from './default-email-handlers';
+import { EmailProcessor } from './email-processor';
 import { EmailSender } from './email-sender';
 import { EmailEventHandler } from './event-handler';
 import { EmailEventListener } from './event-listener';
@@ -859,6 +862,54 @@ describe('EmailPlugin', () => {
             expect(onSend.mock.calls[0][0].replyTo).toBe('foo@bar.com');
         });
     });
+
+    describe('Dynamic transport settings', () => {
+        let injectorArg: Injector | undefined;
+        let ctxArg: RequestContext | undefined;
+
+        it('Initializes with async transport settings', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setFrom('"test from" <noreply@test.com>')
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello')
+                .setTemplateVars(event => ({ subjectVar: 'foo' }));
+            module = await initPluginWithHandlers([handler], {
+                transport: async (injector, ctx) => {
+                    injectorArg = injector;
+                    ctxArg = ctx;
+                    return {
+                        type: 'testing',
+                        onSend: () => {},
+                    }
+                }
+            });
+            const ctx = RequestContext.deserialize({
+                _channel: { code: DEFAULT_CHANNEL_CODE },
+                _languageCode: LanguageCode.en,
+            } as any);
+            module!.get(EventBus).publish(new MockEvent(ctx, true));
+            await pause();
+            expect(module).toBeDefined();
+            expect(typeof (module.get(EmailPlugin) as any).options.transport).toBe('function');
+        });
+
+        it('Passes injector and context to transport function', async () => {
+            const ctx = RequestContext.deserialize({
+                _channel: { code: DEFAULT_CHANNEL_CODE },
+                _languageCode: LanguageCode.en,
+            } as any);
+            module!.get(EventBus).publish(new MockEvent(ctx, true));
+            await pause();
+            expect(injectorArg?.constructor.name).toBe('Injector');
+            expect(ctxArg?.constructor.name).toBe('RequestContext');
+        });
+
+        it('Resolves async transport settings', async () => {
+            const transport = await module!.get(EmailProcessor).getTransportSettings();
+            expect(transport.type).toBe('testing');
+        });
+    });
 });
 
 class FakeCustomSender implements EmailSender {

+ 62 - 7
packages/email-plugin/src/plugin.ts

@@ -15,19 +15,25 @@ import {
     PluginCommonModule,
     ProcessContext,
     registerPluginStartupMessage,
+    RequestContext,
     Type,
+    UserInputError,
     VendurePlugin,
 } from '@vendure/core';
+import Module from 'module';
 
-import { isDevModeOptions } from './common';
+import { isDevModeOptions, resolveTransportSettings } from './common';
 import { EMAIL_PLUGIN_OPTIONS, loggerCtx } from './constants';
 import { DevMailbox } from './dev-mailbox';
 import { EmailProcessor } from './email-processor';
 import { EmailEventHandler, EmailEventHandlerWithAsyncData } from './event-handler';
+import { FileBasedTemplateLoader } from './template-loader';
 import {
     EmailPluginDevModeOptions,
     EmailPluginOptions,
+    EmailTransportOptions,
     EventWithContext,
+    InitializedEmailPluginOptions,
     IntermediateEmailDetails,
 } from './types';
 
@@ -91,6 +97,14 @@ import {
  * `node_modules/\@vendure/email-plugin/templates` to a location of your choice, and then point the `templatePath` config
  * property at that directory.
  *
+ * * ### Dynamic Email Templates
+ * Instead of passing a static value to `templatePath`, use `templateLoader` to define a template path.
+ * ```ts
+ *   EmailPlugin.init({
+ *    ...,
+ *    templateLoader: new FileBasedTemplateLoader(my/order-confirmation/templates)
+ *   })
+ * ```
  * ## Customizing templates
  *
  * Emails are generated from templates which use [MJML](https://mjml.io/) syntax. MJML is an open-source HTML-like markup
@@ -178,6 +192,36 @@ import {
  *
  * For all available methods of extending a handler, see the {@link EmailEventHandler} documentation.
  *
+ * ## Dynamic SMTP settings
+ *
+ * Instead of defining static transport settings, you can also provide a function that dynamically resolves
+ * channel aware transport settings.
+ *
+ * @example
+ * ```ts
+ * import { defaultEmailHandlers, EmailPlugin } from '\@vendure/email-plugin';
+ * import { MyTransportService } from './transport.services.ts';
+ * const config: VendureConfig = {
+ *   plugins: [
+ *     EmailPlugin.init({
+ *       handlers: defaultEmailHandlers,
+ *       templatePath: path.join(__dirname, 'static/email/templates'),
+ *       transport: (injector, ctx) => {
+ *         if (ctx) {
+ *           return injector.get(MyTransportService).getSettings(ctx);
+ *         } else {
+ *           return {
+                type: 'smtp',
+                host: 'smtp.example.com',
+                // ... etc.
+              }
+ *         }
+ *       }
+ *     }),
+ *   ],
+ * };
+ * ```
+ *
  * ## Dev mode
  *
  * For development, the `transport` option can be replaced by `devMode: true`. Doing so configures Vendure to use the
@@ -236,7 +280,7 @@ import {
     compatibility: '^2.0.0-beta.0',
 })
 export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdown, NestModule {
-    private static options: EmailPluginOptions | EmailPluginDevModeOptions;
+    private static options: InitializedEmailPluginOptions;
     private devMailbox: DevMailbox | undefined;
     private jobQueue: JobQueue<IntermediateEmailDetails> | undefined;
     private testingProcessor: EmailProcessor | undefined;
@@ -248,14 +292,24 @@ export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdow
         private emailProcessor: EmailProcessor,
         private jobQueueService: JobQueueService,
         private processContext: ProcessContext,
-        @Inject(EMAIL_PLUGIN_OPTIONS) private options: EmailPluginOptions,
-    ) {}
+        @Inject(EMAIL_PLUGIN_OPTIONS) private options: InitializedEmailPluginOptions,
+    ) { }
 
     /**
      * Set the plugin options.
      */
     static init(options: EmailPluginOptions | EmailPluginDevModeOptions): Type<EmailPlugin> {
-        this.options = options;
+        if (options.templateLoader) {
+            Logger.info(`Using custom template loader '${options.templateLoader.constructor.name}'`);
+        } else if (!options.templateLoader && options.templatePath) {
+            // TODO: this else-if can be removed when deprecated templatePath is removed, 
+            //       because we will either have a custom template loader, or the default loader with a default path
+            options.templateLoader = new FileBasedTemplateLoader(options.templatePath);
+        } else {
+            throw new
+            Error('You must either supply a templatePath or provide a custom templateLoader');
+        }
+        this.options = options as InitializedEmailPluginOptions;
         return EmailPlugin;
     }
 
@@ -263,10 +317,11 @@ export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdow
     async onApplicationBootstrap(): Promise<void> {
         await this.initInjectableStrategies();
         await this.setupEventSubscribers();
-        if (!isDevModeOptions(this.options) && this.options.transport.type === 'testing') {
+        const transport = await resolveTransportSettings(this.options, new Injector(this.moduleRef));
+        if (!isDevModeOptions(this.options) && transport.type === 'testing') {
             // When running tests, we don't want to go through the JobQueue system,
             // so we just call the email sending logic directly.
-            this.testingProcessor = new EmailProcessor(this.options);
+            this.testingProcessor = new EmailProcessor(this.options, this.moduleRef);
             await this.testingProcessor.init();
         } else {
             await this.emailProcessor.init();

+ 22 - 10
packages/email-plugin/src/template-loader.ts

@@ -1,20 +1,32 @@
-import { LanguageCode } from '@vendure/common/lib/generated-types';
-import fs from 'fs-extra';
+import { Injector, RequestContext } from '@vendure/core';
+import fs from 'fs/promises';
 import path from 'path';
+import { LoadTemplateInput, Partial, TemplateLoader } from './types';
 
 /**
  * Loads email templates according to the configured TemplateConfig values.
  */
-export class TemplateLoader {
-    constructor(private templatePath: string) {}
+export class FileBasedTemplateLoader implements TemplateLoader {
+
+    constructor(private templatePath: string) { }
 
     async loadTemplate(
-        type: string,
-        templateFileName: string,
+        _injector: Injector,
+        _ctx: RequestContext,
+        { type, templateName }: LoadTemplateInput,
     ): Promise<string> {
-        // TODO: logic to select other files based on channel / language
-        const templatePath = path.join(this.templatePath, type, templateFileName);
-        const body = await fs.readFile(templatePath, 'utf-8');
-        return body;
+        const templatePath = path.join(this.templatePath, type, templateName);
+        return fs.readFile(templatePath, 'utf-8');
+    }
+
+    async loadPartials(): Promise<Partial[]> {
+        const partialsPath = path.join(this.templatePath, 'partials');
+        const partialsFiles = await fs.readdir(partialsPath);
+        return Promise.all(partialsFiles.map(async (file) => {
+            return {
+                name: path.basename(file, '.hbs'),
+                content: await fs.readFile(path.join(partialsPath, file), 'utf-8')
+            }
+        }));
     }
 }

+ 69 - 6
packages/email-plugin/src/types.ts

@@ -1,6 +1,6 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { Omit } from '@vendure/common/lib/omit';
-import { Injector, RequestContext, VendureEvent } from '@vendure/core';
+import { Injector, RequestContext, SerializedRequestContext, VendureEvent } from '@vendure/core';
 import { Attachment } from 'nodemailer/lib/mailer';
 import SESTransport from 'nodemailer/lib/ses-transport'
 import SMTPTransport from 'nodemailer/lib/smtp-transport';
@@ -42,13 +42,23 @@ export interface EmailPluginOptions {
      * @description
      * The path to the location of the email templates. In a default Vendure installation,
      * the templates are installed to `<project root>/vendure/email/templates`.
+     * 
+     * @deprecated Use `templateLoader` to define a template path: `templateLoader: new FileBasedTemplateLoader('../your-path/templates')`
      */
-    templatePath: string;
+    templatePath?: string;
+    /**
+     * @description
+     * An optional TemplateLoader which can be used to load templates from a custom location or async service.
+     * The default uses the FileBasedTemplateLoader which loads templates from `<project root>/vendure/email/templates`
+     *
+     * @since 2.0.0
+     */
+    templateLoader?: TemplateLoader;
     /**
      * @description
      * Configures how the emails are sent.
      */
-    transport: EmailTransportOptions;
+    transport: EmailTransportOptions | ((injector?: Injector, ctx?: RequestContext) => EmailTransportOptions | Promise<EmailTransportOptions>)
     /**
      * @description
      * An array of {@link EmailEventHandler}s which define which Vendure events will trigger
@@ -80,6 +90,11 @@ export interface EmailPluginOptions {
     emailGenerator?: EmailGenerator;
 }
 
+/**
+ * EmailPLuginOptions type after initialization, where templateLoader is no longer optional
+ */
+export type InitializedEmailPluginOptions = EmailPluginOptions & { templateLoader: TemplateLoader };
+
 /**
  * @description
  * Configuration for running the EmailPlugin in development mode.
@@ -285,6 +300,7 @@ export type SerializedAttachment = OptionalToNullable<
 >;
 
 export type IntermediateEmailDetails = {
+    ctx: SerializedRequestContext;
     type: string;
     from: string;
     recipient: string;
@@ -301,9 +317,8 @@ export type IntermediateEmailDetails = {
  * @description
  * Configures the {@link EmailEventHandler} to handle a particular channel & languageCode
  * combination.
- *
- * @docsCategory EmailPlugin
- * @docsPage Email Plugin Types
+ * 
+ * @deprecated Use a custom {@link TemplateLoader} instead. 
  */
 export interface EmailTemplateConfig {
     /**
@@ -331,6 +346,54 @@ export interface EmailTemplateConfig {
     subject: string;
 }
 
+export interface LoadTemplateInput {
+    type: string,
+    templateName: string
+}
+
+export interface Partial {
+    name: string,
+    content: string
+}
+
+/**
+ * @description
+ * Load an email template based on the given request context, type and template name
+ * and return the template as a string.
+ * 
+ * @example
+ * ```TypeScript
+ * import { EmailPlugin, TemplateLoader } from '@vendure/email-plugin';
+ * 
+ * class MyTemplateLoader implements TemplateLoader {
+ *      loadTemplate(injector, ctx, { type, templateName }){
+ *          return myCustomTemplateFunction(ctx);
+ *      }
+ * }
+ * 
+ * // In vendure-config.ts:
+ * ...
+ * EmailPlugin.init({
+ *     templateLoader: new MyTemplateLoader()
+ *     ...
+ * })
+ * ```
+ *
+ * @docsCategory EmailPlugin
+ * @docsPage Custom Template Loader
+ */
+export interface TemplateLoader {
+    /**
+     * Load template and return it's content as a string
+     */
+    loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;
+    /**
+     * Load partials and return their contents. 
+     * This method is only called during initalization, i.e. during server startup. 
+     */
+    loadPartials?(): Promise<Partial[]>;
+}
+
 /**
  * @description
  * A function used to define template variables available to email templates.