plugin.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import {
  2. Inject,
  3. MiddlewareConsumer,
  4. NestModule,
  5. OnApplicationBootstrap,
  6. OnApplicationShutdown,
  7. } from '@nestjs/common';
  8. import { ModuleRef } from '@nestjs/core';
  9. import {
  10. EventBus,
  11. Injector,
  12. JobQueue,
  13. JobQueueService,
  14. Logger,
  15. PluginCommonModule,
  16. ProcessContext,
  17. registerPluginStartupMessage,
  18. Type,
  19. VendurePlugin,
  20. } from '@vendure/core';
  21. import { isDevModeOptions } from './common';
  22. import { EMAIL_PLUGIN_OPTIONS, loggerCtx } from './constants';
  23. import { DevMailbox } from './dev-mailbox';
  24. import { EmailProcessor } from './email-processor';
  25. import { EmailEventHandler, EmailEventHandlerWithAsyncData } from './event-handler';
  26. import {
  27. EmailPluginDevModeOptions,
  28. EmailPluginOptions,
  29. EventWithContext,
  30. IntermediateEmailDetails,
  31. } from './types';
  32. /**
  33. * @description
  34. * The EmailPlugin creates and sends transactional emails based on Vendure events. By default, it uses an [MJML](https://mjml.io/)-based
  35. * email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emails.
  36. *
  37. * ## High-level description
  38. * Vendure has an internal events system (see {@link EventBus}) that allows plugins to subscribe to events. The EmailPlugin is configured with
  39. * {@link EmailEventHandler}s that listen for a specific event and when it is published, the handler defines which template to use to generate
  40. * the resulting email.
  41. *
  42. * The plugin comes with a set of default handlers for the following events:
  43. * - Order confirmation
  44. * - New customer email address verification
  45. * - Password reset request
  46. * - Email address change request
  47. *
  48. * You can also create your own handlers and register them with the plugin - see the {@link EmailEventHandler} docs for more details.
  49. *
  50. * ## Installation
  51. *
  52. * `yarn add \@vendure/email-plugin`
  53. *
  54. * or
  55. *
  56. * `npm install \@vendure/email-plugin`
  57. *
  58. * @example
  59. * ```ts
  60. * import { defaultEmailHandlers, EmailPlugin } from '\@vendure/email-plugin';
  61. *
  62. * const config: VendureConfig = {
  63. * // Add an instance of the plugin to the plugins array
  64. * plugins: [
  65. * EmailPlugin.init({
  66. * handlers: defaultEmailHandlers,
  67. * templatePath: path.join(__dirname, 'static/email/templates'),
  68. * transport: {
  69. * type: 'smtp',
  70. * host: 'smtp.example.com',
  71. * port: 587,
  72. * auth: {
  73. * user: 'username',
  74. * pass: 'password',
  75. * }
  76. * },
  77. * }),
  78. * ],
  79. * };
  80. * ```
  81. *
  82. * ## Email templates
  83. *
  84. * In the example above, the plugin has been configured to look in `<app-root>/static/email/templates`
  85. * for the email template files. If you used `\@vendure/create` to create your application, the templates will have
  86. * been copied to that location during setup.
  87. *
  88. * If you are installing the EmailPlugin separately, then you'll need to copy the templates manually from
  89. * `node_modules/\@vendure/email-plugin/templates` to a location of your choice, and then point the `templatePath` config
  90. * property at that directory.
  91. *
  92. * ## Customizing templates
  93. *
  94. * Emails are generated from templates which use [MJML](https://mjml.io/) syntax. MJML is an open-source HTML-like markup
  95. * language which makes the task of creating responsive email markup simple. By default, the templates are installed to
  96. * `<project root>/vendure/email/templates` and can be freely edited.
  97. *
  98. * Dynamic data such as the recipient's name or order items are specified using [Handlebars syntax](https://handlebarsjs.com/):
  99. *
  100. * ```HTML
  101. * <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
  102. *
  103. * <p>Thank you for your order!</p>
  104. *
  105. * <mj-table cellpadding="6px">
  106. * {{#each order.lines }}
  107. * <tr class="order-row">
  108. * <td>{{ quantity }} x {{ productVariant.name }}</td>
  109. * <td>{{ productVariant.quantity }}</td>
  110. * <td>{{ formatMoney totalPrice }}</td>
  111. * </tr>
  112. * {{/each}}
  113. * </mj-table>
  114. * ```
  115. *
  116. * ### Handlebars helpers
  117. *
  118. * The following helper functions are available for use in email templates:
  119. *
  120. * * `formatMoney`: Formats an amount of money (which are always stored as integers in Vendure) as a decimal, e.g. `123` => `1.23`
  121. * * `formatDate`: Formats a Date value with the [dateformat](https://www.npmjs.com/package/dateformat) package.
  122. *
  123. * ## Extending the default email handlers
  124. *
  125. * The `defaultEmailHandlers` array defines the default handlers such as for handling new account registration, order confirmation, password reset
  126. * etc. These defaults can be extended by adding custom templates for languages other than the default, or even completely new types of emails
  127. * which respond to any of the available [VendureEvents](/docs/typescript-api/events/).
  128. *
  129. * A good way to learn how to create your own email handlers is to take a look at the
  130. * [source code of the default handlers](https://github.com/vendure-ecommerce/vendure/blob/master/packages/email-plugin/src/default-email-handlers.ts).
  131. * New handlers are defined in exactly the same way.
  132. *
  133. * It is also possible to modify the default handlers:
  134. *
  135. * ```TypeScript
  136. * // Rather than importing `defaultEmailHandlers`, you can
  137. * // import the handlers individually
  138. * import {
  139. * orderConfirmationHandler,
  140. * emailVerificationHandler,
  141. * passwordResetHandler,
  142. * emailAddressChangeHandler,
  143. * } from '\@vendure/email-plugin';
  144. * import { CustomerService } from '\@vendure/core';
  145. *
  146. * // This allows you to then customize each handler to your needs.
  147. * // For example, let's set a new subject line to the order confirmation:
  148. * orderConfirmationHandler
  149. * .setSubject(`We received your order!`);
  150. *
  151. * // Another example: loading additional data and setting new
  152. * // template variables.
  153. * passwordResetHandler
  154. * .loadData(async ({ event, injector }) => {
  155. * const customerService = injector.get(CustomerService);
  156. * const customer = await customerService.findOneByUserId(event.ctx, event.user.id);
  157. * return { customer };
  158. * })
  159. * .setTemplateVars(event => ({
  160. * passwordResetToken: event.user.getNativeAuthenticationMethod().passwordResetToken,
  161. * customer: event.data.customer,
  162. * }));
  163. *
  164. * // Then you pass the handlers to the EmailPlugin init method
  165. * // individually
  166. * EmailPlugin.init({
  167. * handlers: [
  168. * orderConfirmationHandler,
  169. * emailVerificationHandler,
  170. * passwordResetHandler,
  171. * emailAddressChangeHandler,
  172. * ],
  173. * // ...
  174. * }),
  175. * ```
  176. *
  177. * For all available methods of extending a handler, see the {@link EmailEventHandler} documentation.
  178. *
  179. * ## Dev mode
  180. *
  181. * For development, the `transport` option can be replaced by `devMode: true`. Doing so configures Vendure to use the
  182. * file transport (See {@link FileTransportOptions}) and outputs emails as rendered HTML files in the directory specified by the
  183. * `outputPath` property.
  184. *
  185. * ```TypeScript
  186. * EmailPlugin.init({
  187. * devMode: true,
  188. * route: 'mailbox',
  189. * handlers: defaultEmailHandlers,
  190. * templatePath: path.join(__dirname, 'vendure/email/templates'),
  191. * outputPath: path.join(__dirname, 'test-emails'),
  192. * })
  193. * ```
  194. *
  195. * ### Dev mailbox
  196. *
  197. * In dev mode, a webmail-like interface available at the `/mailbox` path, e.g.
  198. * http://localhost:3000/mailbox. This is a simple way to view the output of all emails generated by the EmailPlugin while in dev mode.
  199. *
  200. * ## Troubleshooting SMTP Connections
  201. *
  202. * If you are having trouble sending email over and SMTP connection, set the `logging` and `debug` options to `true`. This will
  203. * send detailed information from the SMTP transporter to the configured logger (defaults to console). For maximum detail combine
  204. * this with a detail log level in the configured VendureLogger:
  205. *
  206. * ```TypeScript
  207. * const config: VendureConfig = {
  208. * logger: new DefaultLogger({ level: LogLevel.Debug })
  209. * // ...
  210. * plugins: [
  211. * EmailPlugin.init({
  212. * // ...
  213. * transport: {
  214. * type: 'smtp',
  215. * host: 'smtp.example.com',
  216. * port: 587,
  217. * auth: {
  218. * user: 'username',
  219. * pass: 'password',
  220. * },
  221. * logging: true,
  222. * debug: true,
  223. * },
  224. * }),
  225. * ],
  226. * };
  227. * ```
  228. *
  229. * @docsCategory EmailPlugin
  230. */
  231. @VendurePlugin({
  232. imports: [PluginCommonModule],
  233. providers: [{ provide: EMAIL_PLUGIN_OPTIONS, useFactory: () => EmailPlugin.options }, EmailProcessor],
  234. })
  235. export class EmailPlugin implements OnApplicationBootstrap, OnApplicationShutdown, NestModule {
  236. private static options: EmailPluginOptions | EmailPluginDevModeOptions;
  237. private devMailbox: DevMailbox | undefined;
  238. private jobQueue: JobQueue<IntermediateEmailDetails> | undefined;
  239. private testingProcessor: EmailProcessor | undefined;
  240. /** @internal */
  241. constructor(
  242. private eventBus: EventBus,
  243. private moduleRef: ModuleRef,
  244. private emailProcessor: EmailProcessor,
  245. private jobQueueService: JobQueueService,
  246. private processContext: ProcessContext,
  247. @Inject(EMAIL_PLUGIN_OPTIONS) private options: EmailPluginOptions,
  248. ) {}
  249. /**
  250. * Set the plugin options.
  251. */
  252. static init(options: EmailPluginOptions | EmailPluginDevModeOptions): Type<EmailPlugin> {
  253. this.options = options;
  254. return EmailPlugin;
  255. }
  256. /** @internal */
  257. async onApplicationBootstrap(): Promise<void> {
  258. await this.initInjectableStrategies();
  259. await this.setupEventSubscribers();
  260. if (!isDevModeOptions(this.options) && this.options.transport.type === 'testing') {
  261. // When running tests, we don't want to go through the JobQueue system,
  262. // so we just call the email sending logic directly.
  263. this.testingProcessor = new EmailProcessor(this.options);
  264. await this.testingProcessor.init();
  265. } else {
  266. await this.emailProcessor.init();
  267. this.jobQueue = await this.jobQueueService.createQueue({
  268. name: 'send-email',
  269. process: job => {
  270. return this.emailProcessor.process(job.data);
  271. },
  272. });
  273. }
  274. }
  275. async onApplicationShutdown() {
  276. await this.destroyInjectableStrategies();
  277. }
  278. configure(consumer: MiddlewareConsumer) {
  279. if (isDevModeOptions(this.options) && this.processContext.isServer) {
  280. Logger.info('Creating dev mailbox middleware', loggerCtx);
  281. this.devMailbox = new DevMailbox();
  282. consumer.apply(this.devMailbox.serve(this.options)).forRoutes(this.options.route);
  283. this.devMailbox.handleMockEvent((handler, event) => this.handleEvent(handler, event));
  284. registerPluginStartupMessage('Dev mailbox', this.options.route);
  285. }
  286. }
  287. private async initInjectableStrategies() {
  288. const injector = new Injector(this.moduleRef);
  289. if (typeof this.options.emailGenerator?.init === 'function') {
  290. await this.options.emailGenerator.init(injector);
  291. }
  292. if (typeof this.options.emailSender?.init === 'function') {
  293. await this.options.emailSender.init(injector);
  294. }
  295. }
  296. private async destroyInjectableStrategies() {
  297. if (typeof this.options.emailGenerator?.destroy === 'function') {
  298. await this.options.emailGenerator.destroy();
  299. }
  300. if (typeof this.options.emailSender?.destroy === 'function') {
  301. await this.options.emailSender.destroy();
  302. }
  303. }
  304. private async setupEventSubscribers() {
  305. for (const handler of EmailPlugin.options.handlers) {
  306. this.eventBus.ofType(handler.event).subscribe(event => {
  307. return this.handleEvent(handler, event);
  308. });
  309. }
  310. }
  311. private async handleEvent(
  312. handler: EmailEventHandler | EmailEventHandlerWithAsyncData<any>,
  313. event: EventWithContext,
  314. ) {
  315. Logger.debug(`Handling event "${handler.type}"`, loggerCtx);
  316. const { type } = handler;
  317. try {
  318. const injector = new Injector(this.moduleRef);
  319. const result = await handler.handle(
  320. event as any,
  321. EmailPlugin.options.globalTemplateVars,
  322. injector,
  323. );
  324. if (!result) {
  325. return;
  326. }
  327. if (this.jobQueue) {
  328. await this.jobQueue.add(result, { retries: 5 });
  329. } else if (this.testingProcessor) {
  330. await this.testingProcessor.process(result);
  331. }
  332. } catch (e) {
  333. Logger.error(e.message, loggerCtx, e.stack);
  334. }
  335. }
  336. }