Browse Source

feat(email-plugin): Add `.setOptionalAddressFields()` - cc, bcc, replyTo

Fixes #921
Michael Bromley 4 years ago
parent
commit
8e9b72f1ae

+ 5 - 1
packages/email-plugin/src/email-processor.ts

@@ -9,6 +9,7 @@ import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
 import { NodemailerEmailSender } from './nodemailer-email-sender';
 import { NodemailerEmailSender } from './nodemailer-email-sender';
 import { TemplateLoader } from './template-loader';
 import { TemplateLoader } from './template-loader';
 import {
 import {
+    EmailDetails,
     EmailGenerator,
     EmailGenerator,
     EmailPluginOptions,
     EmailPluginOptions,
     EmailSender,
     EmailSender,
@@ -70,10 +71,13 @@ export class EmailProcessor {
                 bodySource,
                 bodySource,
                 data.templateVars,
                 data.templateVars,
             );
             );
-            const emailDetails = {
+            const emailDetails: EmailDetails = {
                 ...generated,
                 ...generated,
                 recipient: data.recipient,
                 recipient: data.recipient,
                 attachments: deserializeAttachments(data.attachments),
                 attachments: deserializeAttachments(data.attachments),
+                cc: data.cc,
+                bcc: data.bcc,
+                replyTo: data.replyTo,
             };
             };
             await this.emailSender.send(emailDetails, this.transport);
             await this.emailSender.send(emailDetails, this.transport);
             return true;
             return true;

+ 29 - 1
packages/email-plugin/src/event-handler.ts

@@ -6,12 +6,14 @@ import { serializeAttachments } from './attachment-utils';
 import { loggerCtx } from './constants';
 import { loggerCtx } from './constants';
 import { EmailEventListener } from './event-listener';
 import { EmailEventListener } from './event-listener';
 import {
 import {
+    EmailAttachment,
     EmailTemplateConfig,
     EmailTemplateConfig,
     EventWithAsyncData,
     EventWithAsyncData,
     EventWithContext,
     EventWithContext,
     IntermediateEmailDetails,
     IntermediateEmailDetails,
     LoadDataFn,
     LoadDataFn,
     SetAttachmentsFn,
     SetAttachmentsFn,
+    SetOptionalAddressFieldsFn,
     SetTemplateVarsFn,
     SetTemplateVarsFn,
 } from './types';
 } from './types';
 
 
@@ -58,10 +60,15 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     private setRecipientFn: (event: Event) => string;
     private setRecipientFn: (event: Event) => string;
     private setTemplateVarsFn: SetTemplateVarsFn<Event>;
     private setTemplateVarsFn: SetTemplateVarsFn<Event>;
     private setAttachmentsFn?: SetAttachmentsFn<Event>;
     private setAttachmentsFn?: SetAttachmentsFn<Event>;
+    private setOptionalAddressFieldsFn?: SetOptionalAddressFieldsFn<Event>;
     private filterFns: Array<(event: Event) => boolean> = [];
     private filterFns: Array<(event: Event) => boolean> = [];
     private configurations: EmailTemplateConfig[] = [];
     private configurations: EmailTemplateConfig[] = [];
     private defaultSubject: string;
     private defaultSubject: string;
     private from: string;
     private from: string;
+    private optionalAddressFields: {
+        cc?: string;
+        bcc?: string;
+    };
     private _mockEvent: Omit<Event, 'ctx' | 'data'> | undefined;
     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>) {}
@@ -89,6 +96,10 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     /**
     /**
      * @description
      * @description
      * A function which defines how the recipient email address should be extracted from the incoming event.
      * A function which defines how the recipient email address should be extracted from the incoming event.
+     *
+     * The recipient can be a plain email address: `'foobar@example.com'`
+     * Or with a formatted name (includes unicode support): `'Ноде Майлер <foobar@example.com>'`
+     * Or a comma-separated list of addresses: `'foobar@example.com, "Ноде Майлер" <bar@example.com>'`
      */
      */
     setRecipient(setRecipientFn: (event: Event) => string): EmailEventHandler<T, Event> {
     setRecipient(setRecipientFn: (event: Event) => string): EmailEventHandler<T, Event> {
         this.setRecipientFn = setRecipientFn;
         this.setRecipientFn = setRecipientFn;
@@ -125,6 +136,14 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         return this;
         return this;
     }
     }
 
 
