plugin.ts 9.7 KB

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