@travetto/email 3.4.4 → 4.0.0-rc.1

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/README.md CHANGED
@@ -13,7 +13,7 @@ npm install @travetto/email
13
13
  yarn add @travetto/email
14
14
  ```
15
15
 
16
- A standard API for sending and rendering emails. The mail transport must be defined to allow for mail to be sent properly. Out of the box, the only transport available by default is the [NullTransport](https://github.com/travetto/travetto/tree/main/module/email/src/transport.ts#L15) which will just drop emails. The structure of the API is derived from [nodemailer](https://nodemailer.com/about/), but is compatible with any library that can handle the [EmailOptions](https://github.com/travetto/travetto/tree/main/module/email/src/types.ts#L39) input.
16
+ A standard API for sending and rendering emails. The mail transport must be defined to allow for mail to be sent properly. Out of the box, the only transport available by default is the [NullTransport](https://github.com/travetto/travetto/tree/main/module/email/src/transport.ts#L15) which will just drop emails. The structure of the API is derived from [nodemailer](https://nodemailer.com/about/), but is compatible with any library that can handle the [EmailOptions](https://github.com/travetto/travetto/tree/main/module/email/src/types.ts#L43) input.
17
17
 
18
18
  To expose the necessary email transport, the following pattern is commonly used:
19
19
 
package/__index__.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from './src/service';
2
2
  export * from './src/types';
3
3
  export * from './src/config';
4
- export * from './src/transport';
5
4
  export * from './src/resource';
5
+ export * from './src/transport';
6
6
  export * from './src/util';
7
7
  export * from './src/template';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/email",
3
- "version": "3.4.4",
3
+ "version": "4.0.0-rc.1",
4
4
  "description": "Email transmission module.",
5
5
  "keywords": [
6
6
  "email",
@@ -23,9 +23,9 @@
23
23
  "directory": "module/email"
24
24
  },
25
25
  "dependencies": {
26
- "@travetto/base": "^3.4.2",
27
- "@travetto/config": "^3.4.4",
28
- "@travetto/di": "^3.4.2",
26
+ "@travetto/base": "^4.0.0-rc.1",
27
+ "@travetto/config": "^4.0.0-rc.1",
28
+ "@travetto/di": "^4.0.0-rc.1",
29
29
  "@types/mustache": "^4.2.5",
30
30
  "mustache": "^4.2.0"
31
31
  },
package/src/resource.ts CHANGED
@@ -1,10 +1,19 @@
1
- import { ResourceLoader } from '@travetto/base';
2
- import { InjectableFactory } from '@travetto/di';
1
+ import { AppError, Env, FileLoader } from '@travetto/base';
2
+ import { RuntimeIndex } from '@travetto/manifest';
3
3
 
4
- export class EmailResource extends ResourceLoader {
5
-
6
- @InjectableFactory()
7
- static getResources(): EmailResource {
8
- return new EmailResource();
4
+ /** Build a resource loader that looks into a module and it's dependencies */
5
+ export class EmailResourceLoader extends FileLoader {
6
+ constructor(module: string, globalResources?: string[]) {
7
+ const mod = RuntimeIndex.getModule(module);
8
+ if (!mod) {
9
+ throw new AppError(`Unknown module - ${module}`, 'notfound', { module });
10
+ }
11
+ super([
12
+ ...Env.TRV_RESOURCES.list ?? [],
13
+ `${module}#resources`,
14
+ ...RuntimeIndex.getDependentModules(mod, 'children').map(x => `${x.name}#resources`),
15
+ '@@#resources',
16
+ ...globalResources ?? []
17
+ ]);
9
18
  }
10
19
  }
package/src/service.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { GlobalEnv } from '@travetto/base';
1
+ import { Env, RuntimeResources } from '@travetto/base';
2
2
  import { Injectable } from '@travetto/di';
3
3
 
4
4
  import { EmailCompiled, EmailOptions, SentEmail } from './types';
5
5
  import { MailTransport } from './transport';
6
6
  import { MailInterpolator } from './template';
7
7
  import { MailUtil } from './util';
8
- import { EmailResource } from './resource';
9
8
 
10
9
  type MessageWithoutBody = Omit<EmailOptions, keyof EmailCompiled>;
11
10
 