+    /**
+     * A function which allows {@link OptionalAddressFields} to be specified such as "cc" and "bcc".
+     */
+    setOptionalAddressFields(optionalAddressFieldsFn: SetOptionalAddressFieldsFn<Event>) {
+        this.setOptionalAddressFieldsFn = optionalAddressFieldsFn;
+        return this;
+    }
+
     /**
     /**
      * @description
      * @description
      * Defines one or more files to be attached to the email. An attachment can be specified
      * Defines one or more files to be attached to the email. An attachment can be specified
@@ -196,6 +215,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         asyncHandler.setRecipientFn = this.setRecipientFn;
         asyncHandler.setRecipientFn = this.setRecipientFn;
         asyncHandler.setTemplateVarsFn = this.setTemplateVarsFn;
         asyncHandler.setTemplateVarsFn = this.setTemplateVarsFn;
         asyncHandler.setAttachmentsFn = this.setAttachmentsFn;
         asyncHandler.setAttachmentsFn = this.setAttachmentsFn;
+        asyncHandler.setOptionalAddressFieldsFn = this.setOptionalAddressFieldsFn;
         asyncHandler.filterFns = this.filterFns;
         asyncHandler.filterFns = this.filterFns;
         asyncHandler.configurations = this.configurations;
         asyncHandler.configurations = this.configurations;
         asyncHandler.defaultSubject = this.defaultSubject;
         asyncHandler.defaultSubject = this.defaultSubject;
@@ -258,7 +278,14 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         }
         }
         const recipient = this.setRecipientFn(event);
         const recipient = this.setRecipientFn(event);
         const templateVars = this.setTemplateVarsFn ? this.setTemplateVarsFn(event, globals) : {};
         const templateVars = this.setTemplateVarsFn ? this.setTemplateVarsFn(event, globals) : {};
-        const attachments = await serializeAttachments((await this.setAttachmentsFn?.(event)) ?? []);
+        let attachmentsArray: EmailAttachment[] = [];
+        try {
+            attachmentsArray = (await this.setAttachmentsFn?.(event)) ?? [];
+        } catch (e) {
+            Logger.error(e, loggerCtx, e.stack);
+        }
+        const attachments = await serializeAttachments(attachmentsArray);
+        const optionalAddressFields = (await this.setOptionalAddressFieldsFn?.(event)) ?? {};
         return {
         return {
             type: this.type,
             type: this.type,
             recipient,
             recipient,
@@ -267,6 +294,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
             subject,
             subject,
             templateFile: configuration ? configuration.templateFile : 'body.hbs',
             templateFile: configuration ? configuration.templateFile : 'body.hbs',
             attachments,
             attachments,
+            ...optionalAddressFields,
         };
         };
     }
     }
 
 

+ 6 - 0
packages/email-plugin/src/nodemailer-email-sender.ts

@@ -91,6 +91,9 @@ export class NodemailerEmailSender implements EmailSender {
             subject: email.subject,
             subject: email.subject,
             html: email.body,
             html: email.body,
             attachments: email.attachments,
             attachments: email.attachments,
+            cc: email.cc,
+            bcc: email.bcc,
+            replyTo: email.replyTo,
         });
         });
     }
     }
 
 
@@ -101,6 +104,9 @@ export class NodemailerEmailSender implements EmailSender {
             recipient: email.recipient,
             recipient: email.recipient,
             subject: email.subject,
             subject: email.subject,
             body: email.body,
             body: email.body,
+            cc: email.cc,
+            bcc: email.bcc,
+            replyTo: email.replyTo,
         };
         };
         await fs.writeFile(pathWithoutExt + '.json', JSON.stringify(output, null, 2));
         await fs.writeFile(pathWithoutExt + '.json', JSON.stringify(output, null, 2));
     }
     }

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

@@ -781,6 +781,58 @@ describe('EmailPlugin', () => {
             expect(onSend).toHaveBeenCalledTimes(0);
             expect(onSend).toHaveBeenCalledTimes(0);
         });
         });
     });
     });
+
+    describe('optional address fields', () => {
+        const ctx = RequestContext.deserialize({
+            _channel: { code: DEFAULT_CHANNEL_CODE },
+            _languageCode: LanguageCode.en,
+        } as any);
+
+        it('cc', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setFrom('"test from" <noreply@test.com>')
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello {{ subjectVar }}')
+                .setOptionalAddressFields(() => ({ cc: 'foo@bar.com' }));
+
+            await initPluginWithHandlers([handler]);
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+
+            expect(onSend.mock.calls[0][0].cc).toBe('foo@bar.com');
+        });
+
+        it('bcc', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setFrom('"test from" <noreply@test.com>')
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello {{ subjectVar }}')
+                .setOptionalAddressFields(() => ({ bcc: 'foo@bar.com' }));
+
+            await initPluginWithHandlers([handler]);
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+
+            expect(onSend.mock.calls[0][0].bcc).toBe('foo@bar.com');
+        });
+
+        it('replyTo', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setFrom('"test from" <noreply@test.com>')
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello {{ subjectVar }}')
+                .setOptionalAddressFields(() => ({ replyTo: 'foo@bar.com' }));
+
+            await initPluginWithHandlers([handler]);
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+
+            expect(onSend.mock.calls[0][0].replyTo).toBe('foo@bar.com');
+        });
+    });
 });
 });
 
 
 class FakeCustomSender implements EmailSender {
 class FakeCustomSender implements EmailSender {

+ 43 - 1
packages/email-plugin/src/types.ts

@@ -258,6 +258,9 @@ export interface EmailDetails<Type extends 'serialized' | 'unserialized' = 'unse
     subject: string;
     subject: string;
     body: string;
     body: string;
     attachments: Array<Type extends 'serialized' ? SerializedAttachment : Attachment>;
     attachments: Array<Type extends 'serialized' ? SerializedAttachment : Attachment>;
+    cc?: string;
+    bcc?: string;
+    replyTo?: string;
 }
 }
 
 
 /**
 /**
@@ -345,7 +348,7 @@ export interface EmailGenerator<T extends string = any, E extends VendureEvent =
         subject: string,
         subject: string,
         body: string,
         body: string,
         templateVars: { [key: string]: any },
         templateVars: { [key: string]: any },
-    ): Omit<EmailDetails, 'recipient' | 'attachments'>;
+    ): Pick<EmailDetails, 'from' | 'subject' | 'body'>;
 }
 }
 
 
 /**
 /**
@@ -388,6 +391,9 @@ export type IntermediateEmailDetails = {
     subject: string;
     subject: string;
     templateFile: string;
     templateFile: string;
     attachments: SerializedAttachment[];
     attachments: SerializedAttachment[];
+    cc?: string;
+    bcc?: string;
+    replyTo?: string;
 };
 };
 
 
 /**
 /**
@@ -447,3 +453,39 @@ export type SetTemplateVarsFn<Event> = (
  * @docsPage Email Plugin Types
  * @docsPage Email Plugin Types
  */
  */
 export type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<EmailAttachment[]>;
 export type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<EmailAttachment[]>;
+
+/**
+ * @description
+ * Optional address-related fields for sending the email.
+ *
+ * @docsCategory EmailPlugin
+ * @docsPage Email Plugin Types
+ */
+export interface OptionalAddressFields {
+    /**
+     * @description
+     * Comma separated list of recipients email addresses that will appear on the _Cc:_ field
+     */
+    cc?: string;
+    /**
+     * @description
+     * Comma separated list of recipients email addresses that will appear on the _Bcc:_ field
+     */
+    bcc?: string;
+    /**
+     * @description
+     * An email address that will appear on the _Reply-To:_ field
+     */
+    replyTo?: string;
+}
+
+/**
+ * @description
+ * A function used to set the {@link OptionalAddressFields}.
+ *
+ * @docsCategory EmailPlugin
+ * @docsPage Email Plugin Types
+ */
+export type SetOptionalAddressFieldsFn<Event> = (
+    event: Event,
+) => OptionalAddressFields | Promise<OptionalAddressFields>;