plugin.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { EventBus, InternalServerError, Type, VendureConfig, VendureEvent, VendurePlugin } from '@vendure/core';
  2. import fs from 'fs-extra';
  3. import path from 'path';
  4. import { DefaultEmailType, defaultEmailTypes } from './default-email-types';
  5. import { EmailContext } from './email-context';
  6. import { EmailSender } from './email-sender';
  7. import { HandlebarsMjmlGenerator } from './handlebars-mjml-generator';
  8. import { TemplateLoader } from './template-loader';
  9. import { EmailOptions, EmailPluginDevModeOptions, EmailPluginOptions, EmailTransportOptions, EmailTypeConfig } from './types';
  10. /**
  11. * @description
  12. * The EmailPlugin creates and sends transactional emails based on Vendure events. It uses an [MJML](https://mjml.io/)-based
  13. * email generator to generate the email body and [Nodemailer](https://nodemailer.com/about/) to send the emais.
  14. *
  15. * @example
  16. * ```ts
  17. * const config: VendureConfig = {
  18. * // Add an instance of the plugin to the plugins array
  19. * plugins: [
  20. * new EmailPlugin({
  21. * templatePath: path.join(__dirname, 'vendure/email/templates'),
  22. * transport: {
  23. * type: 'smtp',
  24. * host: 'smtp.example.com',
  25. * port: 587,
  26. * auth: {
  27. * user: 'username',
  28. * pass: 'password',
  29. * }
  30. * },
  31. * }),
  32. * ],
  33. * };
  34. * ```
  35. *
  36. * ## Customizing templates
  37. *
  38. * Emails are generated from templates which use [MJML](https://mjml.io/) syntax. MJML is an open-source HTML-like markup
  39. * language which makes the task of creating responsive email markup simple. By default, the templates are installed to
  40. * `<project root>/vendure/email/templates` and can be freely edited.
  41. *
  42. * Dynamic data such as the recipient's name or order items are specified using [Handlebars syntax](https://handlebarsjs.com/):
  43. *
  44. * ```HTML
  45. * <p>Dear {{ order.customer.firstName }} {{ order.customer.lastName }},</p>
  46. *
  47. * <p>Thank you for your order!</p>
  48. *
  49. * <mj-table cellpadding="6px">
  50. * {{#each order.lines }}
  51. * <tr class="order-row">
  52. * <td>{{ quantity }} x {{ productVariant.name }}</td>
  53. * <td>{{ productVariant.quantity }}</td>
  54. * <td>{{ formatMoney totalPrice }}</td>
  55. * </tr>
  56. * {{/each}}
  57. * </mj-table>
  58. * ```
  59. *
  60. * ### Handlebars helpers
  61. *
  62. * The following helper functions are available for use in email templates:
  63. *
  64. * * `formatMoney`: Formats an amount of money (which are always stored as integers in Vendure) as a decimal, e.g. `123` => `1.23`
  65. * * `formatDate`: Formats a Date value with the [dateformat](https://www.npmjs.com/package/dateformat) package.
  66. *
  67. * ## Dev mode
  68. *
  69. * For development, the `transport` option can be replaced by `devMode: true`. Doing so configures Vendure to use the
  70. * [file transport]({{}}) and outputs emails as rendered HTML files in a directory named "test-emails" which is located adjacent to the directory configured in the `templatePath`.
  71. *
  72. * ```ts
  73. * new EmailPlugin({
  74. * templatePath: path.join(__dirname, 'vendure/email/templates'),
  75. * devMode: true,
  76. * })
  77. * ```
  78. *
  79. * @docsCategory EmailPlugin
  80. */
  81. export class EmailPlugin implements VendurePlugin {
  82. private readonly templatePath: string;
  83. private readonly transport: EmailTransportOptions;
  84. private readonly templateVars: { [name: string]: any };
  85. private eventBus: EventBus;
  86. private templateLoader: TemplateLoader;
  87. private emailSender: EmailSender;
  88. private readonly emailOptions: EmailOptions<DefaultEmailType>;
  89. constructor(options: EmailPluginOptions | EmailPluginDevModeOptions) {
  90. this.templatePath = options.templatePath;
  91. this.templateVars = options.templateVars || {};
  92. if (isDevModeOptions(options)) {
  93. this.transport = {
  94. type: 'file',
  95. raw: false,
  96. outputPath: options.outputPath,
  97. };
  98. } else {
  99. if (!options.transport) {
  100. throw new InternalServerError(
  101. `When devMode is not set to true, the 'transport' property must be set.`,
  102. );
  103. }
  104. this.transport = options.transport;
  105. }
  106. this.emailOptions = {
  107. emailTemplatePath: this.templatePath,
  108. emailTypes: defaultEmailTypes,
  109. generator: new HandlebarsMjmlGenerator(),
  110. transport: this.transport,
  111. templateVars: this.templateVars,
  112. };
  113. }
  114. async onBootstrap(inject: <T>(type: Type<T>) => T): Promise<void> {
  115. this.eventBus = inject(EventBus);
  116. this.templateLoader = new TemplateLoader(this.emailOptions);
  117. this.emailSender = new EmailSender();
  118. await this.setupEventSubscribers();
  119. const { generator } = this.emailOptions;
  120. if (generator.onInit) {
  121. await generator.onInit.call(generator, this.emailOptions);
  122. }
  123. }
  124. async setupEventSubscribers() {
  125. const { emailTypes } = this.emailOptions;
  126. for (const [type, config] of Object.entries(emailTypes)) {
  127. this.eventBus.subscribe(config.triggerEvent, event => {
  128. return this.handleEvent(type, config, event);
  129. });
  130. }
  131. if (this.emailOptions.transport.type === 'file') {
  132. // ensure the configured directory exists before
  133. // we attempt to write files to it
  134. const emailPath = this.emailOptions.transport.outputPath;
  135. await fs.ensureDir(emailPath);
  136. }
  137. }
  138. private async handleEvent(type: string, config: EmailTypeConfig<any>, event: VendureEvent) {
  139. const { generator, transport, templateVars } = this.emailOptions;
  140. const contextConfig = config.createContext(event);
  141. if (contextConfig) {
  142. const emailContext = new EmailContext({
  143. ...contextConfig,
  144. type,
  145. event,
  146. templateVars: templateVars || {},
  147. });
  148. const { subject, body, templateContext } = await this.templateLoader.loadTemplate(
  149. type,
  150. emailContext,
  151. );
  152. const generatedEmailContext = await generator.generate(
  153. subject,
  154. body,
  155. templateContext,
  156. emailContext,
  157. );
  158. await this.emailSender.send(generatedEmailContext, transport);
  159. }
  160. }
  161. }
  162. function isDevModeOptions(
  163. input: EmailPluginOptions | EmailPluginDevModeOptions,
  164. ): input is EmailPluginDevModeOptions {
  165. return (input as EmailPluginDevModeOptions).devMode === true;
  166. }