@@ -18,27 +17,24 @@ export class MailService {
18
17
  #compiled = new Map<string, EmailCompiled>();
19
18
  #transport: MailTransport;
20
19
  #interpolator: MailInterpolator;
21
- #resources: EmailResource;
22
20
 
23
21
  constructor(
24
22
  transport: MailTransport,
25
23
  interpolator: MailInterpolator,
26
- resources: EmailResource
27
24
  ) {
28
25
  this.#interpolator = interpolator;
29
26
  this.#transport = transport;
30
- this.#resources = resources;
31
27
  }
32
28
 
33
29
  /**
34
30
  * Get compiled content by key
35
31
  */
36
32
  async getCompiled(key: string): Promise<EmailCompiled> {
37
- if (GlobalEnv.dynamic || !this.#compiled.has(key)) {
33
+ if (Env.dynamic || !this.#compiled.has(key)) {
38
34
  const [html, text, subject] = await Promise.all([
39
- this.#resources.read(`${key}.compiled.html`),
40
- this.#resources.read(`${key}.compiled.text`),
41
- this.#resources.read(`${key}.compiled.subject`)
35
+ RuntimeResources.read(`${key}.compiled.html`),
36
+ RuntimeResources.read(`${key}.compiled.text`),
37
+ RuntimeResources.read(`${key}.compiled.subject`)
42
38
  ].map(x => x.then(MailUtil.purgeBrand)));
43
39
 
44
40
  this.#compiled.set(key, { html, text, subject });
@@ -94,6 +90,17 @@ export class MailService {
94
90
  final.attachments = [...attachments, ...(final.attachments ?? [])];
95
91
  }
96
92
 
93
+ // Disable threading if desired, provide a unique message id and a unique reply-to
94
+ if ('disableThreading' in message && message.disableThreading) {
95
+ const id = MailUtil.buildUniqueMessageId(message);
96
+ final.headers = {
97
+ 'In-Reply-To': id,
98
+ References: id,
99
+ 'X-Message-Id': id,
100
+ ...final.headers
101
+ };
102
+ }
103
+
97
104
  return this.#transport.send<S>(final);
98
105
  }
99
106
 
package/src/template.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  import mustache from 'mustache';
2
2
 
3
- import { Inject, Injectable } from '@travetto/di';
4
-
5
- import { EmailResource } from './resource';
3
+ import { Injectable } from '@travetto/di';
4
+ import { RuntimeResources } from '@travetto/base';
6
5
 
7
6
  /**
8
7
  * Mail interpolation engine
9
8
  *
10
- * @concrete ./internal/types:MailInterpolatorTarget
9
+ * @concrete ./internal/types#MailInterpolatorTarget
11
10
  */
12
11
  export interface MailInterpolator {
13
12
  /**
@@ -24,9 +23,6 @@ export interface MailInterpolator {
24
23
  @Injectable()
25
24
  export class MustacheInterpolator implements MailInterpolator {
26
25
 
27
- @Inject()
28
- resources: EmailResource;
29
-
30
26
  /**
31
27
  * Resolved nested templates
32
28
  */
@@ -34,7 +30,7 @@ export class MustacheInterpolator implements MailInterpolator {
34
30
  const promises: Promise<string>[] = [];
35
31
  template = template.replace(/[{]{2,3}>\s+(\S+)([.]html)?\s*[}]{2,3}/g, (all: string, name: string) => {
36
32
  promises.push(
37
- this.resources.read(`${name}.html`) // Ensure html file
33
+ RuntimeResources.read(`${name}.html`) // Ensure html file
38
34
  .then(contents => this.resolveNested(contents))
39
35
  );
40
36
  return `$%${promises.length - 1}%$`;
package/src/transport.ts CHANGED
@@ -3,7 +3,7 @@ import { EmailOptions, SentEmail } from './types';
3
3
  /**
4
4
  * Default mail transport
5
5
  *
6
- * @concrete ./internal/types:MailTransportTarget
6
+ * @concrete ./internal/types#MailTransportTarget
7
7
  */
8
8
  export interface MailTransport {
9
9
  send<S extends SentEmail = SentEmail>(mail: EmailOptions): Promise<S>;
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Readable } from 'stream';
2
- import { Url } from 'url';
1
+ import { FileLoader } from '@travetto/base';
2
+ import { Readable } from 'node:stream';
3
+ import { Url } from 'node:url';
3
4
 
4
5
  /**
5
6
  * An address
@@ -33,6 +34,9 @@ export interface EmailAttachment extends AttachmentLike {
33
34
 
34
35
  type EmailContentType = 'html' | 'text' | 'subject';
35
36
 
37
+ export type EmailIdentity = string | EmailAddress;
38
+ export type EmailIdentityList = EmailIdentity | EmailIdentity[];
39
+
36
40
  /**
37
41
  * Full message options
38
42
  */
@@ -42,13 +46,15 @@ export interface EmailOptions {
42
46
  subject: string;
43
47
  context?: Record<string, unknown>; // For templating
44
48
 
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;
49
+ disableThreading?: boolean;
50
+
51
+ from?: EmailIdentity;
52
+ sender?: EmailIdentity;
53
+ to?: EmailIdentityList;
54
+ cc?: EmailIdentityList;
55
+ bcc?: EmailIdentityList;
56
+ replyTo?: EmailIdentity;
57
+ inReplyTo?: EmailIdentity;
52
58
  references?: string | string[];
53
59
  headers?: Record<string, string | string[]>;
54
60
  attachments?: EmailAttachment[];
@@ -65,20 +71,15 @@ export type SentEmail = {
65
71
  export type EmailCompiled = Record<EmailContentType, string>;
66
72
 
67
73
  // Compilation support, defined here to allow for templates to not have a direct dependency on the compiler
68
- type BaseTemplateConfig = {
69
- search?: string[];
70
- inline?: boolean;
74
+ export type EmailTemplateResource = {
75
+ loader: FileLoader;
76
+ inlineStyle?: boolean;
77
+ inlineImages?: boolean;
78
+ globalStyles?: string;
71
79
  };
72
80
 
73
- export type EmailTemplateStyleConfig = BaseTemplateConfig & { global?: string };
74
- export type EmailTemplateImageConfig = BaseTemplateConfig & {};
75
-
76
- export type EmailTemplateConfig = {
77
- styles?: EmailTemplateStyleConfig;
78
- images?: EmailTemplateImageConfig;
79
- };
81
+ type EmailTemplateContent = Record<EmailContentType, () => (Promise<string> | string)>;
80
82
 
81
83
  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;
84
+ export type EmailTemplateModule = EmailTemplateResource & EmailTemplateContent;
85
+ export type EmailTemplateImport = { prepare(loc: EmailTemplateLocation): Promise<EmailTemplateModule> };
package/src/util.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { RootIndex } from '@travetto/manifest';
1
+ import { RuntimeContext } from '@travetto/manifest';
2
2
 
3
- import { EmailAttachment } from './types';
3
+ import { EmailAttachment, EmailIdentity, EmailIdentityList, EmailOptions } from './types';
4
+ import { Util } from '@travetto/base';
4
5
 
5
6
  /**
6
7
  * Utilities for email
@@ -15,7 +16,7 @@ export class MailUtil {
15
16
  static buildBrand(file: string, content: string, compile?: string): string {
16
17
  const out = [
17
18
  'WARNING: Do not modify.',
18
- `File is generated from "${file.replace(RootIndex.manifest.workspacePath, '.')}"`,
19
+ `File is generated from "${file.replace(RuntimeContext.workspace.path, '.')}"`,
19
20
  compile ? `Run \`${compile.replaceAll('\n', ' ')}\` to regenerate` : ''
20
21
  ];
21
22
  return `<!-- ${out.join(' ').trim()} -->\n${content}`;
@@ -57,4 +58,27 @@ export class MailUtil {
57
58
  html, attachments
58
59
  };
59
60
  }
61
+
62
+ /**
63
+ * Get the primary email, if set from an email identity or identity list
64
+ */
65
+ static getPrimaryEmail(src?: EmailIdentity | EmailIdentityList): string | undefined {
66
+ if (!src) {
67
+ return;
68
+ }
69
+ if (Array.isArray(src)) {
70
+ src = src[0];
71
+ }
72
+ return (typeof src === 'string') ? src : src.address;
73
+ }
74
+
75
+ /**
76
+ * Build a unique message id
77
+ */
78
+ static buildUniqueMessageId(message: EmailOptions): string {
79
+ const from = this.getPrimaryEmail(message.from)!;
80
+ const to = this.getPrimaryEmail(message.to)!;
81
+ const uid = Util.shortHash(`${to}${from}${message.subject}${Date.now()}`).substring(0, 12);
82
+ return `<${uid}@${from.split('@')[1]}>`;
83
+ }
60
84
  }