Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supports computed email subject #2863

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/email-plugin/src/email-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ export class EmailProcessor {
templateVars: data.templateVars,
},
);
if (this.options.templateLoader.loadSubject) {
data.subject = await this.options.templateLoader.loadSubject(
new Injector(this.moduleRef),
ctx,
{
templateName: data.templateFile,
type: data.type,
templateVars: data.templateVars,
subject: data.subject,
},
);
}
const generated = this.generator.generate(data.from, data.subject, bodySource, data.templateVars);
emailDetails = {
...generated,
Expand Down
16 changes: 13 additions & 3 deletions packages/email-plugin/src/handler/event-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
LoadDataFn,
SetAttachmentsFn,
SetOptionalAddressFieldsFn,
SetSubjectFn,
SetTemplateVarsFn,
} from '../types';

Expand Down Expand Up @@ -135,6 +136,7 @@ import {
export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
private setRecipientFn: (event: Event) => string;
private setLanguageCodeFn: (event: Event) => LanguageCode | undefined;
private setSubjectFn?: SetSubjectFn<Event>;
private setTemplateVarsFn: SetTemplateVarsFn<Event>;
private setAttachmentsFn?: SetAttachmentsFn<Event>;
private setOptionalAddressFieldsFn?: SetOptionalAddressFieldsFn<Event>;
Expand Down Expand Up @@ -214,8 +216,12 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
* Sets the default subject of the email. The subject string may use Handlebars variables defined by the
* setTemplateVars() method.
*/
setSubject(defaultSubject: string): EmailEventHandler<T, Event> {
this.defaultSubject = defaultSubject;
setSubject(defaultSubject: string | SetSubjectFn<Event>): EmailEventHandler<T, Event> {
if (typeof defaultSubject === 'string') {
this.defaultSubject = defaultSubject;
} else {
this.setSubjectFn = defaultSubject;
}
return this;
}

Expand Down Expand Up @@ -370,7 +376,11 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
const { ctx } = event;
const languageCode = this.setLanguageCodeFn?.(event) || ctx.languageCode;
const configuration = this.getBestConfiguration(ctx.channel.code, languageCode);
const subject = configuration ? configuration.subject : this.defaultSubject;
const subject = configuration
? configuration.subject
: this.setSubjectFn
? await this.setSubjectFn(event, ctx, injector)
: this.defaultSubject;
if (subject == null) {
throw new Error(
`No subject field has been defined. ` +
Expand Down
102 changes: 101 additions & 1 deletion packages/email-plugin/src/plugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import { EmailSender } from './sender/email-sender';
import { EmailEventHandler } from './handler/event-handler';
import { EmailEventListener } from './event-listener';
import { EmailPlugin } from './plugin';
import { EmailDetails, EmailPluginOptions, EmailTransportOptions } from './types';
import { EmailDetails, EmailPluginOptions, EmailTransportOptions, LoadTemplateInput } from './types';
import { TemplateLoader } from './template-loader/template-loader';
import fs from 'fs-extra';

describe('EmailPlugin', () => {
let eventBus: EventBus;
Expand Down Expand Up @@ -913,6 +915,72 @@ describe('EmailPlugin', () => {
expect(transport.type).toBe('testing');
});
});

describe('Dynamic subject handling', () => {
it('With string', async () => {
const ctx = RequestContext.deserialize({
_channel: { code: DEFAULT_CHANNEL_CODE },
_languageCode: LanguageCode.en,
} as any);
const handler = new EmailEventListener('test')
.on(MockEvent)
.setFrom('"test from" <[email protected]>')
.setRecipient(() => '[email protected]')
.setSubject('Hello')
.setTemplateVars(event => ({ subjectVar: 'foo' }));

await initPluginWithHandlers([handler]);

eventBus.publish(new MockEvent(ctx, true));
await pause();
expect(onSend.mock.calls[0][0].subject).toBe('Hello');
expect(onSend.mock.calls[0][0].recipient).toBe('[email protected]');
expect(onSend.mock.calls[0][0].from).toBe('"test from" <[email protected]>');
});
it('With callback function', async () => {
const ctx = RequestContext.deserialize({
_channel: { code: DEFAULT_CHANNEL_CODE },
_languageCode: LanguageCode.en,
} as any);
const handler = new EmailEventListener('test')
.on(MockEvent)
.setFrom('"test from" <[email protected]>')
.setRecipient(() => '[email protected]')
.setSubject(async (_e, _ctx, _i) => {
const service = _i.get(MockService)
const mockData = await service.someAsyncMethod()
return `Hello from ${mockData} and {{ subjectVar }}`;
})
.setTemplateVars(event => ({ subjectVar: 'foo' }));

await initPluginWithHandlers([handler]);

eventBus.publish(new MockEvent(ctx, true));
await pause();
expect(onSend.mock.calls[0][0].subject).toBe('Hello');
rein1410 marked this conversation as resolved.
Show resolved Hide resolved
expect(onSend.mock.calls[0][0].recipient).toBe('[email protected]');
expect(onSend.mock.calls[0][0].from).toBe('"test from" <[email protected]>');
});

it('With custom template loader', async () => {
const ctx = RequestContext.deserialize({
_channel: { code: DEFAULT_CHANNEL_CODE },
_languageCode: LanguageCode.en,
} as any);
const handler = new EmailEventListener('test')
.on(MockEvent)
.setFrom('"test from" <[email protected]>')
.setRecipient(() => '[email protected]')

await initPluginWithHandlers([handler], { templateLoader: new MockTemplateLoader(path.join(__dirname, '../test-templates')) });

eventBus.publish(new MockEvent(ctx, true));
await pause();
expect(onSend.mock.calls[0][0].subject).toBe('Hello');
rein1410 marked this conversation as resolved.
Show resolved Hide resolved
expect(onSend.mock.calls[0][0].recipient).toBe('[email protected]');
expect(onSend.mock.calls[0][0].from).toBe('"test from" <[email protected]>');
});
})
});

class FakeCustomSender implements EmailSender {
Expand All @@ -932,3 +1000,35 @@ class MockService {
return Promise.resolve('loaded data');
}
}

export class MockTemplateLoader implements TemplateLoader {
constructor(private templatePath: string) {}

async loadTemplate(
_injector: Injector,
_ctx: RequestContext,
{ type, templateName }: LoadTemplateInput,
): Promise<string> {
const templatePath = path.join(this.templatePath, type, templateName);
return fs.readFile(templatePath, 'utf-8');
}

async loadSubject(injector: Injector, ctx: RequestContext, input: LoadTemplateInput & { subject: string; }): Promise<string> {
const orderService = injector.get(MockService);
const content = await orderService.someAsyncMethod()
return `Hello from ${content}`;
}

async loadPartials(): Promise<any[]> {
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'),
};
}),
);
}
}
10 changes: 10 additions & 0 deletions packages/email-plugin/src/template-loader/template-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export interface TemplateLoader {
*/
loadTemplate(injector: Injector, ctx: RequestContext, input: LoadTemplateInput): Promise<string>;

/**
* @description
* Load the email's subject and return it as a string.
*/
loadSubject?(
injector: Injector,
ctx: RequestContext,
input: LoadTemplateInput & { subject: string },
): Promise<string>;

/**
* @description
* Load partials and return their contents.
Expand Down
10 changes: 9 additions & 1 deletion packages/email-plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Injector, RequestContext, SerializedRequestContext, VendureEvent } from
import { Attachment } from 'nodemailer/lib/mailer';
import SESTransport from 'nodemailer/lib/ses-transport';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { EmailEventHandler } from './handler/event-handler';

import { EmailGenerator } from './generator/email-generator';
import { EmailEventHandler } from './handler/event-handler';
import { EmailSender } from './sender/email-sender';
import { TemplateLoader } from './template-loader/template-loader';

Expand Down Expand Up @@ -387,6 +387,14 @@ export type SetTemplateVarsFn<Event> = (
*/
export type SetAttachmentsFn<Event> = (event: Event) => EmailAttachment[] | Promise<EmailAttachment[]>;

/**
* @description
* A function used to define the subject to be sent with the email.
* @docsCategory core plugins/EmailPlugin
* @docsPage Email Plugin Types
*/
export type SetSubjectFn<Event> = (event: Event, ctx: RequestContext, injector: Injector) => string | Promise<string>;

/**
* @description
* Optional address-related fields for sending the email.
Expand Down
Loading