@travetto/email 3.1.5 → 3.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/service.ts +52 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/email",
3
- "version": "3.1.5",
3
+ "version": "3.1.7",
4
4
  "description": "Email transmission module.",
5
5
  "keywords": [
6
6
  "email",
package/src/service.ts CHANGED
@@ -7,13 +7,15 @@ import { MailTemplateEngine } from './template';
7
7
  import { MailUtil } from './util';
8
8
  import { EmailResource } from './resource';
9
9
 
10
+ type MessageWithoutBody = Omit<MessageOptions, 'html' | 'text' | 'subject'>;
11
+
10
12
  /**
11
13
  * Email service for sending and templating emails
12
14
  */
13
15
  @Injectable()
14
16
  export class MailService {
15
17
 
16
- #compiled = new Map<string, MessageOptions>();
18
+ #compiled = new Map<string, { html: string, subject: string, text?: string }>();
17
19
  #transport: MailTransport;
18
20
  #tplEngine: MailTemplateEngine;
19
21
  #resources: EmailResource;
@@ -28,6 +30,21 @@ export class MailService {
28
30
  this.#resources = resources;
29
31
  }
30
32
 
33
+ /**
34
+ * Force content into alternative slots, satisfies node mailer
35
+ */
36
+ #forceContentToAlternative(msg: MessageOptions): MessageOptions {
37
+ for (const [key, mime] of [['text', 'text/plain'], ['html', 'text/html']] as const) {
38
+ if (msg[key]) {
39
+ (msg.alternatives ??= []).push({
40
+ content: msg[key], contentDisposition: 'inline', contentTransferEncoding: '7bit', contentType: `${mime}; charset=utf-8`
41
+ });
42
+ delete msg[key];
43
+ }
44
+ }
45
+ return msg;
46
+ }
47
+
31
48
  /**
32
49
  * Send multiple messages.
33
50
  */
@@ -45,10 +62,9 @@ export class MailService {
45
62
  }
46
63
 
47
64
  /**
48
- * Send a pre compiled email that has a relevant html, subject and optional text file associated
65
+ * Get compiled content by key
49
66
  */
50
- async sendCompiled<S extends SentMessage = SentMessage>(key: string, msg: Omit<MessageOptions, 'html' | 'text' | 'subject'>): Promise<S> {
51
- // Bypass cache if in dynamic mode
67
+ async getCompiled(key: string): Promise<{ html: string, text?: string, subject: string }> {
52
68
  if (GlobalEnv.dynamic || !this.#compiled.has(key)) {
53
69
  const [html, text, subject] = await Promise.all([
54
70
  this.#resources.read(`${key}.compiled.html`),
@@ -58,43 +74,47 @@ export class MailService {
58
74
 
59
75
  this.#compiled.set(key, { html, text, subject });
60
76
  }
61
- return this.send<S>({ ...msg, ...this.#compiled.get(key)! });
77
+ return this.#compiled.get(key)!;
62
78
  }
63
79
 
64
80
  /**
65
- * Send a single message
81
+ * Build message from key/context
82
+ * @param key
83
+ * @param ctx
84
+ * @returns
66
85
  */
67
- async send<S extends SentMessage>(msg: MessageOptions): Promise<S> {
68
- // Template if context is provided
69
- if (msg.context) {
70
- const [html, text, subject] = await Promise.all([
71
- msg.html ? this.#tplEngine!.template(msg.html, msg.context) : undefined,
72
- msg.text ? this.#tplEngine!.template(msg.text, msg.context) : undefined,
73
- msg.subject ? this.#tplEngine!.template(msg.subject, msg.context) : undefined
74
- ]);
86
+ async buildMessage(key: string | MessageOptions, ctx?: Record<string, unknown>): Promise<MessageOptions> {
87
+ const tpl = (typeof key === 'string' ? await this.getCompiled(key) : key);
88
+ ctx ??= (typeof key === 'string' ? {} : key.context ?? {});
89
+ const [rawHtml, text, subject] = await Promise.all([
90
+ tpl.html ? this.#tplEngine!.template(tpl.html, ctx) : undefined,
91
+ tpl.text ? this.#tplEngine!.template(tpl.text, ctx) : undefined,
92
+ tpl.subject ? this.#tplEngine!.template(tpl.subject, ctx) : undefined
93
+ ]);
75
94
 
76
- Object.assign(msg, { html, text, subject });
77
- }
78
-
79
- if (msg.text) {
80
- (msg.alternatives = msg.alternatives || []).push({
81
- content: msg.text, contentDisposition: 'inline', contentTransferEncoding: '7bit', contentType: 'text/plain; charset=utf-8'
82
- });
83
- delete msg.text;
84
- }
95
+ const msg: MessageOptions = {
96
+ html: rawHtml ?? '',
97
+ text,
98
+ subject
99
+ };
85
100
 
86
- // Force html to the end per the mime spec
87
101
  if (msg.html) {
88
102
  const { html, attachments } = await MailUtil.extractImageAttachments(msg.html);
89
- (msg.attachments = msg.attachments || []).push(...attachments);
90
- (msg.alternatives = msg.alternatives || []).push({
91
- // NOTE: The leading space on the content type is to force node mailer to not do anything fancy with
92
- content: html, contentDisposition: 'inline', contentTransferEncoding: '7bit', contentType: ' text/html; charset=utf-8'
93
- });
94
- // @ts-expect-error
95
- delete msg.html; // This is a hack to fix nodemailer
103
+ msg.html = html;
104
+ msg.attachments = attachments;
96
105
  }
97
106
 
98
- return this.#transport.send<S>(msg);
107
+ return msg;
108
+ }
109
+
110
+ /**
111
+ * Send a single message
112
+ */
113
+ async send<S extends SentMessage = SentMessage>(key: string, base?: MessageWithoutBody): Promise<S>;
114
+ async send<S extends SentMessage = SentMessage>(message: MessageOptions): Promise<S>;
115
+ async send<S extends SentMessage = SentMessage>(keyOrMessage: MessageOptions | string, base?: MessageWithoutBody): Promise<S> {
116
+ let msg = await this.buildMessage(keyOrMessage);
117
+ msg = this.#forceContentToAlternative(msg);
118
+ return this.#transport.send<S>({ ...base, ...msg });
99
119
  }
100
120
  }