@travetto/email 3.1.13 → 3.1.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/email",
3
- "version": "3.1.13",
3
+ "version": "3.1.14",
4
4
  "description": "Email transmission module.",
5
5
  "keywords": [
6
6
  "email",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@travetto/base": "^3.1.3",
27
- "@travetto/config": "^3.1.7",
27
+ "@travetto/config": "^3.1.8",
28
28
  "@travetto/di": "^3.1.4",
29
29
  "@types/mustache": "^4.2.2",
30
30
  "mustache": "^4.2.0"
@@ -1,2 +1,2 @@
1
- export class MailTemplateEngineTarget { }
1
+ export class MailInterpolatorTarget { }
2
2
  export class MailTransportTarget { }
package/src/service.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { GlobalEnv } from '@travetto/base';
2
2
  import { Injectable } from '@travetto/di';
3
3
 
4
- import { MessageCompiled, MessageOptions, SentMessage } from './types';
4
+ import { EmailCompiled, EmailOptions, SentEmail } from './types';
5
5
  import { MailTransport } from './transport';
6
- import { MailTemplateEngine } from './template';
6
+ import { MailInterpolator } from './template';
7
7
  import { MailUtil } from './util';
8
8
  import { EmailResource } from './resource';
9
9
 
10
- type MessageWithoutBody = Omit<MessageOptions, keyof MessageCompiled>;
10
+ type MessageWithoutBody = Omit<EmailOptions, keyof EmailCompiled>;
11
11
 
12
12
  /**
13
13
  * Email service for sending and templating emails
@@ -15,17 +15,17 @@ type MessageWithoutBody = Omit<MessageOptions, keyof MessageCompiled>;
15
15
  @Injectable()
16
16
  export class MailService {
17
17
 
18
- #compiled = new Map<string, MessageCompiled>();
18
+ #compiled = new Map<string, EmailCompiled>();
19
19
  #transport: MailTransport;
20
- #tplEngine: MailTemplateEngine;
20
+ #interpolator: MailInterpolator;
21
21
  #resources: EmailResource;
22
22
 
23
23
  constructor(
24
24
  transport: MailTransport,
25
- tplEngine: MailTemplateEngine,
25
+ interpolator: MailInterpolator,
26
26
  resources: EmailResource
27
27
  ) {
28
- this.#tplEngine = tplEngine;
28
+ this.#interpolator = interpolator;
29
29
  this.#transport = transport;
30
30
  this.#resources = resources;
31
31
  }
@@ -33,13 +33,13 @@ export class MailService {
33
33
  /**
34
34
  * Get compiled content by key
35
35
  */
36
- async getCompiled(key: string): Promise<MessageCompiled> {
36
+ async getCompiled(key: string): Promise<EmailCompiled> {
37
37
  if (GlobalEnv.dynamic || !this.#compiled.has(key)) {
38
38
  const [html, text, subject] = await Promise.all([
39
39
  this.#resources.read(`${key}.compiled.html`),
40
40
  this.#resources.read(`${key}.compiled.text`),
41
41
  this.#resources.read(`${key}.compiled.subject`)
42
- ]);
42
+ ].map(x => x.then(MailUtil.purgeBrand)));
43
43
 
44
44
  this.#compiled.set(key, { html, text, subject });
45
45
  }
@@ -52,13 +52,13 @@ export class MailService {
52
52
  * @param ctx
53
53
  * @returns
54
54
  */
55
- async templateMessage(keyOrMessage: string | MessageCompiled, ctx: Record<string, unknown>): Promise<MessageCompiled> {
55
+ async renderMessage(keyOrMessage: string | EmailCompiled, ctx: Record<string, unknown>): Promise<EmailCompiled> {
56
56
  const tpl = (typeof keyOrMessage === 'string' ? await this.getCompiled(keyOrMessage) : keyOrMessage);
57
57
 
58
58
  const [html, text, subject] = await Promise.all([
59
- this.#tplEngine!.template(tpl.html, ctx),
60
- this.#tplEngine!.template(tpl.text, ctx),
61
- this.#tplEngine!.template(tpl.subject, ctx)
59
+ this.#interpolator.render(tpl.html, ctx),
60
+ this.#interpolator.render(tpl.text ?? '', ctx),
61
+ this.#interpolator.render(tpl.subject, ctx)
62
62
  ]);
63
63
 
64
64
  return { html, text, subject };
@@ -67,15 +67,15 @@ export class MailService {
67
67
  /**
68
68
  * Send a single message
69
69
  */
70
- async send<S extends SentMessage = SentMessage>(
71
- message: Pick<MessageOptions, 'to' | 'from' | 'replyTo'>,
70
+ async send<S extends SentEmail = SentEmail>(
71
+ message: Pick<EmailOptions, 'to' | 'from' | 'replyTo'>,
72
72
  key: string,
73
73
  ctx?: Record<string, unknown>,
74
74
  base?: MessageWithoutBody
75
75
  ): Promise<S>;
76
- async send<S extends SentMessage = SentMessage>(message: MessageOptions): Promise<S>;
77
- async send<S extends SentMessage = SentMessage>(
78
- message: MessageOptions | Pick<MessageOptions, 'to' | 'from' | 'replyTo'>,
76
+ async send<S extends SentEmail = SentEmail>(message: EmailOptions): Promise<S>;
77
+ async send<S extends SentEmail = SentEmail>(
78
+ message: EmailOptions | Pick<EmailOptions, 'to' | 'from' | 'replyTo'>,
79
79
  key?: string,
80
80
  ctx?: Record<string, unknown>,
81
81
  base?: MessageWithoutBody
@@ -83,7 +83,7 @@ export class MailService {
83
83
  const keyOrMessage = key ?? ('html' in message ? message : '') ?? '';
84
84
  const context = ctx ?? (('context' in message) ? message.context : {}) ?? {};
85
85
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
86
- const compiled = await this.templateMessage(keyOrMessage as MessageCompiled, context);
86
+ const compiled = await this.renderMessage(keyOrMessage as EmailCompiled, context);
87
87
 
88
88
  const final = { ...base, ...message, ...compiled, context };
89
89
 
@@ -100,7 +100,7 @@ export class MailService {
100
100
  /**
101
101
  * Send multiple messages.
102
102
  */
103
- async sendAll<S extends SentMessage = SentMessage>(messages: MessageOptions[], base: Partial<MessageOptions> = {}): Promise<S[]> {
103
+ async sendAll<S extends SentEmail = SentEmail>(messages: EmailOptions[], base: Partial<EmailOptions> = {}): Promise<S[]> {
104
104
  return Promise.all(messages.map(msg => this.send<S>({
105
105
  ...base,
106
106
  ...msg,
package/src/template.ts CHANGED
@@ -5,24 +5,24 @@ import { Injectable } from '@travetto/di';
5
5
  import { EmailResource } from './resource';
6
6
 
7
7
  /**
8
- * Mail templating engine
8
+ * Mail interpolation engine
9
9
  *
10
- * @concrete ./internal/types:MailTemplateEngineTarget
10
+ * @concrete ./internal/types:MailInterpolatorTarget
11
11
  */
12
- export interface MailTemplateEngine {
12
+ export interface MailInterpolator {
13
13
  /**
14
14
  * Resolved nested templates
15
15
  */
16
16
  resolveNested(template: string): Promise<string>;
17
17
 
18
18
  /**
19
- * Interpolate a string with a given context, useful for simple messages
19
+ * Interpolate a string with a given context
20
20
  */
21
- template(text: string, ctx: Record<string, unknown>): Promise<string> | string;
21
+ render(text: string, ctx: Record<string, unknown>): Promise<string> | string;
22
22
  }
23
23
 
24
24
  @Injectable()
25
- export class MustacheTemplateEngine implements MailTemplateEngine {
25
+ export class MustacheInterpolator implements MailInterpolator {
26
26
 
27
27
  resources = new EmailResource();
28
28
 
@@ -45,7 +45,7 @@ export class MustacheTemplateEngine implements MailTemplateEngine {
45
45
  /**
46
46
  * Interpolate text with data
47
47
  */
48
- template(text: string, data: Record<string, unknown>): string {
48
+ render(text: string, data: Record<string, unknown>): string {
49
49
  return mustache.render(text, data);
50
50
  }
51
51
  }
package/src/transport.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MessageOptions, SentMessage } from './types';
1
+ import { EmailOptions, SentEmail } from './types';
2
2
 
3
3
  /**
4
4
  * Default mail transport
@@ -6,14 +6,14 @@ import { MessageOptions, SentMessage } from './types';
6
6
  * @concrete ./internal/types:MailTransportTarget
7
7
  */
8
8
  export interface MailTransport {
9
- send<S extends SentMessage = SentMessage>(mail: MessageOptions): Promise<S>;
9
+ send<S extends SentEmail = SentEmail>(mail: EmailOptions): Promise<S>;
10
10
  }
11
11
 
12
12
  /**
13
13
  * Transport that consumes messages without sending
14
14
  */
15
15
  export class NullTransport implements MailTransport {
16
- async send<S extends SentMessage = SentMessage>(mail: MessageOptions): Promise<S> {
16
+ async send<S extends SentEmail = SentEmail>(mail: EmailOptions): Promise<S> {
17
17
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
18
18
  return {} as S;
19
19
  }
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@ import { Url } from 'url';
4
4
  /**
5
5
  * An address
6
6
  */
7
- export interface Address {
7
+ export interface EmailAddress {
8
8
  name: string;
9
9
  address: string;
10
10
  }
@@ -20,7 +20,7 @@ interface AttachmentLike {
20
20
  /**
21
21
  * A full attachment
22
22
  */
23
- export interface Attachment extends AttachmentLike {
23
+ export interface EmailAttachment extends AttachmentLike {
24
24
  filename?: string | false;
25
25
  cid?: string;
26
26
  encoding?: string;
@@ -31,53 +31,54 @@ export interface Attachment extends AttachmentLike {
31
31
  raw?: string | Buffer | Readable | AttachmentLike;
32
32
  }
33
33
 
34
+ type EmailContentType = 'html' | 'text' | 'subject';
35
+
34
36
  /**
35
37
  * Full message options
36
38
  */
37
- export interface MessageOptions {
39
+ export interface EmailOptions {
38
40
  html: string;
39
41
  text?: string;
40
- subject?: string;
42
+ subject: string;
41
43
  context?: Record<string, unknown>; // For templating
42
44
 
43
- from?: string | Address;
44
- sender?: string | Address;
45
- to?: string | Address | (string | Address)[];
46
- cc?: string | Address | (string | Address)[];
47
- bcc?: string | Address | (string | Address)[];
48
- replyTo?: string | Address;
49
- inReplyTo?: string | Address;
45
+ from?: string | EmailAddress;
46
+ sender?: string | EmailAddress;
47
+ to?: string | EmailAddress | (string | EmailAddress)[];
48
+ cc?: string | EmailAddress | (string | EmailAddress)[];
49
+ bcc?: string | EmailAddress | (string | EmailAddress)[];
50
+ replyTo?: string | EmailAddress;
51
+ inReplyTo?: string | EmailAddress;
50
52
  references?: string | string[];
51
53
  headers?: Record<string, string | string[]>;
52
- attachments?: Attachment[];
53
- alternatives?: Attachment[];
54
+ attachments?: EmailAttachment[];
55
+ alternatives?: EmailAttachment[];
54
56
  messageId?: string;
55
57
  date?: Date | string;
56
58
  encoding?: string;
57
59
  }
58
60
 
59
- export type SentMessage = {
61
+ export type SentEmail = {
60
62
  messageId?: string;
61
63
  };
62
64
 
63
- export type MessageCompiled = { html: string, text: string, subject: string };
65
+ export type EmailCompiled = Record<EmailContentType, string>;
64
66
 
65
- export type MessageCompilationStyles = {
67
+ // Compilation support, defined here to allow for templates to not have a direct dependency on the compiler
68
+ type BaseTemplateConfig = {
66
69
  search?: string[];
67
- global?: string;
68
70
  inline?: boolean;
69
71
  };
70
72
 
71
- export type MessageCompilationImages = {
72
- search?: string[];
73
- inline?: boolean;
73
+ export type EmailTemplateStyleConfig = BaseTemplateConfig & { global?: string };
74
+ export type EmailTemplateImageConfig = BaseTemplateConfig & {};
75
+
76
+ export type EmailTemplateConfig = {
77
+ styles?: EmailTemplateStyleConfig;
78
+ images?: EmailTemplateImageConfig;
74
79
  };
75
80
 
76
- export type MessageCompilationSource = {
77
- file?: string;
78
- styles?: MessageCompilationStyles;
79
- images?: MessageCompilationImages;
80
- html: () => Promise<string> | string;
81
- text: () => Promise<string> | string;
82
- subject: () => Promise<string> | string;
83
- };
81
+ export type EmailTemplateLocation = { file: string, module: string };
82
+ export type EmailRenderer = (ctx: EmailTemplateLocation & EmailTemplateConfig) => Promise<string> | string;
83
+ export type EmailCompileSource = EmailTemplateConfig & Record<EmailContentType, EmailRenderer>;
84
+ export type EmailCompileContext = EmailTemplateLocation & EmailCompileSource;
package/src/util.ts CHANGED
@@ -1,17 +1,34 @@
1
- import { Attachment } from './types';
1
+ import { RootIndex } from '@travetto/manifest';
2
+
3
+ import { EmailAttachment } from './types';
2
4
 
3
5
  /**
4
6
  * Utilities for email
5
7
  */
6
8
  export class MailUtil {
9
+ /** Remove brand from text */
10
+ static purgeBrand(text: string): string {
11
+ return text.replace(/<!-- WARNING:[^\n]*\n/m, '');
12
+ }
13
+
14
+ /** Add brand to text */
15
+ static buildBrand(file: string, content: string, compile?: string): string {
16
+ const out = [
17
+ 'WARNING: Do not modify.',
18
+ `File is generated from "${file.replace(RootIndex.manifest.workspacePath, '.')}"`,
19
+ compile ? `Run \`${compile.replaceAll('\n', ' ')}\` to regenerate` : ''
20
+ ];
21
+ return `<!-- ${out.join(' ').trim()} -->\n${content}`;
22
+ }
23
+
7
24
  /**
8
25
  * Extract images from html as attachments
9
26
  *
10
27
  * @param html
11
28
  */
12
- static async extractImageAttachments(html: string): Promise<{ html: string, attachments: Attachment[] }> {
29
+ static async extractImageAttachments(html: string): Promise<{ html: string, attachments: EmailAttachment[] }> {
13
30
  let idx = 0;
14
- const attachments: Attachment[] = [];
31
+ const attachments: EmailAttachment[] = [];
15
32
  const contentMap = new Map<string, string>();
16
33
 
17
34
  html = html.replace(/data:(image\/[^;]+);base64,([^"']+)/g, (__, contentType, content) => {