@breadstone/archipel-platform-mailing 0.0.34 → 0.0.35

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": "@breadstone/archipel-platform-mailing",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "description": "Email delivery with strategy-based transport and template engines for NestJS applications.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -134,10 +134,10 @@
134
134
  "README.md"
135
135
  ],
136
136
  "dependencies": {
137
- "@breadstone/archipel-platform-blob-storage": "0.0.34",
138
- "@breadstone/archipel-platform-configuration": "0.0.34",
139
- "@breadstone/archipel-platform-core": "0.0.34",
140
- "@breadstone/archipel-platform-resources": "0.0.34",
137
+ "@breadstone/archipel-platform-blob-storage": "0.0.35",
138
+ "@breadstone/archipel-platform-configuration": "0.0.35",
139
+ "@breadstone/archipel-platform-core": "0.0.35",
140
+ "@breadstone/archipel-platform-resources": "0.0.35",
141
141
  "tslib": "2.8.1"
142
142
  },
143
143
  "peerDependencies": {
@@ -1,29 +1,50 @@
1
1
  import type { IConfigRegistryEntry } from '@breadstone/archipel-platform-configuration';
2
- import { type DynamicModule, type Provider } from '@nestjs/common';
2
+ import { type DynamicModule, type Provider, type Type } from '@nestjs/common';
3
+ import type { DeliveryStrategyBase } from './delivering/strategies/abstracts/DeliveryStrategyBase';
3
4
  import type { IMailTemplateRepository } from './ports/IMailTemplateRepository';
5
+ import type { TemplateFetchStrategyBase } from './templating/strategies/abstracts/TemplateFetchStrategyBase';
4
6
  /**
5
7
  * Options for configuring the {@link MailModule}.
6
8
  *
7
9
  * @public
8
10
  */
9
11
  export interface IMailModuleOptions {
12
+ /**
13
+ * Concrete implementation of the {@link DeliveryStrategyBase} port.
14
+ * For example, `SmtpDeliveryStrategy`, `PostmarkDeliveryStrategy`, or `LogDeliveryStrategy`.
15
+ */
16
+ readonly deliveryStrategy: Type<DeliveryStrategyBase>;
17
+ /**
18
+ * Concrete implementation of the {@link TemplateFetchStrategyBase} port.
19
+ * For example, `FileTemplateFetchStrategy`, `BlobTemplateFetchStrategy`, or `DatabaseTemplateFetchStrategy`.
20
+ */
21
+ readonly templateFetchStrategy: Type<TemplateFetchStrategyBase>;
10
22
  /**
11
23
  * Whether to register the module globally.
12
24
  * Defaults to `true`.
13
25
  */
14
26
  readonly isGlobal?: boolean;
15
27
  /**
16
- * Additional or overriding configuration entries.
17
- * Merged with the platform default entries.
28
+ * Provider-specific configuration entries to register with the config module.
18
29
  */
19
30
  readonly configEntries?: ReadonlyArray<Omit<IConfigRegistryEntry, 'module'>>;
31
+ /**
32
+ * Template names to load when using the `FileTemplateFetchStrategy`.
33
+ * Each name corresponds to a resource file (e.g. `'AuthRegister'` loads
34
+ * `AuthRegister.html` and `AuthRegister.txt`).
35
+ *
36
+ * Defaults to an empty array (no templates loaded).
37
+ */
38
+ readonly templateNames?: ReadonlyArray<string>;
20
39
  /**
21
40
  * Provider for the `IMailTemplateRepository` port.
22
- * Required when `MAIL_TEMPLATE_STRATEGY` is set to `'database'`.
41
+ * Required when using `DatabaseTemplateFetchStrategy`.
23
42
  *
24
43
  * Example:
25
44
  * ```typescript
26
45
  * MailModule.register({
46
+ * deliveryStrategy: SmtpDeliveryStrategy,
47
+ * templateFetchStrategy: DatabaseTemplateFetchStrategy,
27
48
  * templateRepositoryProvider: {
28
49
  * provide: MAIL_TEMPLATE_REPOSITORY_TOKEN,
29
50
  * useClass: PrismaMailTemplateRepository,
@@ -36,19 +57,19 @@ export interface IMailModuleOptions {
36
57
  /**
37
58
  * Represents the mail module.
38
59
  *
39
- * Delivery and template strategies are resolved at runtime based on
40
- * configuration keys `MAIL_DELIVERY_STRATEGY` and `MAIL_TEMPLATE_STRATEGY`.
60
+ * Delivery and template strategies are selected at compile time via
61
+ * `IMailModuleOptions`. NestJS DI resolves the concrete classes.
41
62
  *
42
63
  * @public
43
64
  */
44
65
  export declare class MailModule {
45
66
  /**
46
- * Registers the mail module with optional configuration.
67
+ * Registers the mail module with the specified configuration.
47
68
  *
48
69
  * @param options - Module registration options.
49
70
  * @returns A configured `DynamicModule`.
50
71
  *
51
72
  * @public
52
73
  */
53
- static register(options?: IMailModuleOptions): DynamicModule;
74
+ static register(options: IMailModuleOptions): DynamicModule;
54
75
  }
package/src/MailModule.js CHANGED
@@ -7,7 +7,6 @@ const tslib_1 = require("tslib");
7
7
  const archipel_platform_blob_storage_1 = require("@breadstone/archipel-platform-blob-storage");
8
8
  const archipel_platform_configuration_1 = require("@breadstone/archipel-platform-configuration");
9
9
  const archipel_platform_core_1 = require("@breadstone/archipel-platform-core");
10
- const archipel_platform_resources_1 = require("@breadstone/archipel-platform-resources");
11
10
  const common_1 = require("@nestjs/common");
12
11
  const env_1 = require("./env");
13
12
  const MailEventSubscriber_1 = require("./events/listeners/MailEventSubscriber");
@@ -19,14 +18,15 @@ const MailTokens_1 = require("./tokens/MailTokens");
19
18
  /**
20
19
  * Represents the mail module.
21
20
  *
22
- * Delivery and template strategies are resolved at runtime based on
23
- * configuration keys `MAIL_DELIVERY_STRATEGY` and `MAIL_TEMPLATE_STRATEGY`.
21
+ * Delivery and template strategies are selected at compile time via
22
+ * `IMailModuleOptions`. NestJS DI resolves the concrete classes.
24
23
  *
25
24
  * @public
26
25
  */
27
26
  let MailModule = MailModule_1 = class MailModule {
27
+ // #region Methods
28
28
  /**
29
- * Registers the mail module with optional configuration.
29
+ * Registers the mail module with the specified configuration.
30
30
  *
31
31
  * @param options - Module registration options.
32
32
  * @returns A configured `DynamicModule`.
@@ -34,87 +34,42 @@ let MailModule = MailModule_1 = class MailModule {
34
34
  * @public
35
35
  */
36
36
  static register(options) {
37
- const isGlobal = options?.isGlobal ?? true;
38
- const configEntries = options?.configEntries ?? env_1.PLATFORM_MAILING_CONFIG_ENTRIES;
39
- const additionalProviders = [];
40
- if (options?.templateRepositoryProvider) {
41
- additionalProviders.push(options.templateRepositoryProvider);
37
+ const isGlobal = options.isGlobal ?? true;
38
+ const configEntries = options.configEntries ?? env_1.PLATFORM_MAILING_CONFIG_ENTRIES;
39
+ const providers = [
40
+ {
41
+ provide: MailTokens_1.MAIL_TEMPLATE_FETCH_STRATEGY_TOKEN,
42
+ useClass: options.templateFetchStrategy,
43
+ },
44
+ {
45
+ provide: MailTokens_1.MAIL_DELIVERY_STRATEGY_TOKEN,
46
+ useClass: options.deliveryStrategy,
47
+ },
48
+ {
49
+ provide: MailTokens_1.MAIL_TEMPLATE_NAMES_TOKEN,
50
+ useValue: options.templateNames ?? [],
51
+ },
52
+ archipel_platform_core_1.ContentTemplateEngine,
53
+ MailService_1.MailService,
54
+ MailTemplateEngine_1.MailTemplateEngine,
55
+ MailVerificationService_1.MailVerificationService,
56
+ SmtpConnectionVerifier_1.SmtpConnectionVerifier,
57
+ MailEventSubscriber_1.MailEventSubscriber,
58
+ ];
59
+ const exports = [
60
+ MailService_1.MailService,
61
+ MailVerificationService_1.MailVerificationService,
62
+ SmtpConnectionVerifier_1.SmtpConnectionVerifier,
63
+ ];
64
+ if (options.templateRepositoryProvider) {
65
+ providers.push(options.templateRepositoryProvider);
42
66
  }
43
67
  return {
44
68
  module: MailModule_1,
45
69
  global: isGlobal,
46
70
  imports: [archipel_platform_configuration_1.ConfigModule.register('platform-mailing', configEntries), archipel_platform_blob_storage_1.BlobModule, archipel_platform_core_1.EventModule],
47
- providers: [
48
- ...additionalProviders,
49
- {
50
- provide: MailTokens_1.MAIL_TEMPLATE_FETCH_STRATEGY_TOKEN,
51
- useFactory: async (configService, blobService, resourceManager, ...extra) => {
52
- const templateStrategy = configService.get(env_1.MAIL_TEMPLATE_STRATEGY.key);
53
- if (templateStrategy === 'file') {
54
- const { FileTemplateFetchStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./templating/strategies/file/FileTemplateFetchStrategy')));
55
- return new FileTemplateFetchStrategy(resourceManager);
56
- }
57
- if (templateStrategy === 'blob') {
58
- const { BlobTemplateFetchStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./templating/strategies/blob/BlobTemplateFetchStrategy')));
59
- return new BlobTemplateFetchStrategy(blobService);
60
- }
61
- if (templateStrategy === 'database') {
62
- const repository = extra[0];
63
- if (!repository) {
64
- throw new Error("Template strategy 'database' requires a templateRepositoryProvider in MailModule.register() options.");
65
- }
66
- const { DatabaseTemplateFetchStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./templating/strategies/database/DatabaseTemplateFetchStrategy')));
67
- return new DatabaseTemplateFetchStrategy(repository);
68
- }
69
- throw new Error(`Unknown template strategy: ${templateStrategy}`);
70
- },
71
- inject: [
72
- archipel_platform_configuration_1.ConfigService,
73
- archipel_platform_blob_storage_1.BlobService,
74
- archipel_platform_resources_1.ResourceManager,
75
- ...(options?.templateRepositoryProvider ? [MailTokens_1.MAIL_TEMPLATE_REPOSITORY_TOKEN] : []),
76
- ],
77
- },
78
- {
79
- provide: MailTokens_1.MAIL_DELIVERY_STRATEGY_TOKEN,
80
- useFactory: async (configService) => {
81
- const deliveryStrategy = configService.get(env_1.MAIL_DELIVERY_STRATEGY.key);
82
- if (deliveryStrategy === 'log') {
83
- const { LogDeliveryStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./delivering/strategies/logger/LogDeliveryStrategy')));
84
- return new LogDeliveryStrategy();
85
- }
86
- if (deliveryStrategy === 'smtp') {
87
- const { SmtpDeliveryStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./delivering/strategies/smtp/SmtpDeliveryStrategy')));
88
- return new SmtpDeliveryStrategy(configService);
89
- }
90
- if (deliveryStrategy === 'postmark') {
91
- const { PostmarkDeliveryStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./delivering/strategies/postmark/PostmarkDeliveryStrategy')));
92
- return new PostmarkDeliveryStrategy(configService);
93
- }
94
- if (deliveryStrategy === 'resend') {
95
- const { ResendDeliveryStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./delivering/strategies/resend/ResendDeliveryStrategy')));
96
- return new ResendDeliveryStrategy(configService);
97
- }
98
- if (deliveryStrategy === 'sendgrid') {
99
- const { SendGridDeliveryStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./delivering/strategies/sendgrid/SendGridDeliveryStrategy')));
100
- return new SendGridDeliveryStrategy(configService);
101
- }
102
- if (deliveryStrategy === 'mailgun') {
103
- const { MailgunDeliveryStrategy } = await Promise.resolve().then(() => tslib_1.__importStar(require('./delivering/strategies/mailgun/MailgunDeliveryStrategy')));
104
- return new MailgunDeliveryStrategy(configService);
105
- }
106
- throw new Error(`Unknown delivery strategy: ${deliveryStrategy}`);
107
- },
108
- inject: [archipel_platform_configuration_1.ConfigService],
109
- },
110
- archipel_platform_core_1.ContentTemplateEngine,
111
- MailService_1.MailService,
112
- MailTemplateEngine_1.MailTemplateEngine,
113
- MailVerificationService_1.MailVerificationService,
114
- SmtpConnectionVerifier_1.SmtpConnectionVerifier,
115
- MailEventSubscriber_1.MailEventSubscriber,
116
- ],
117
- exports: [MailService_1.MailService, MailVerificationService_1.MailVerificationService, SmtpConnectionVerifier_1.SmtpConnectionVerifier],
71
+ providers,
72
+ exports,
118
73
  };
119
74
  }
120
75
  };
package/src/env.d.ts CHANGED
@@ -1,8 +1,4 @@
1
1
  import { IConfigRegistryEntry } from '@breadstone/archipel-platform-configuration';
2
- /** Which template-fetch strategy to use (e.g. `file`, `blob`). */
3
- export declare const MAIL_TEMPLATE_STRATEGY: import("@breadstone/archipel-platform-configuration").IConfigKey<string>;
4
- /** Which delivery strategy to use (e.g. `smtp`, `postmark`, `resend`, `sendgrid`, `mailgun`, `log`). */
5
- export declare const MAIL_DELIVERY_STRATEGY: import("@breadstone/archipel-platform-configuration").IConfigKey<string>;
6
2
  /** Sender e-mail address shown in the `From` header. */
7
3
  export declare const MAIL_SENDER_EMAIL: import("@breadstone/archipel-platform-configuration").IConfigKey<string>;
8
4
  /** Template engine format identifier (e.g. `handlebars`). */
package/src/env.js CHANGED
@@ -1,16 +1,12 @@
1
1
  "use strict";
2
2
  // #region Imports
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.PLATFORM_MAILING_CONFIG_ENTRIES = exports.MAIL_PORT = exports.MAIL_USER = exports.MAIL_HOST = exports.MAIL_SMTP_PASSWORD = exports.MAIL_SMTP_USER = exports.MAIL_SMTP_SECURE = exports.MAIL_SMTP_PORT = exports.MAIL_SMTP_HOST = exports.MAIL_MAILGUN_DOMAIN = exports.MAIL_MAILGUN_API_KEY = exports.MAIL_SENDGRID_API_KEY = exports.MAIL_RESEND_API_KEY = exports.MAIL_POSTMARK_API_KEY = exports.MAIL_TEMPLATE_ENGINE_FORMAT = exports.MAIL_SENDER_EMAIL = exports.MAIL_DELIVERY_STRATEGY = exports.MAIL_TEMPLATE_STRATEGY = void 0;
4
+ exports.PLATFORM_MAILING_CONFIG_ENTRIES = exports.MAIL_PORT = exports.MAIL_USER = exports.MAIL_HOST = exports.MAIL_SMTP_PASSWORD = exports.MAIL_SMTP_USER = exports.MAIL_SMTP_SECURE = exports.MAIL_SMTP_PORT = exports.MAIL_SMTP_HOST = exports.MAIL_MAILGUN_DOMAIN = exports.MAIL_MAILGUN_API_KEY = exports.MAIL_SENDGRID_API_KEY = exports.MAIL_RESEND_API_KEY = exports.MAIL_POSTMARK_API_KEY = exports.MAIL_TEMPLATE_ENGINE_FORMAT = exports.MAIL_SENDER_EMAIL = void 0;
5
5
  const archipel_platform_configuration_1 = require("@breadstone/archipel-platform-configuration");
6
6
  // #endregion
7
7
  // ──────────────────────────────────────────────────────────────
8
- // Delivery & Template Strategy
8
+ // Core
9
9
  // ──────────────────────────────────────────────────────────────
10
- /** Which template-fetch strategy to use (e.g. `file`, `blob`). */
11
- exports.MAIL_TEMPLATE_STRATEGY = (0, archipel_platform_configuration_1.createConfigKey)('MAIL_TEMPLATE_STRATEGY');
12
- /** Which delivery strategy to use (e.g. `smtp`, `postmark`, `resend`, `sendgrid`, `mailgun`, `log`). */
13
- exports.MAIL_DELIVERY_STRATEGY = (0, archipel_platform_configuration_1.createConfigKey)('MAIL_DELIVERY_STRATEGY');
14
10
  /** Sender e-mail address shown in the `From` header. */
15
11
  exports.MAIL_SENDER_EMAIL = (0, archipel_platform_configuration_1.createConfigKey)('MAIL_SENDER_EMAIL');
16
12
  /** Template engine format identifier (e.g. `handlebars`). */
@@ -64,13 +60,7 @@ exports.MAIL_PORT = (0, archipel_platform_configuration_1.createConfigKey)('MAIL
64
60
  // ──────────────────────────────────────────────────────────────
65
61
  /** All configuration entries required by `platform-mailing`. */
66
62
  exports.PLATFORM_MAILING_CONFIG_ENTRIES = [
67
- // Strategy
68
- { key: exports.MAIL_TEMPLATE_STRATEGY, required: true, description: 'Template-fetch strategy (file, blob, database).' },
69
- {
70
- key: exports.MAIL_DELIVERY_STRATEGY,
71
- required: true,
72
- description: 'Delivery strategy (smtp, postmark, resend, sendgrid, mailgun, log).',
73
- },
63
+ // Core
74
64
  { key: exports.MAIL_SENDER_EMAIL, required: true, description: 'Sender e-mail address for the From header.' },
75
65
  { key: exports.MAIL_TEMPLATE_ENGINE_FORMAT, required: true, description: 'Template engine format identifier.' },
76
66
  // Postmark
@@ -2,14 +2,25 @@ import { IEventMap } from '@breadstone/archipel-platform-core';
2
2
  /**
3
3
  * Represents the contract of events emitted by the mail domain.
4
4
  *
5
+ * Publishers only need to specify `recipientEmail`, `templateName`, and `templateParams`.
6
+ * The subject is resolved from the stored template (industry-standard behavior).
7
+ * An explicit `subject` can be provided as an override.
8
+ *
5
9
  * @public
6
10
  */
7
11
  export interface IMailEvents extends IEventMap {
8
12
  sendMail: {
13
+ /** Recipient email address. */
9
14
  recipientEmail: string;
10
- subject: string;
15
+ /** Template key to resolve (e.g. `'AuthRegister'`). */
11
16
  templateName: string;
17
+ /** Placeholder values to interpolate into subject and body. */
12
18
  templateParams: Record<string, string | number | boolean | Date | undefined | null>;
19
+ /** Optional locale for template resolution (e.g. `'de'`, `'en'`). */
20
+ locale?: string;
21
+ /** Optional subject override. When omitted, resolved from the template. */
22
+ subject?: string;
23
+ /** Optional sender email override. When omitted, uses the configured default. */
13
24
  senderEmail?: string;
14
25
  };
15
26
  }
@@ -29,7 +29,8 @@ let MailEventSubscriber = MailEventSubscriber_1 = class MailEventSubscriber {
29
29
  this._mailService = mailService;
30
30
  this._subscription = eventHub.observe(MailEventKeys_1.SEND_MAIL_EVENT_KEY).subscribe({
31
31
  next: (payload) => {
32
- this._logger.debug(`Received mail event for recipient ${(0, archipel_platform_core_1.maskSensitive)(payload.recipientEmail)} with subject "${(0, archipel_platform_core_1.maskSensitive)(payload.subject)}"`);
32
+ this._logger.debug(`Received mail event for recipient ${(0, archipel_platform_core_1.maskSensitive)(payload.recipientEmail)} ` +
33
+ `with template "${payload.templateName}"${payload.subject ? ` (subject override: "${(0, archipel_platform_core_1.maskSensitive)(payload.subject)}")` : ''}`);
33
34
  void this.handle(payload);
34
35
  },
35
36
  error: (err) => {
package/src/index.d.ts CHANGED
@@ -5,6 +5,8 @@ export * from './events';
5
5
  export * from './MailModule';
6
6
  export * from './models/IMailAttachment';
7
7
  export * from './models/IMailTemplate';
8
+ export * from './models/IMailTemplateVariants';
9
+ export * from './models/MailTemplateFormat';
8
10
  export * from './ports/IMailTemplateRepository';
9
11
  export * from './services/MailService';
10
12
  export * from './services/MailVerificationService';
package/src/index.js CHANGED
@@ -8,6 +8,8 @@ tslib_1.__exportStar(require("./events"), exports);
8
8
  tslib_1.__exportStar(require("./MailModule"), exports);
9
9
  tslib_1.__exportStar(require("./models/IMailAttachment"), exports);
10
10
  tslib_1.__exportStar(require("./models/IMailTemplate"), exports);
11
+ tslib_1.__exportStar(require("./models/IMailTemplateVariants"), exports);
12
+ tslib_1.__exportStar(require("./models/MailTemplateFormat"), exports);
11
13
  tslib_1.__exportStar(require("./ports/IMailTemplateRepository"), exports);
12
14
  tslib_1.__exportStar(require("./services/MailService"), exports);
13
15
  tslib_1.__exportStar(require("./services/MailVerificationService"), exports);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Represents the in-memory structure for a single template (multiple formats).
3
+ *
4
+ * @public
5
+ */
6
+ export interface IMailTemplateVariants {
7
+ subject?: string;
8
+ html?: string;
9
+ txt?: string;
10
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=IMailTemplateVariants.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Union type of all supported template formats.
3
+ *
4
+ * @public
5
+ */
6
+ export type MailTemplateFormat = 'html' | 'txt';
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=MailTemplateFormat.js.map
@@ -37,17 +37,21 @@ export declare class MailService {
37
37
  * Sends an email using a predefined template (HTML or text).
38
38
  * Automatically uses the template format (either 'html' or 'txt') based on the environment variable.
39
39
  *
40
+ * When no `subject` is provided in `mailOptions`, the subject is resolved from the
41
+ * stored template and interpolated with the given `templateParams` (industry-standard
42
+ * behavior matching Mailchimp/SendGrid/Postmark).
43
+ *
40
44
  * @public
41
45
  * @param mailOptions.from - Sender's email address (optional, defaults to config)
42
46
  * @param mailOptions.to - Recipient's email address
43
- * @param mailOptions.subject - Subject of the email
47
+ * @param mailOptions.subject - Subject of the email (optional — resolved from template if omitted)
44
48
  * @param templateOptions.templateName - Name of the template to be used
45
- * @param templateOptions.params - Parameters to inject into the template
49
+ * @param templateOptions.templateParams - Parameters to inject into the template
46
50
  */
47
51
  sendTemplate(mailOptions: {
48
52
  from?: string;
49
53
  to: string;
50
- subject: string;
54
+ subject?: string;
51
55
  }, templateOptions: {
52
56
  templateName: string;
53
57
  templateParams: Record<string, string | number | boolean | Date | undefined | null>;
@@ -62,17 +62,26 @@ let MailService = class MailService {
62
62
  * Sends an email using a predefined template (HTML or text).
63
63
  * Automatically uses the template format (either 'html' or 'txt') based on the environment variable.
64
64
  *
65
+ * When no `subject` is provided in `mailOptions`, the subject is resolved from the
66
+ * stored template and interpolated with the given `templateParams` (industry-standard
67
+ * behavior matching Mailchimp/SendGrid/Postmark).
68
+ *
65
69
  * @public
66
70
  * @param mailOptions.from - Sender's email address (optional, defaults to config)
67
71
  * @param mailOptions.to - Recipient's email address
68
- * @param mailOptions.subject - Subject of the email
72
+ * @param mailOptions.subject - Subject of the email (optional — resolved from template if omitted)
69
73
  * @param templateOptions.templateName - Name of the template to be used
70
- * @param templateOptions.params - Parameters to inject into the template
74
+ * @param templateOptions.templateParams - Parameters to inject into the template
71
75
  */
72
76
  async sendTemplate(mailOptions, templateOptions) {
73
77
  const content = this._mailTemplateEngine.compileTemplate(templateOptions.templateName, templateOptions.templateParams, this._mailTemplateFormat);
74
78
  const isHtml = this._mailTemplateFormat === 'html';
75
- await this.send(mailOptions, content, isHtml);
79
+ const subject = mailOptions.subject ?? this._mailTemplateEngine.resolveSubject(templateOptions.templateName, templateOptions.templateParams);
80
+ if (!subject) {
81
+ throw new Error(`No subject provided and template '${templateOptions.templateName}' has no stored subject. ` +
82
+ 'Either pass a subject in mailOptions or ensure the template includes a subject.');
83
+ }
84
+ await this.send({ from: mailOptions.from, to: mailOptions.to, subject }, content, isHtml);
76
85
  }
77
86
  /**
78
87
  * Performs a minimal email address validation. Not exhaustive but filters obvious mistakes.
@@ -97,9 +106,7 @@ let MailService = class MailService {
97
106
  if (!IMailAttachment_1.ALLOWED_ATTACHMENT_MIME_TYPES.includes(attachment.contentType)) {
98
107
  throw new Error(`Unsupported attachment MIME type: '${attachment.contentType}'`);
99
108
  }
100
- const size = typeof attachment.content === 'string'
101
- ? Buffer.byteLength(attachment.content, 'utf-8')
102
- : attachment.content.length;
109
+ const size = typeof attachment.content === 'string' ? Buffer.byteLength(attachment.content, 'utf-8') : attachment.content.length;
103
110
  if (size > IMailAttachment_1.MAX_ATTACHMENT_SIZE_BYTES) {
104
111
  throw new Error(`Attachment '${attachment.filename}' exceeds maximum size of ${IMailAttachment_1.MAX_ATTACHMENT_SIZE_BYTES} bytes`);
105
112
  }
@@ -1,6 +1,6 @@
1
1
  import { ContentTemplateEngine } from '@breadstone/archipel-platform-core';
2
2
  import { OnModuleInit } from '@nestjs/common';
3
- import { MailTemplateFormat } from '../tokens/MailTokens';
3
+ import type { MailTemplateFormat } from '../models/MailTemplateFormat';
4
4
  import { TemplateFetchStrategyBase } from './strategies/abstracts/TemplateFetchStrategyBase';
5
5
  /**
6
6
  * The `MailTemplateEngine` class.
@@ -35,6 +35,15 @@ export declare class MailTemplateEngine implements OnModuleInit {
35
35
  * @returns The compiled template as a string.
36
36
  */
37
37
  compileTemplate(templateKey: string, params: Record<string, unknown>, format: MailTemplateFormat): string;
38
+ /**
39
+ * Resolves and interpolates the subject line from a stored template.
40
+ *
41
+ * @public
42
+ * @param templateKey The key of the template.
43
+ * @param params The parameters to insert into the subject.
44
+ * @returns The compiled subject string, or `undefined` if no subject is stored.
45
+ */
46
+ resolveSubject(templateKey: string, params: Record<string, unknown>): string | undefined;
38
47
  private escapeHtmlParams;
39
48
  private escapeHtml;
40
49
  }
@@ -66,6 +66,21 @@ let MailTemplateEngine = class MailTemplateEngine {
66
66
  throw error;
67
67
  }
68
68
  }
69
+ /**
70
+ * Resolves and interpolates the subject line from a stored template.
71
+ *
72
+ * @public
73
+ * @param templateKey The key of the template.
74
+ * @param params The parameters to insert into the subject.
75
+ * @returns The compiled subject string, or `undefined` if no subject is stored.
76
+ */
77
+ resolveSubject(templateKey, params) {
78
+ const templateVariants = this._templates[templateKey];
79
+ if (!templateVariants?.subject) {
80
+ return undefined;
81
+ }
82
+ return this._contentTemplateEngine.compileTemplate(templateVariants.subject, params);
83
+ }
69
84
  escapeHtmlParams(params) {
70
85
  const escaped = {};
71
86
  for (const [key, value] of Object.entries(params)) {
@@ -74,12 +89,7 @@ let MailTemplateEngine = class MailTemplateEngine {
74
89
  return escaped;
75
90
  }
76
91
  escapeHtml(text) {
77
- return text
78
- .replace(/&/g, '&amp;')
79
- .replace(/</g, '&lt;')
80
- .replace(/>/g, '&gt;')
81
- .replace(/"/g, '&quot;')
82
- .replace(/'/g, '&#39;');
92
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
83
93
  }
84
94
  };
85
95
  exports.MailTemplateEngine = MailTemplateEngine;
@@ -1,3 +1,4 @@
1
+ import type { IMailTemplateVariants } from '../../../models/IMailTemplateVariants';
1
2
  /**
2
3
  * Represents the base class for template fetch strategies.
3
4
  *
@@ -17,8 +18,5 @@ export declare abstract class TemplateFetchStrategyBase {
17
18
  * @public
18
19
  * @abstract
19
20
  */
20
- abstract load(): Promise<Record<string, {
21
- html?: string;
22
- txt?: string;
23
- }>>;
21
+ abstract load(): Promise<Record<string, IMailTemplateVariants>>;
24
22
  }
@@ -1,5 +1,5 @@
1
1
  import { BlobService } from '@breadstone/archipel-platform-blob-storage';
2
- import { IMailTemplateVariants } from '../../../tokens/MailTokens';
2
+ import type { IMailTemplateVariants } from '../../../models/IMailTemplateVariants';
3
3
  import { TemplateFetchStrategyBase } from '../abstracts/TemplateFetchStrategyBase';
4
4
  /**
5
5
  * The `BlobTemplateFetchStrategy` class.
@@ -1,5 +1,5 @@
1
+ import type { IMailTemplateVariants } from '../../../models/IMailTemplateVariants';
1
2
  import type { IMailTemplateRepository } from '../../../ports/IMailTemplateRepository';
2
- import type { IMailTemplateVariants } from '../../../tokens/MailTokens';
3
3
  import { TemplateFetchStrategyBase } from '../abstracts/TemplateFetchStrategyBase';
4
4
  /**
5
5
  * Template fetch strategy that loads templates from a database via the
@@ -44,6 +44,9 @@ let DatabaseTemplateFetchStrategy = DatabaseTemplateFetchStrategy_1 = class Data
44
44
  templates[key] = {};
45
45
  }
46
46
  // Only set if not already filled (first match wins — default locale preferred)
47
+ if (template.subject && !templates[key].subject) {
48
+ templates[key].subject = template.subject;
49
+ }
47
50
  if (template.htmlBody && !templates[key].html) {
48
51
  templates[key].html = template.htmlBody;
49
52
  }
@@ -1,25 +1,23 @@
1
1
  import { ResourceManager } from '@breadstone/archipel-platform-resources';
2
- import { IMailTemplateVariants } from '../../../tokens/MailTokens';
2
+ import type { IMailTemplateVariants } from '../../../models/IMailTemplateVariants';
3
3
  import { TemplateFetchStrategyBase } from '../abstracts/TemplateFetchStrategyBase';
4
4
  /**
5
5
  * The `FileTemplateFetchStrategy` class.
6
6
  *
7
+ * Loads mail templates from the file system via `ResourceManager`.
8
+ * Template names are injected via `MAIL_TEMPLATE_NAMES_TOKEN` — defaults to an empty array.
9
+ *
7
10
  * @public
8
11
  */
9
12
  export declare class FileTemplateFetchStrategy extends TemplateFetchStrategyBase {
10
13
  private readonly _resourceManager;
14
+ private readonly _templateNames;
11
15
  private readonly _logger;
12
- constructor(resourceManager: ResourceManager);
16
+ constructor(resourceManager: ResourceManager, templateNames: ReadonlyArray<string>);
13
17
  /**
14
18
  * Loads mail templates from the file system.
15
19
  *
16
20
  * @public
17
21
  */
18
22
  load(): Promise<Record<string, IMailTemplateVariants>>;
19
- /**
20
- * Returns the list of known template names.
21
- *
22
- * @private
23
- */
24
- private getTemplateNames;
25
23
  }
@@ -6,20 +6,25 @@ exports.FileTemplateFetchStrategy = void 0;
6
6
  const tslib_1 = require("tslib");
7
7
  const archipel_platform_resources_1 = require("@breadstone/archipel-platform-resources");
8
8
  const common_1 = require("@nestjs/common");
9
+ const MailTokens_1 = require("../../../tokens/MailTokens");
9
10
  const TemplateFetchStrategyBase_1 = require("../abstracts/TemplateFetchStrategyBase");
10
11
  // #endregion
11
12
  /**
12
13
  * The `FileTemplateFetchStrategy` class.
13
14
  *
15
+ * Loads mail templates from the file system via `ResourceManager`.
16
+ * Template names are injected via `MAIL_TEMPLATE_NAMES_TOKEN` — defaults to an empty array.
17
+ *
14
18
  * @public
15
19
  */
16
20
  let FileTemplateFetchStrategy = FileTemplateFetchStrategy_1 = class FileTemplateFetchStrategy extends TemplateFetchStrategyBase_1.TemplateFetchStrategyBase {
17
21
  // #endregion
18
22
  // #region Ctor
19
- constructor(resourceManager) {
23
+ constructor(resourceManager, templateNames) {
20
24
  super();
21
25
  this._logger = new common_1.Logger(FileTemplateFetchStrategy_1.name);
22
26
  this._resourceManager = resourceManager;
27
+ this._templateNames = templateNames;
23
28
  }
24
29
  // #endregion
25
30
  // #region Methods
@@ -30,9 +35,8 @@ let FileTemplateFetchStrategy = FileTemplateFetchStrategy_1 = class FileTemplate
30
35
  */
31
36
  async load() {
32
37
  const templates = {};
33
- const templateNames = this.getTemplateNames();
34
38
  let templateCount = 0;
35
- for (const templateName of templateNames) {
39
+ for (const templateName of this._templateNames) {
36
40
  if (!templates[templateName]) {
37
41
  templates[templateName] = {};
38
42
  }
@@ -52,20 +56,11 @@ let FileTemplateFetchStrategy = FileTemplateFetchStrategy_1 = class FileTemplate
52
56
  this._logger.log(`${templateCount} Templates loaded from file system`);
53
57
  return templates;
54
58
  }
55
- // #endregion
56
- // #region Private Methods
57
- /**
58
- * Returns the list of known template names.
59
- *
60
- * @private
61
- */
62
- getTemplateNames() {
63
- return ['AppointmentInvitation', 'AppointmentUpdate', 'AuthForgotPassword', 'AuthRegister', 'AuthVerify'];
64
- }
65
59
  };
66
60
  exports.FileTemplateFetchStrategy = FileTemplateFetchStrategy;
67
61
  exports.FileTemplateFetchStrategy = FileTemplateFetchStrategy = FileTemplateFetchStrategy_1 = tslib_1.__decorate([
68
62
  (0, common_1.Injectable)(),
69
- tslib_1.__metadata("design:paramtypes", [archipel_platform_resources_1.ResourceManager])
63
+ tslib_1.__param(1, (0, common_1.Inject)(MailTokens_1.MAIL_TEMPLATE_NAMES_TOKEN)),
64
+ tslib_1.__metadata("design:paramtypes", [archipel_platform_resources_1.ResourceManager, Array])
70
65
  ], FileTemplateFetchStrategy);
71
66
  //# sourceMappingURL=FileTemplateFetchStrategy.js.map
@@ -6,14 +6,4 @@
6
6
  export declare const MAIL_TEMPLATE_FETCH_STRATEGY_TOKEN: unique symbol;
7
7
  export declare const MAIL_DELIVERY_STRATEGY_TOKEN: unique symbol;
8
8
  export declare const MAIL_TEMPLATE_REPOSITORY_TOKEN: unique symbol;
9
- /**
10
- * Union type of all supported template formats.
11
- */
12
- export type MailTemplateFormat = 'html' | 'txt';
13
- /**
14
- * Represents the in-memory structure for a single template (multiple formats).
15
- */
16
- export interface IMailTemplateVariants {
17
- html?: string;
18
- txt?: string;
19
- }
9
+ export declare const MAIL_TEMPLATE_NAMES_TOKEN: unique symbol;
@@ -7,8 +7,9 @@
7
7
  */
8
8
  // #endregion
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.MAIL_TEMPLATE_REPOSITORY_TOKEN = exports.MAIL_DELIVERY_STRATEGY_TOKEN = exports.MAIL_TEMPLATE_FETCH_STRATEGY_TOKEN = void 0;
10
+ exports.MAIL_TEMPLATE_NAMES_TOKEN = exports.MAIL_TEMPLATE_REPOSITORY_TOKEN = exports.MAIL_DELIVERY_STRATEGY_TOKEN = exports.MAIL_TEMPLATE_FETCH_STRATEGY_TOKEN = void 0;
11
11
  exports.MAIL_TEMPLATE_FETCH_STRATEGY_TOKEN = Symbol('MailTemplateFetchStrategy');
12
12
  exports.MAIL_DELIVERY_STRATEGY_TOKEN = Symbol('MailDeliveryStrategy');
13
13
  exports.MAIL_TEMPLATE_REPOSITORY_TOKEN = Symbol('MailTemplateRepository');
14
+ exports.MAIL_TEMPLATE_NAMES_TOKEN = Symbol('MailTemplateNames');
14
15
  //# sourceMappingURL=MailTokens.js.map