1
0

plugin.ts 9.7 KB

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