@xcelsior/email 1.0.2 → 1.0.4

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.
@@ -1,22 +1,22 @@
1
1
 
2
- > @xcelsior/email@1.0.0 build /Users/tuannguyen/Work/excelsior-packages/packages/services/email
3
- > tsup
2
+ > @xcelsior/email@1.0.3 build /Users/tuannguyen/Work/xcelsior-packages/packages/services/email
3
+ > tsup && tsc --noEmit
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.0
8
- CLI Using tsup config: /Users/tuannguyen/Work/excelsior-packages/packages/services/email/tsup.config.ts
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /Users/tuannguyen/Work/xcelsior-packages/packages/services/email/tsup.config.ts
9
9
  CLI Target: es2020
10
10
  CLI Cleaning output folder
11
11
  CJS Build start
12
12
  ESM Build start
13
- ESM dist/index.mjs 5.19 KB
14
- ESM dist/index.mjs.map 11.34 KB
15
- ESM ⚡️ Build success in 27ms
16
- CJS dist/index.js 7.42 KB
17
- CJS dist/index.js.map 11.60 KB
18
- CJS ⚡️ Build success in 27ms
13
+ ESM dist/index.mjs 5.72 KB
14
+ ESM dist/index.mjs.map 12.54 KB
15
+ ESM ⚡️ Build success in 40ms
16
+ CJS dist/index.js 7.95 KB
17
+ CJS dist/index.js.map 12.80 KB
18
+ CJS ⚡️ Build success in 40ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 3138ms
21
- DTS dist/index.d.ts 849.00 B
22
- DTS dist/index.d.mts 849.00 B
20
+ DTS ⚡️ Build success in 1658ms
21
+ DTS dist/index.d.ts 818.00 B
22
+ DTS dist/index.d.mts 818.00 B
@@ -1,5 +1,5 @@
1
1
 
2
- > @xcelsior/email@1.0.0 lint /Users/tuannguyen/Work/excelsior-packages/packages/services/email
2
+ > @xcelsior/email@1.0.3 lint /Users/tuannguyen/Work/xcelsior-packages/packages/services/email
3
3
  > biome check .
4
4
 
5
- Checked 9 files in 19ms. No fixes applied.
5
+ Checked 9 files in 12ms. No fixes applied.
@@ -1,5 +1,5 @@
1
1
 
2
- > @xcelsior/email@1.0.0 test /Users/tuannguyen/Work/excelsior-packages/packages/services/email
2
+ > @xcelsior/email@1.0.3 test /Users/tuannguyen/Work/xcelsior-packages/packages/services/email
3
3
  > jest --passWithNoTests
4
4
 
5
5
  Determining test suites to run...No tests found, exiting with code 0
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @xcelsior/email
2
+
3
+ ## 1.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - improve email
8
+
9
+ ## 1.0.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [e734bf5]
14
+ - @xcelsior/utils@1.0.1
package/dist/index.d.mts CHANGED
@@ -1,5 +1,39 @@
1
1
  import * as nodemailer_lib_smtp_pool from 'nodemailer/lib/smtp-pool';
2
2
 
3
+ interface SMTPTransportConfig {
4
+ host: string;
5
+ port?: number;
6
+ auth?: {
7
+ user: string;
8
+ pass: string;
9
+ };
10
+ }
11
+ interface SESTransportConfig {
12
+ region?: string;
13
+ sourceArn?: string;
14
+ credentials?: {
15
+ accessKeyId: string;
16
+ secretAccessKey: string;
17
+ };
18
+ }
19
+ interface EmailLoggingConfig {
20
+ /** DynamoDB table name for email logs */
21
+ tableName: string;
22
+ /** AWS region for the logging table */
23
+ region?: string;
24
+ }
25
+ interface EmailServiceConfig {
26
+ /** Transport type – defaults to 'ses' */
27
+ transport?: 'smtp' | 'ses';
28
+ /** SMTP options (required when transport is 'smtp') */
29
+ smtp?: SMTPTransportConfig;
30
+ /** SES options (used when transport is 'ses') */
31
+ ses?: SESTransportConfig;
32
+ /** Email logging configuration – if omitted, logging is derived from env vars */
33
+ logging?: EmailLoggingConfig;
34
+ /** Enable nodemailer debug output */
35
+ debug?: boolean;
36
+ }
3
37
  interface Email {
4
38
  to?: string | null;
5
39
  from?: string;
@@ -11,11 +45,38 @@ interface Email {
11
45
  attachments?: any[];
12
46
  subject: string;
13
47
  }
14
- declare const emailTransporter: (logger?: any) => {
48
+ /**
49
+ * Create an email transporter.
50
+ *
51
+ * Accepts an optional {@link EmailServiceConfig} so callers can pass
52
+ * explicit configuration instead of relying on environment variables.
53
+ * Any value not provided in `config` falls back to the corresponding
54
+ * `process.env` variable, preserving full backward-compatibility.
55
+ */
56
+ declare const emailTransporter: (logger?: any, config?: EmailServiceConfig) => {
15
57
  sendMail({ to, reference, from, replyTo, template, text, attachments, idempotencyKey, subject, }: Email, idempotentCheck?: boolean): Promise<nodemailer_lib_smtp_pool.SentMessageInfo | undefined>;
16
58
  };
17
59
 
18
60
  declare function renderTemplate(content: string, data: Record<string, any>): string;
19
61
  declare function renderTemplateFile(file: string, data: Record<string, any>): Promise<string>;
20
62
 
21
- export { type Email, emailTransporter, renderTemplate, renderTemplateFile };
63
+ interface EmailLog {
64
+ reference?: string;
65
+ timestamp: number;
66
+ subject: string;
67
+ sender: string;
68
+ receiver: string;
69
+ idempotencyKey?: string;
70
+ status: 'success' | 'failed';
71
+ error?: string;
72
+ }
73
+ declare class EmailLogService {
74
+ private readonly tableName;
75
+ private readonly dynamoClient;
76
+ private readonly docClient;
77
+ constructor(loggingConfig?: EmailLoggingConfig);
78
+ logEmail({ subject, sender, receiver, status, error, idempotencyKey, reference, }: Omit<EmailLog, 'timestamp'>): Promise<string>;
79
+ getEmailByIdempotencyKey(idempotencyKey: string | undefined): Promise<EmailLog | null>;
80
+ }
81
+
82
+ export { type Email, EmailLogService, type EmailLoggingConfig, type EmailServiceConfig, type SESTransportConfig, type SMTPTransportConfig, emailTransporter, renderTemplate, renderTemplateFile };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,39 @@
1
1
  import * as nodemailer_lib_smtp_pool from 'nodemailer/lib/smtp-pool';
2
2
 
3
+ interface SMTPTransportConfig {
4
+ host: string;
5
+ port?: number;
6
+ auth?: {
7
+ user: string;
8
+ pass: string;
9
+ };
10
+ }
11
+ interface SESTransportConfig {
12
+ region?: string;
13
+ sourceArn?: string;
14
+ credentials?: {
15
+ accessKeyId: string;
16
+ secretAccessKey: string;
17
+ };
18
+ }
19
+ interface EmailLoggingConfig {
20
+ /** DynamoDB table name for email logs */
21
+ tableName: string;
22
+ /** AWS region for the logging table */
23
+ region?: string;
24
+ }
25
+ interface EmailServiceConfig {
26
+ /** Transport type – defaults to 'ses' */
27
+ transport?: 'smtp' | 'ses';
28
+ /** SMTP options (required when transport is 'smtp') */
29
+ smtp?: SMTPTransportConfig;
30
+ /** SES options (used when transport is 'ses') */
31
+ ses?: SESTransportConfig;
32
+ /** Email logging configuration – if omitted, logging is derived from env vars */
33
+ logging?: EmailLoggingConfig;
34
+ /** Enable nodemailer debug output */
35
+ debug?: boolean;
36
+ }
3
37
  interface Email {
4
38
  to?: string | null;
5
39
  from?: string;
@@ -11,11 +45,38 @@ interface Email {
11
45
  attachments?: any[];
12
46
  subject: string;
13
47
  }
14
- declare const emailTransporter: (logger?: any) => {
48
+ /**
49
+ * Create an email transporter.
50
+ *
51
+ * Accepts an optional {@link EmailServiceConfig} so callers can pass
52
+ * explicit configuration instead of relying on environment variables.
53
+ * Any value not provided in `config` falls back to the corresponding
54
+ * `process.env` variable, preserving full backward-compatibility.
55
+ */
56
+ declare const emailTransporter: (logger?: any, config?: EmailServiceConfig) => {
15
57
  sendMail({ to, reference, from, replyTo, template, text, attachments, idempotencyKey, subject, }: Email, idempotentCheck?: boolean): Promise<nodemailer_lib_smtp_pool.SentMessageInfo | undefined>;
16
58
  };
17
59
 
18
60
  declare function renderTemplate(content: string, data: Record<string, any>): string;
19
61
  declare function renderTemplateFile(file: string, data: Record<string, any>): Promise<string>;
20
62
 
21
- export { type Email, emailTransporter, renderTemplate, renderTemplateFile };
63
+ interface EmailLog {
64
+ reference?: string;
65
+ timestamp: number;
66
+ subject: string;
67
+ sender: string;
68
+ receiver: string;
69
+ idempotencyKey?: string;
70
+ status: 'success' | 'failed';
71
+ error?: string;
72
+ }
73
+ declare class EmailLogService {
74
+ private readonly tableName;
75
+ private readonly dynamoClient;
76
+ private readonly docClient;
77
+ constructor(loggingConfig?: EmailLoggingConfig);
78
+ logEmail({ subject, sender, receiver, status, error, idempotencyKey, reference, }: Omit<EmailLog, 'timestamp'>): Promise<string>;
79
+ getEmailByIdempotencyKey(idempotencyKey: string | undefined): Promise<EmailLog | null>;
80
+ }
81
+
82
+ export { type Email, EmailLogService, type EmailLoggingConfig, type EmailServiceConfig, type SESTransportConfig, type SMTPTransportConfig, emailTransporter, renderTemplate, renderTemplateFile };
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ EmailLogService: () => EmailLogService,
33
34
  emailTransporter: () => emailTransporter,
34
35
  renderTemplate: () => renderTemplate,
35
36
  renderTemplateFile: () => renderTemplateFile
@@ -45,11 +46,11 @@ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
45
46
  var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
46
47
  var import_uuid = require("uuid");
47
48
  var EmailLogService = class {
48
- constructor() {
49
- this.tableName = process.env.EMAIL_TABLE_NAME;
49
+ constructor(loggingConfig) {
50
+ this.tableName = loggingConfig?.tableName ?? process.env.EMAIL_TABLE_NAME;
50
51
  if (this.tableName) {
51
52
  this.dynamoClient = new import_client_dynamodb.DynamoDBClient({
52
- region: process.env.AWS_REGION
53
+ region: loggingConfig?.region ?? process.env.AWS_REGION
53
54
  });
54
55
  this.docClient = import_lib_dynamodb.DynamoDBDocumentClient.from(this.dynamoClient);
55
56
  }
@@ -105,33 +106,48 @@ var EmailLogService = class {
105
106
  };
106
107
 
107
108
  // src/email.ts
108
- var sesClient = new import_client_sesv2.SESv2Client({
109
- region: process.env.AWS_REGION
110
- });
111
- var emailOptions = process.env.EMAIL_TRANSPORT === "smtp" ? {
112
- port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
113
- host: process.env.SMTP_HOST,
114
- auth: process.env.SMTP_USER && process.env.SMTP_PASSWORD ? {
115
- user: process.env.SMTP_USER,
116
- pass: process.env.SMTP_PASSWORD
117
- } : void 0
118
- } : {
119
- SES: { sesClient, SendEmailCommand: import_client_sesv2.SendEmailCommand }
120
- };
121
- var emailTransporter = (logger = console) => {
109
+ function resolveTransportType(config) {
110
+ if (config?.transport) return config.transport;
111
+ if (process.env.EMAIL_TRANSPORT === "smtp") return "smtp";
112
+ return "ses";
113
+ }
114
+ function buildTransportOptions(type, config) {
115
+ if (type === "smtp") {
116
+ const smtp = config?.smtp;
117
+ return {
118
+ port: smtp?.port ?? (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587),
119
+ host: smtp?.host ?? process.env.SMTP_HOST,
120
+ auth: smtp?.auth ?? (process.env.SMTP_USER && process.env.SMTP_PASSWORD ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD } : void 0)
121
+ };
122
+ }
123
+ const ses = config?.ses;
124
+ const sesClient = new import_client_sesv2.SESv2Client({
125
+ region: ses?.region ?? process.env.AWS_REGION,
126
+ ...ses?.credentials ? { credentials: ses.credentials } : {}
127
+ });
128
+ return { SES: { sesClient, SendEmailCommand: import_client_sesv2.SendEmailCommand } };
129
+ }
130
+ function buildDefaultOptions(type, config) {
131
+ if (type === "ses") {
132
+ const sourceArn = config?.ses?.sourceArn ?? process.env.EMAIL_SOURCE_ARN;
133
+ if (sourceArn) {
134
+ return { ses: { FromEmailAddressIdentityArn: sourceArn } };
135
+ }
136
+ }
137
+ return void 0;
138
+ }
139
+ var emailTransporter = (logger = console, config) => {
140
+ const type = resolveTransportType(config);
141
+ const transportOptions = buildTransportOptions(type, config);
142
+ const defaults = buildDefaultOptions(type, config);
122
143
  const transporter = import_nodemailer.default.createTransport(
123
144
  {
124
- ...emailOptions,
125
- debug: true
145
+ ...transportOptions,
146
+ debug: config?.debug ?? true
126
147
  },
127
- {
128
- ...process.env.EMAIL_SOURCE_ARN ? {
129
- ses: {
130
- FromEmailAddressIdentityArn: process.env.EMAIL_SOURCE_ARN
131
- }
132
- } : void 0
133
- }
148
+ defaults ? defaults : void 0
134
149
  );
150
+ const emailLogService = new EmailLogService(config?.logging);
135
151
  return {
136
152
  async sendMail({
137
153
  to,
@@ -148,7 +164,6 @@ var emailTransporter = (logger = console) => {
148
164
  return;
149
165
  }
150
166
  const sender = from;
151
- const emailLogService = new EmailLogService();
152
167
  logger.info(
153
168
  `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`
154
169
  );
@@ -239,6 +254,7 @@ async function renderTemplateFile(file, data) {
239
254
  }
240
255
  // Annotate the CommonJS export names for ESM import in node:
241
256
  0 && (module.exports = {
257
+ EmailLogService,
242
258
  emailTransporter,
243
259
  renderTemplate,
244
260
  renderTemplateFile
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/email.ts","../src/email-log.ts","../src/template.ts"],"sourcesContent":["export * from './email';\nexport * from './template';\n","import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';\nimport nodemailer from 'nodemailer';\nimport { EmailLogService } from './email-log';\n\n/**\n * Template to use\n * await emailTransporter().sendMail({\n from: 'AusVie noreply<noreply@ausvie.com.au>',\n to: 'admin@ausvie.com.au',\n subject: 'Test', // Subject line\n text: 'Test text', // plaintext version\n html: `<div>${'Test text'}</div>`, // html version\n // attachments: [\n // {\n // filename,\n // content: fileData,\n // },\n // ],\n });\n */\n// Load the AWS SDK for Node.js\n// https://stackoverflow.com/questions/23042835/sending-mail-via-aws-ses-with-attachment-in-node-js\n\nconst sesClient = new SESv2Client({\n region: process.env.AWS_REGION,\n});\n\nconst emailOptions =\n process.env.EMAIL_TRANSPORT === 'smtp'\n ? {\n port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,\n host: process.env.SMTP_HOST,\n auth:\n process.env.SMTP_USER && process.env.SMTP_PASSWORD\n ? {\n user: process.env.SMTP_USER,\n pass: process.env.SMTP_PASSWORD,\n }\n : undefined,\n }\n : {\n SES: { sesClient, SendEmailCommand },\n };\n\nexport interface Email {\n to?: string | null;\n from?: string;\n reference?: string;\n template?: string;\n idempotencyKey?: string;\n replyTo?: string | null;\n text?: string;\n attachments?: any[];\n subject: string;\n}\n\nexport const emailTransporter = (logger: any = console) => {\n const transporter = nodemailer.createTransport(\n {\n ...emailOptions,\n debug: true,\n } as any,\n {\n ...(process.env.EMAIL_SOURCE_ARN\n ? {\n ses: {\n FromEmailAddressIdentityArn: process.env.EMAIL_SOURCE_ARN!,\n },\n }\n : undefined),\n } as any\n );\n\n return {\n async sendMail(\n {\n to,\n reference,\n from,\n replyTo,\n template,\n text,\n attachments = [],\n idempotencyKey,\n subject,\n }: Email,\n idempotentCheck?: boolean\n ) {\n if (!to) {\n return;\n }\n const sender = from;\n const emailLogService = new EmailLogService();\n logger.info(\n `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`\n );\n\n try {\n if (idempotentCheck) {\n const existingEmail =\n await emailLogService.getEmailByIdempotencyKey(idempotencyKey);\n if (existingEmail) {\n logger.info(`Email with idempotency key ${idempotencyKey} already sent`);\n return;\n }\n }\n const result = await transporter.sendMail({\n to,\n from: sender,\n replyTo: replyTo ?? undefined,\n html: template,\n text,\n subject,\n attachments,\n });\n\n // Log successful email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n reference,\n sender: sender || 'Unknown Sender',\n idempotencyKey,\n receiver: to,\n status: 'success',\n });\n\n return result;\n } catch (error) {\n // Log failed email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n sender: sender || 'Unknown Sender',\n receiver: to,\n status: 'failed',\n error: error instanceof Error ? error.message : String(error),\n });\n\n throw error;\n }\n },\n };\n};\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';\nimport { v4 as uuidv4 } from 'uuid';\n\ninterface EmailLog {\n reference?: string;\n timestamp: number;\n subject: string;\n sender: string;\n receiver: string;\n idempotencyKey?: string;\n status: 'success' | 'failed';\n error?: string;\n}\n\nexport class EmailLogService {\n private readonly tableName: string;\n private readonly dynamoClient: DynamoDBClient | undefined;\n private readonly docClient: DynamoDBDocumentClient | undefined;\n\n constructor() {\n this.tableName = process.env.EMAIL_TABLE_NAME!;\n if (this.tableName) {\n this.dynamoClient = new DynamoDBClient({\n region: process.env.AWS_REGION,\n });\n this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);\n }\n }\n\n async logEmail({\n subject,\n sender,\n receiver,\n status,\n error,\n idempotencyKey,\n reference = uuidv4(),\n }: Omit<EmailLog, 'timestamp'>): Promise<string> {\n if (!this.docClient) {\n return reference;\n }\n const timestamp = Date.now();\n\n const log: EmailLog = {\n reference,\n timestamp,\n subject,\n sender,\n receiver,\n status,\n idempotencyKey,\n ...(error && { error }),\n };\n\n await this.docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: log,\n })\n );\n return reference;\n }\n\n async getEmailByIdempotencyKey(idempotencyKey: string | undefined) {\n if (!idempotencyKey || !this.docClient) {\n return null;\n }\n\n const result = await this.docClient.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'idempotency-key-index',\n KeyConditionExpression: 'idempotencyKey = :idempotencyKey',\n ExpressionAttributeValues: {\n ':idempotencyKey': idempotencyKey,\n },\n Limit: 1,\n })\n );\n return result.Items?.[0] as EmailLog | null;\n }\n}\n","import fs from 'node:fs';\nimport { formatters, dates } from '@xcelsior/utils';\nimport handlebars from 'handlebars';\nimport _ from 'lodash';\nimport moment from 'moment';\n\nconst { getDateTime } = dates;\nconst { convertMoneyFormat, moneyFormat } = formatters;\n\nhandlebars.registerHelper('multiply', (a, b) => a * b);\n\nhandlebars.registerHelper('datetime', (a) => moment(getDateTime(a)).format('DD/MM/YYYY HH:mm'));\n\nhandlebars.registerHelper('moneyFormat', (a, ...others) => {\n let currency = 'AUD' as const;\n if (others.length > 1) {\n currency = others[0] as any;\n }\n return moneyFormat(a, currency);\n});\n\nhandlebars.registerHelper('convertMoneyFormat', (amount, currency, rate = 1) =>\n convertMoneyFormat(amount, currency, rate)\n);\n\nhandlebars.registerHelper('titleCase', (string) => {\n if (!string) return '';\n return _.startCase(_.camelCase(string)).replace(/\\s/g, ' ');\n});\n\nhandlebars.registerHelper('choose', (a, b) => (a ? a : b));\n\nhandlebars.registerHelper(\n 'phoneFormat',\n (phone) =>\n `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`\n);\n\nhandlebars.registerHelper('title', (title) => _.startCase(title));\n\nhandlebars.registerHelper('eq', (a, b) => a === b);\nhandlebars.registerHelper('lowercase', (title: string) => title.toLowerCase());\n\nhandlebars.registerHelper('JSONparse', (string, key) => JSON.parse(string)[key]);\n\nhandlebars.registerHelper('incr', (a) => a + 1);\n\nhandlebars.registerHelper('gt', (a, b) => a > b);\n\nexport function renderTemplate(content: string, data: Record<string, any>): string {\n return handlebars.compile(content)(data);\n}\n\nexport async function renderTemplateFile(file: string, data: Record<string, any>) {\n const content = await fs.promises.readFile(file, {\n encoding: 'utf-8',\n });\n return renderTemplate(content, data);\n}\n"],"mappings":"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","names":["uuidv4","nodemailer","handlebars","moment","_","fs"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/email.ts","../src/email-log.ts","../src/template.ts"],"sourcesContent":["export * from './email';\nexport * from './template';\nexport { EmailLogService } from './email-log';\n","import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';\nimport nodemailer from 'nodemailer';\nimport { EmailLogService } from './email-log';\n\n// ─── Configuration types ───────────────────────────────────────────\n\nexport interface SMTPTransportConfig {\n host: string;\n port?: number;\n auth?: {\n user: string;\n pass: string;\n };\n}\n\nexport interface SESTransportConfig {\n region?: string;\n sourceArn?: string;\n credentials?: {\n accessKeyId: string;\n secretAccessKey: string;\n };\n}\n\nexport interface EmailLoggingConfig {\n /** DynamoDB table name for email logs */\n tableName: string;\n /** AWS region for the logging table */\n region?: string;\n}\n\nexport interface EmailServiceConfig {\n /** Transport type – defaults to 'ses' */\n transport?: 'smtp' | 'ses';\n /** SMTP options (required when transport is 'smtp') */\n smtp?: SMTPTransportConfig;\n /** SES options (used when transport is 'ses') */\n ses?: SESTransportConfig;\n /** Email logging configuration – if omitted, logging is derived from env vars */\n logging?: EmailLoggingConfig;\n /** Enable nodemailer debug output */\n debug?: boolean;\n}\n\n// ─── Email payload ─────────────────────────────────────────────────\n\nexport interface Email {\n to?: string | null;\n from?: string;\n reference?: string;\n template?: string;\n idempotencyKey?: string;\n replyTo?: string | null;\n text?: string;\n attachments?: any[];\n subject: string;\n}\n\n// ─── Internal helpers ──────────────────────────────────────────────\n\nfunction resolveTransportType(config?: EmailServiceConfig): 'smtp' | 'ses' {\n if (config?.transport) return config.transport;\n if (process.env.EMAIL_TRANSPORT === 'smtp') return 'smtp';\n return 'ses';\n}\n\nfunction buildTransportOptions(type: 'smtp' | 'ses', config?: EmailServiceConfig) {\n if (type === 'smtp') {\n const smtp = config?.smtp;\n return {\n port: smtp?.port ?? (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587),\n host: smtp?.host ?? process.env.SMTP_HOST,\n auth:\n smtp?.auth ??\n (process.env.SMTP_USER && process.env.SMTP_PASSWORD\n ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD }\n : undefined),\n };\n }\n\n const ses = config?.ses;\n const sesClient = new SESv2Client({\n region: ses?.region ?? process.env.AWS_REGION,\n ...(ses?.credentials ? { credentials: ses.credentials } : {}),\n });\n return { SES: { sesClient, SendEmailCommand } };\n}\n\nfunction buildDefaultOptions(type: 'smtp' | 'ses', config?: EmailServiceConfig) {\n if (type === 'ses') {\n const sourceArn = config?.ses?.sourceArn ?? process.env.EMAIL_SOURCE_ARN;\n if (sourceArn) {\n return { ses: { FromEmailAddressIdentityArn: sourceArn } };\n }\n }\n return undefined;\n}\n\n// ─── Public API ────────────────────────────────────────────────────\n\n/**\n * Create an email transporter.\n *\n * Accepts an optional {@link EmailServiceConfig} so callers can pass\n * explicit configuration instead of relying on environment variables.\n * Any value not provided in `config` falls back to the corresponding\n * `process.env` variable, preserving full backward-compatibility.\n */\nexport const emailTransporter = (logger: any = console, config?: EmailServiceConfig) => {\n const type = resolveTransportType(config);\n const transportOptions = buildTransportOptions(type, config);\n const defaults = buildDefaultOptions(type, config);\n\n const transporter = nodemailer.createTransport(\n {\n ...transportOptions,\n debug: config?.debug ?? true,\n } as any,\n defaults ? (defaults as any) : undefined\n );\n\n const emailLogService = new EmailLogService(config?.logging);\n\n return {\n async sendMail(\n {\n to,\n reference,\n from,\n replyTo,\n template,\n text,\n attachments = [],\n idempotencyKey,\n subject,\n }: Email,\n idempotentCheck?: boolean\n ) {\n if (!to) {\n return;\n }\n const sender = from;\n logger.info(\n `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`\n );\n\n try {\n if (idempotentCheck) {\n const existingEmail =\n await emailLogService.getEmailByIdempotencyKey(idempotencyKey);\n if (existingEmail) {\n logger.info(`Email with idempotency key ${idempotencyKey} already sent`);\n return;\n }\n }\n const result = await transporter.sendMail({\n to,\n from: sender,\n replyTo: replyTo ?? undefined,\n html: template,\n text,\n subject,\n attachments,\n });\n\n // Log successful email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n reference,\n sender: sender || 'Unknown Sender',\n idempotencyKey,\n receiver: to,\n status: 'success',\n });\n\n return result;\n } catch (error) {\n // Log failed email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n sender: sender || 'Unknown Sender',\n receiver: to,\n status: 'failed',\n error: error instanceof Error ? error.message : String(error),\n });\n\n throw error;\n }\n },\n };\n};\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';\nimport { v4 as uuidv4 } from 'uuid';\nimport type { EmailLoggingConfig } from './email';\n\ninterface EmailLog {\n reference?: string;\n timestamp: number;\n subject: string;\n sender: string;\n receiver: string;\n idempotencyKey?: string;\n status: 'success' | 'failed';\n error?: string;\n}\n\nexport class EmailLogService {\n private readonly tableName: string;\n private readonly dynamoClient: DynamoDBClient | undefined;\n private readonly docClient: DynamoDBDocumentClient | undefined;\n\n constructor(loggingConfig?: EmailLoggingConfig) {\n this.tableName = loggingConfig?.tableName ?? process.env.EMAIL_TABLE_NAME!;\n if (this.tableName) {\n this.dynamoClient = new DynamoDBClient({\n region: loggingConfig?.region ?? process.env.AWS_REGION,\n });\n this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);\n }\n }\n\n async logEmail({\n subject,\n sender,\n receiver,\n status,\n error,\n idempotencyKey,\n reference = uuidv4(),\n }: Omit<EmailLog, 'timestamp'>): Promise<string> {\n if (!this.docClient) {\n return reference;\n }\n const timestamp = Date.now();\n\n const log: EmailLog = {\n reference,\n timestamp,\n subject,\n sender,\n receiver,\n status,\n idempotencyKey,\n ...(error && { error }),\n };\n\n await this.docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: log,\n })\n );\n return reference;\n }\n\n async getEmailByIdempotencyKey(idempotencyKey: string | undefined) {\n if (!idempotencyKey || !this.docClient) {\n return null;\n }\n\n const result = await this.docClient.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'idempotency-key-index',\n KeyConditionExpression: 'idempotencyKey = :idempotencyKey',\n ExpressionAttributeValues: {\n ':idempotencyKey': idempotencyKey,\n },\n Limit: 1,\n })\n );\n return result.Items?.[0] as EmailLog | null;\n }\n}\n","import fs from 'node:fs';\nimport { formatters, dates } from '@xcelsior/utils';\nimport handlebars from 'handlebars';\nimport _ from 'lodash';\nimport moment from 'moment';\n\nconst { getDateTime } = dates;\nconst { convertMoneyFormat, moneyFormat } = formatters;\n\nhandlebars.registerHelper('multiply', (a, b) => a * b);\n\nhandlebars.registerHelper('datetime', (a) => moment(getDateTime(a)).format('DD/MM/YYYY HH:mm'));\n\nhandlebars.registerHelper('moneyFormat', (a, ...others) => {\n let currency = 'AUD' as const;\n if (others.length > 1) {\n currency = others[0] as any;\n }\n return moneyFormat(a, currency);\n});\n\nhandlebars.registerHelper('convertMoneyFormat', (amount, currency, rate = 1) =>\n convertMoneyFormat(amount, currency, rate)\n);\n\nhandlebars.registerHelper('titleCase', (string) => {\n if (!string) return '';\n return _.startCase(_.camelCase(string)).replace(/\\s/g, ' ');\n});\n\nhandlebars.registerHelper('choose', (a, b) => (a ? a : b));\n\nhandlebars.registerHelper(\n 'phoneFormat',\n (phone) =>\n `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`\n);\n\nhandlebars.registerHelper('title', (title) => _.startCase(title));\n\nhandlebars.registerHelper('eq', (a, b) => a === b);\nhandlebars.registerHelper('lowercase', (title: string) => title.toLowerCase());\n\nhandlebars.registerHelper('JSONparse', (string, key) => JSON.parse(string)[key]);\n\nhandlebars.registerHelper('incr', (a) => a + 1);\n\nhandlebars.registerHelper('gt', (a, b) => a > b);\n\nexport function renderTemplate(content: string, data: Record<string, any>): string {\n return handlebars.compile(content)(data);\n}\n\nexport async function renderTemplateFile(file: string, data: Record<string, any>) {\n const content = await fs.promises.readFile(file, {\n encoding: 'utf-8',\n });\n return renderTemplate(content, data);\n}\n"],"mappings":"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","names":["uuidv4","nodemailer","handlebars","moment","_","fs"]}
package/dist/index.mjs CHANGED
@@ -7,11 +7,11 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
7
7
  import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
8
8
  import { v4 as uuidv4 } from "uuid";
9
9
  var EmailLogService = class {
10
- constructor() {
11
- this.tableName = process.env.EMAIL_TABLE_NAME;
10
+ constructor(loggingConfig) {
11
+ this.tableName = loggingConfig?.tableName ?? process.env.EMAIL_TABLE_NAME;
12
12
  if (this.tableName) {
13
13
  this.dynamoClient = new DynamoDBClient({
14
- region: process.env.AWS_REGION
14
+ region: loggingConfig?.region ?? process.env.AWS_REGION
15
15
  });
16
16
  this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);
17
17
  }
@@ -67,33 +67,48 @@ var EmailLogService = class {
67
67
  };
68
68
 
69
69
  // src/email.ts
70
- var sesClient = new SESv2Client({
71
- region: process.env.AWS_REGION
72
- });
73
- var emailOptions = process.env.EMAIL_TRANSPORT === "smtp" ? {
74
- port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
75
- host: process.env.SMTP_HOST,
76
- auth: process.env.SMTP_USER && process.env.SMTP_PASSWORD ? {
77
- user: process.env.SMTP_USER,
78
- pass: process.env.SMTP_PASSWORD
79
- } : void 0
80
- } : {
81
- SES: { sesClient, SendEmailCommand }
82
- };
83
- var emailTransporter = (logger = console) => {
70
+ function resolveTransportType(config) {
71
+ if (config?.transport) return config.transport;
72
+ if (process.env.EMAIL_TRANSPORT === "smtp") return "smtp";
73
+ return "ses";
74
+ }
75
+ function buildTransportOptions(type, config) {
76
+ if (type === "smtp") {
77
+ const smtp = config?.smtp;
78
+ return {
79
+ port: smtp?.port ?? (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587),
80
+ host: smtp?.host ?? process.env.SMTP_HOST,
81
+ auth: smtp?.auth ?? (process.env.SMTP_USER && process.env.SMTP_PASSWORD ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD } : void 0)
82
+ };
83
+ }
84
+ const ses = config?.ses;
85
+ const sesClient = new SESv2Client({
86
+ region: ses?.region ?? process.env.AWS_REGION,
87
+ ...ses?.credentials ? { credentials: ses.credentials } : {}
88
+ });
89
+ return { SES: { sesClient, SendEmailCommand } };
90
+ }
91
+ function buildDefaultOptions(type, config) {
92
+ if (type === "ses") {
93
+ const sourceArn = config?.ses?.sourceArn ?? process.env.EMAIL_SOURCE_ARN;
94
+ if (sourceArn) {
95
+ return { ses: { FromEmailAddressIdentityArn: sourceArn } };
96
+ }
97
+ }
98
+ return void 0;
99
+ }
100
+ var emailTransporter = (logger = console, config) => {
101
+ const type = resolveTransportType(config);
102
+ const transportOptions = buildTransportOptions(type, config);
103
+ const defaults = buildDefaultOptions(type, config);
84
104
  const transporter = nodemailer.createTransport(
85
105
  {
86
- ...emailOptions,
87
- debug: true
106
+ ...transportOptions,
107
+ debug: config?.debug ?? true
88
108
  },
89
- {
90
- ...process.env.EMAIL_SOURCE_ARN ? {
91
- ses: {
92
- FromEmailAddressIdentityArn: process.env.EMAIL_SOURCE_ARN
93
- }
94
- } : void 0
95
- }
109
+ defaults ? defaults : void 0
96
110
  );
111
+ const emailLogService = new EmailLogService(config?.logging);
97
112
  return {
98
113
  async sendMail({
99
114
  to,
@@ -110,7 +125,6 @@ var emailTransporter = (logger = console) => {
110
125
  return;
111
126
  }
112
127
  const sender = from;
113
- const emailLogService = new EmailLogService();
114
128
  logger.info(
115
129
  `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`
116
130
  );
@@ -200,6 +214,7 @@ async function renderTemplateFile(file, data) {
200
214
  return renderTemplate(content, data);
201
215
  }
202
216
  export {
217
+ EmailLogService,
203
218
  emailTransporter,
204
219
  renderTemplate,
205
220
  renderTemplateFile
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/email.ts","../src/email-log.ts","../src/template.ts"],"sourcesContent":["import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';\nimport nodemailer from 'nodemailer';\nimport { EmailLogService } from './email-log';\n\n/**\n * Template to use\n * await emailTransporter().sendMail({\n from: 'AusVie noreply<noreply@ausvie.com.au>',\n to: 'admin@ausvie.com.au',\n subject: 'Test', // Subject line\n text: 'Test text', // plaintext version\n html: `<div>${'Test text'}</div>`, // html version\n // attachments: [\n // {\n // filename,\n // content: fileData,\n // },\n // ],\n });\n */\n// Load the AWS SDK for Node.js\n// https://stackoverflow.com/questions/23042835/sending-mail-via-aws-ses-with-attachment-in-node-js\n\nconst sesClient = new SESv2Client({\n region: process.env.AWS_REGION,\n});\n\nconst emailOptions =\n process.env.EMAIL_TRANSPORT === 'smtp'\n ? {\n port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,\n host: process.env.SMTP_HOST,\n auth:\n process.env.SMTP_USER && process.env.SMTP_PASSWORD\n ? {\n user: process.env.SMTP_USER,\n pass: process.env.SMTP_PASSWORD,\n }\n : undefined,\n }\n : {\n SES: { sesClient, SendEmailCommand },\n };\n\nexport interface Email {\n to?: string | null;\n from?: string;\n reference?: string;\n template?: string;\n idempotencyKey?: string;\n replyTo?: string | null;\n text?: string;\n attachments?: any[];\n subject: string;\n}\n\nexport const emailTransporter = (logger: any = console) => {\n const transporter = nodemailer.createTransport(\n {\n ...emailOptions,\n debug: true,\n } as any,\n {\n ...(process.env.EMAIL_SOURCE_ARN\n ? {\n ses: {\n FromEmailAddressIdentityArn: process.env.EMAIL_SOURCE_ARN!,\n },\n }\n : undefined),\n } as any\n );\n\n return {\n async sendMail(\n {\n to,\n reference,\n from,\n replyTo,\n template,\n text,\n attachments = [],\n idempotencyKey,\n subject,\n }: Email,\n idempotentCheck?: boolean\n ) {\n if (!to) {\n return;\n }\n const sender = from;\n const emailLogService = new EmailLogService();\n logger.info(\n `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`\n );\n\n try {\n if (idempotentCheck) {\n const existingEmail =\n await emailLogService.getEmailByIdempotencyKey(idempotencyKey);\n if (existingEmail) {\n logger.info(`Email with idempotency key ${idempotencyKey} already sent`);\n return;\n }\n }\n const result = await transporter.sendMail({\n to,\n from: sender,\n replyTo: replyTo ?? undefined,\n html: template,\n text,\n subject,\n attachments,\n });\n\n // Log successful email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n reference,\n sender: sender || 'Unknown Sender',\n idempotencyKey,\n receiver: to,\n status: 'success',\n });\n\n return result;\n } catch (error) {\n // Log failed email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n sender: sender || 'Unknown Sender',\n receiver: to,\n status: 'failed',\n error: error instanceof Error ? error.message : String(error),\n });\n\n throw error;\n }\n },\n };\n};\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';\nimport { v4 as uuidv4 } from 'uuid';\n\ninterface EmailLog {\n reference?: string;\n timestamp: number;\n subject: string;\n sender: string;\n receiver: string;\n idempotencyKey?: string;\n status: 'success' | 'failed';\n error?: string;\n}\n\nexport class EmailLogService {\n private readonly tableName: string;\n private readonly dynamoClient: DynamoDBClient | undefined;\n private readonly docClient: DynamoDBDocumentClient | undefined;\n\n constructor() {\n this.tableName = process.env.EMAIL_TABLE_NAME!;\n if (this.tableName) {\n this.dynamoClient = new DynamoDBClient({\n region: process.env.AWS_REGION,\n });\n this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);\n }\n }\n\n async logEmail({\n subject,\n sender,\n receiver,\n status,\n error,\n idempotencyKey,\n reference = uuidv4(),\n }: Omit<EmailLog, 'timestamp'>): Promise<string> {\n if (!this.docClient) {\n return reference;\n }\n const timestamp = Date.now();\n\n const log: EmailLog = {\n reference,\n timestamp,\n subject,\n sender,\n receiver,\n status,\n idempotencyKey,\n ...(error && { error }),\n };\n\n await this.docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: log,\n })\n );\n return reference;\n }\n\n async getEmailByIdempotencyKey(idempotencyKey: string | undefined) {\n if (!idempotencyKey || !this.docClient) {\n return null;\n }\n\n const result = await this.docClient.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'idempotency-key-index',\n KeyConditionExpression: 'idempotencyKey = :idempotencyKey',\n ExpressionAttributeValues: {\n ':idempotencyKey': idempotencyKey,\n },\n Limit: 1,\n })\n );\n return result.Items?.[0] as EmailLog | null;\n }\n}\n","import fs from 'node:fs';\nimport { formatters, dates } from '@xcelsior/utils';\nimport handlebars from 'handlebars';\nimport _ from 'lodash';\nimport moment from 'moment';\n\nconst { getDateTime } = dates;\nconst { convertMoneyFormat, moneyFormat } = formatters;\n\nhandlebars.registerHelper('multiply', (a, b) => a * b);\n\nhandlebars.registerHelper('datetime', (a) => moment(getDateTime(a)).format('DD/MM/YYYY HH:mm'));\n\nhandlebars.registerHelper('moneyFormat', (a, ...others) => {\n let currency = 'AUD' as const;\n if (others.length > 1) {\n currency = others[0] as any;\n }\n return moneyFormat(a, currency);\n});\n\nhandlebars.registerHelper('convertMoneyFormat', (amount, currency, rate = 1) =>\n convertMoneyFormat(amount, currency, rate)\n);\n\nhandlebars.registerHelper('titleCase', (string) => {\n if (!string) return '';\n return _.startCase(_.camelCase(string)).replace(/\\s/g, ' ');\n});\n\nhandlebars.registerHelper('choose', (a, b) => (a ? a : b));\n\nhandlebars.registerHelper(\n 'phoneFormat',\n (phone) =>\n `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`\n);\n\nhandlebars.registerHelper('title', (title) => _.startCase(title));\n\nhandlebars.registerHelper('eq', (a, b) => a === b);\nhandlebars.registerHelper('lowercase', (title: string) => title.toLowerCase());\n\nhandlebars.registerHelper('JSONparse', (string, key) => JSON.parse(string)[key]);\n\nhandlebars.registerHelper('incr', (a) => a + 1);\n\nhandlebars.registerHelper('gt', (a, b) => a > b);\n\nexport function renderTemplate(content: string, data: Record<string, any>): string {\n return handlebars.compile(content)(data);\n}\n\nexport async function renderTemplateFile(file: string, data: Record<string, any>) {\n const content = await fs.promises.readFile(file, {\n encoding: 'utf-8',\n });\n return renderTemplate(content, data);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/email.ts","../src/email-log.ts","../src/template.ts"],"sourcesContent":["import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';\nimport nodemailer from 'nodemailer';\nimport { EmailLogService } from './email-log';\n\n// ─── Configuration types ───────────────────────────────────────────\n\nexport interface SMTPTransportConfig {\n host: string;\n port?: number;\n auth?: {\n user: string;\n pass: string;\n };\n}\n\nexport interface SESTransportConfig {\n region?: string;\n sourceArn?: string;\n credentials?: {\n accessKeyId: string;\n secretAccessKey: string;\n };\n}\n\nexport interface EmailLoggingConfig {\n /** DynamoDB table name for email logs */\n tableName: string;\n /** AWS region for the logging table */\n region?: string;\n}\n\nexport interface EmailServiceConfig {\n /** Transport type – defaults to 'ses' */\n transport?: 'smtp' | 'ses';\n /** SMTP options (required when transport is 'smtp') */\n smtp?: SMTPTransportConfig;\n /** SES options (used when transport is 'ses') */\n ses?: SESTransportConfig;\n /** Email logging configuration – if omitted, logging is derived from env vars */\n logging?: EmailLoggingConfig;\n /** Enable nodemailer debug output */\n debug?: boolean;\n}\n\n// ─── Email payload ─────────────────────────────────────────────────\n\nexport interface Email {\n to?: string | null;\n from?: string;\n reference?: string;\n template?: string;\n idempotencyKey?: string;\n replyTo?: string | null;\n text?: string;\n attachments?: any[];\n subject: string;\n}\n\n// ─── Internal helpers ──────────────────────────────────────────────\n\nfunction resolveTransportType(config?: EmailServiceConfig): 'smtp' | 'ses' {\n if (config?.transport) return config.transport;\n if (process.env.EMAIL_TRANSPORT === 'smtp') return 'smtp';\n return 'ses';\n}\n\nfunction buildTransportOptions(type: 'smtp' | 'ses', config?: EmailServiceConfig) {\n if (type === 'smtp') {\n const smtp = config?.smtp;\n return {\n port: smtp?.port ?? (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587),\n host: smtp?.host ?? process.env.SMTP_HOST,\n auth:\n smtp?.auth ??\n (process.env.SMTP_USER && process.env.SMTP_PASSWORD\n ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD }\n : undefined),\n };\n }\n\n const ses = config?.ses;\n const sesClient = new SESv2Client({\n region: ses?.region ?? process.env.AWS_REGION,\n ...(ses?.credentials ? { credentials: ses.credentials } : {}),\n });\n return { SES: { sesClient, SendEmailCommand } };\n}\n\nfunction buildDefaultOptions(type: 'smtp' | 'ses', config?: EmailServiceConfig) {\n if (type === 'ses') {\n const sourceArn = config?.ses?.sourceArn ?? process.env.EMAIL_SOURCE_ARN;\n if (sourceArn) {\n return { ses: { FromEmailAddressIdentityArn: sourceArn } };\n }\n }\n return undefined;\n}\n\n// ─── Public API ────────────────────────────────────────────────────\n\n/**\n * Create an email transporter.\n *\n * Accepts an optional {@link EmailServiceConfig} so callers can pass\n * explicit configuration instead of relying on environment variables.\n * Any value not provided in `config` falls back to the corresponding\n * `process.env` variable, preserving full backward-compatibility.\n */\nexport const emailTransporter = (logger: any = console, config?: EmailServiceConfig) => {\n const type = resolveTransportType(config);\n const transportOptions = buildTransportOptions(type, config);\n const defaults = buildDefaultOptions(type, config);\n\n const transporter = nodemailer.createTransport(\n {\n ...transportOptions,\n debug: config?.debug ?? true,\n } as any,\n defaults ? (defaults as any) : undefined\n );\n\n const emailLogService = new EmailLogService(config?.logging);\n\n return {\n async sendMail(\n {\n to,\n reference,\n from,\n replyTo,\n template,\n text,\n attachments = [],\n idempotencyKey,\n subject,\n }: Email,\n idempotentCheck?: boolean\n ) {\n if (!to) {\n return;\n }\n const sender = from;\n logger.info(\n `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`\n );\n\n try {\n if (idempotentCheck) {\n const existingEmail =\n await emailLogService.getEmailByIdempotencyKey(idempotencyKey);\n if (existingEmail) {\n logger.info(`Email with idempotency key ${idempotencyKey} already sent`);\n return;\n }\n }\n const result = await transporter.sendMail({\n to,\n from: sender,\n replyTo: replyTo ?? undefined,\n html: template,\n text,\n subject,\n attachments,\n });\n\n // Log successful email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n reference,\n sender: sender || 'Unknown Sender',\n idempotencyKey,\n receiver: to,\n status: 'success',\n });\n\n return result;\n } catch (error) {\n // Log failed email\n await emailLogService.logEmail({\n subject: subject || 'No Subject',\n sender: sender || 'Unknown Sender',\n receiver: to,\n status: 'failed',\n error: error instanceof Error ? error.message : String(error),\n });\n\n throw error;\n }\n },\n };\n};\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';\nimport { v4 as uuidv4 } from 'uuid';\nimport type { EmailLoggingConfig } from './email';\n\ninterface EmailLog {\n reference?: string;\n timestamp: number;\n subject: string;\n sender: string;\n receiver: string;\n idempotencyKey?: string;\n status: 'success' | 'failed';\n error?: string;\n}\n\nexport class EmailLogService {\n private readonly tableName: string;\n private readonly dynamoClient: DynamoDBClient | undefined;\n private readonly docClient: DynamoDBDocumentClient | undefined;\n\n constructor(loggingConfig?: EmailLoggingConfig) {\n this.tableName = loggingConfig?.tableName ?? process.env.EMAIL_TABLE_NAME!;\n if (this.tableName) {\n this.dynamoClient = new DynamoDBClient({\n region: loggingConfig?.region ?? process.env.AWS_REGION,\n });\n this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);\n }\n }\n\n async logEmail({\n subject,\n sender,\n receiver,\n status,\n error,\n idempotencyKey,\n reference = uuidv4(),\n }: Omit<EmailLog, 'timestamp'>): Promise<string> {\n if (!this.docClient) {\n return reference;\n }\n const timestamp = Date.now();\n\n const log: EmailLog = {\n reference,\n timestamp,\n subject,\n sender,\n receiver,\n status,\n idempotencyKey,\n ...(error && { error }),\n };\n\n await this.docClient.send(\n new PutCommand({\n TableName: this.tableName,\n Item: log,\n })\n );\n return reference;\n }\n\n async getEmailByIdempotencyKey(idempotencyKey: string | undefined) {\n if (!idempotencyKey || !this.docClient) {\n return null;\n }\n\n const result = await this.docClient.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: 'idempotency-key-index',\n KeyConditionExpression: 'idempotencyKey = :idempotencyKey',\n ExpressionAttributeValues: {\n ':idempotencyKey': idempotencyKey,\n },\n Limit: 1,\n })\n );\n return result.Items?.[0] as EmailLog | null;\n }\n}\n","import fs from 'node:fs';\nimport { formatters, dates } from '@xcelsior/utils';\nimport handlebars from 'handlebars';\nimport _ from 'lodash';\nimport moment from 'moment';\n\nconst { getDateTime } = dates;\nconst { convertMoneyFormat, moneyFormat } = formatters;\n\nhandlebars.registerHelper('multiply', (a, b) => a * b);\n\nhandlebars.registerHelper('datetime', (a) => moment(getDateTime(a)).format('DD/MM/YYYY HH:mm'));\n\nhandlebars.registerHelper('moneyFormat', (a, ...others) => {\n let currency = 'AUD' as const;\n if (others.length > 1) {\n currency = others[0] as any;\n }\n return moneyFormat(a, currency);\n});\n\nhandlebars.registerHelper('convertMoneyFormat', (amount, currency, rate = 1) =>\n convertMoneyFormat(amount, currency, rate)\n);\n\nhandlebars.registerHelper('titleCase', (string) => {\n if (!string) return '';\n return _.startCase(_.camelCase(string)).replace(/\\s/g, ' ');\n});\n\nhandlebars.registerHelper('choose', (a, b) => (a ? a : b));\n\nhandlebars.registerHelper(\n 'phoneFormat',\n (phone) =>\n `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`\n);\n\nhandlebars.registerHelper('title', (title) => _.startCase(title));\n\nhandlebars.registerHelper('eq', (a, b) => a === b);\nhandlebars.registerHelper('lowercase', (title: string) => title.toLowerCase());\n\nhandlebars.registerHelper('JSONparse', (string, key) => JSON.parse(string)[key]);\n\nhandlebars.registerHelper('incr', (a) => a + 1);\n\nhandlebars.registerHelper('gt', (a, b) => a > b);\n\nexport function renderTemplate(content: string, data: Record<string, any>): string {\n return handlebars.compile(content)(data);\n}\n\nexport async function renderTemplateFile(file: string, data: Record<string, any>) {\n const content = await fs.promises.readFile(file, {\n encoding: 'utf-8',\n });\n return renderTemplate(content, data);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/email",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "exports": {
5
5
  ".": {
6
6
  "import": "./src/index.ts",
@@ -23,7 +23,7 @@
23
23
  "nodemailer": "^7.0.4",
24
24
  "@aws-sdk/client-sesv2": "^3.888.0",
25
25
  "uuid": "^11.1.0",
26
- "@xcelsior/utils": "1.0.0"
26
+ "@xcelsior/utils": "1.0.1"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/lodash": "^4.17.20",
package/src/email-log.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
2
  import { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
+ import type { EmailLoggingConfig } from './email';
4
5
 
5
6
  interface EmailLog {
6
7
  reference?: string;
@@ -18,11 +19,11 @@ export class EmailLogService {
18
19
  private readonly dynamoClient: DynamoDBClient | undefined;
19
20
  private readonly docClient: DynamoDBDocumentClient | undefined;
20
21
 
21
- constructor() {
22
- this.tableName = process.env.EMAIL_TABLE_NAME!;
22
+ constructor(loggingConfig?: EmailLoggingConfig) {
23
+ this.tableName = loggingConfig?.tableName ?? process.env.EMAIL_TABLE_NAME!;
23
24
  if (this.tableName) {
24
25
  this.dynamoClient = new DynamoDBClient({
25
- region: process.env.AWS_REGION,
26
+ region: loggingConfig?.region ?? process.env.AWS_REGION,
26
27
  });
27
28
  this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);
28
29
  }
package/src/email.ts CHANGED
@@ -2,45 +2,47 @@ import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';
2
2
  import nodemailer from 'nodemailer';
3
3
  import { EmailLogService } from './email-log';
4
4
 
5
- /**
6
- * Template to use
7
- * await emailTransporter().sendMail({
8
- from: 'AusVie noreply<noreply@ausvie.com.au>',
9
- to: 'admin@ausvie.com.au',
10
- subject: 'Test', // Subject line
11
- text: 'Test text', // plaintext version
12
- html: `<div>${'Test text'}</div>`, // html version
13
- // attachments: [
14
- // {
15
- // filename,
16
- // content: fileData,
17
- // },
18
- // ],
19
- });
20
- */
21
- // Load the AWS SDK for Node.js
22
- // https://stackoverflow.com/questions/23042835/sending-mail-via-aws-ses-with-attachment-in-node-js
23
-
24
- const sesClient = new SESv2Client({
25
- region: process.env.AWS_REGION,
26
- });
27
-
28
- const emailOptions =
29
- process.env.EMAIL_TRANSPORT === 'smtp'
30
- ? {
31
- port: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
32
- host: process.env.SMTP_HOST,
33
- auth:
34
- process.env.SMTP_USER && process.env.SMTP_PASSWORD
35
- ? {
36
- user: process.env.SMTP_USER,
37
- pass: process.env.SMTP_PASSWORD,
38
- }
39
- : undefined,
40
- }
41
- : {
42
- SES: { sesClient, SendEmailCommand },
43
- };
5
+ // ─── Configuration types ───────────────────────────────────────────
6
+
7
+ export interface SMTPTransportConfig {
8
+ host: string;
9
+ port?: number;
10
+ auth?: {
11
+ user: string;
12
+ pass: string;
13
+ };
14
+ }
15
+
16
+ export interface SESTransportConfig {
17
+ region?: string;
18
+ sourceArn?: string;
19
+ credentials?: {
20
+ accessKeyId: string;
21
+ secretAccessKey: string;
22
+ };
23
+ }
24
+
25
+ export interface EmailLoggingConfig {
26
+ /** DynamoDB table name for email logs */
27
+ tableName: string;
28
+ /** AWS region for the logging table */
29
+ region?: string;
30
+ }
31
+
32
+ export interface EmailServiceConfig {
33
+ /** Transport type – defaults to 'ses' */
34
+ transport?: 'smtp' | 'ses';
35
+ /** SMTP options (required when transport is 'smtp') */
36
+ smtp?: SMTPTransportConfig;
37
+ /** SES options (used when transport is 'ses') */
38
+ ses?: SESTransportConfig;
39
+ /** Email logging configuration – if omitted, logging is derived from env vars */
40
+ logging?: EmailLoggingConfig;
41
+ /** Enable nodemailer debug output */
42
+ debug?: boolean;
43
+ }
44
+
45
+ // ─── Email payload ─────────────────────────────────────────────────
44
46
 
45
47
  export interface Email {
46
48
  to?: string | null;
@@ -54,23 +56,71 @@ export interface Email {
54
56
  subject: string;
55
57
  }
56
58
 
57
- export const emailTransporter = (logger: any = console) => {
59
+ // ─── Internal helpers ──────────────────────────────────────────────
60
+
61
+ function resolveTransportType(config?: EmailServiceConfig): 'smtp' | 'ses' {
62
+ if (config?.transport) return config.transport;
63
+ if (process.env.EMAIL_TRANSPORT === 'smtp') return 'smtp';
64
+ return 'ses';
65
+ }
66
+
67
+ function buildTransportOptions(type: 'smtp' | 'ses', config?: EmailServiceConfig) {
68
+ if (type === 'smtp') {
69
+ const smtp = config?.smtp;
70
+ return {
71
+ port: smtp?.port ?? (process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587),
72
+ host: smtp?.host ?? process.env.SMTP_HOST,
73
+ auth:
74
+ smtp?.auth ??
75
+ (process.env.SMTP_USER && process.env.SMTP_PASSWORD
76
+ ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD }
77
+ : undefined),
78
+ };
79
+ }
80
+
81
+ const ses = config?.ses;
82
+ const sesClient = new SESv2Client({
83
+ region: ses?.region ?? process.env.AWS_REGION,
84
+ ...(ses?.credentials ? { credentials: ses.credentials } : {}),
85
+ });
86
+ return { SES: { sesClient, SendEmailCommand } };
87
+ }
88
+
89
+ function buildDefaultOptions(type: 'smtp' | 'ses', config?: EmailServiceConfig) {
90
+ if (type === 'ses') {
91
+ const sourceArn = config?.ses?.sourceArn ?? process.env.EMAIL_SOURCE_ARN;
92
+ if (sourceArn) {
93
+ return { ses: { FromEmailAddressIdentityArn: sourceArn } };
94
+ }
95
+ }
96
+ return undefined;
97
+ }
98
+
99
+ // ─── Public API ────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Create an email transporter.
103
+ *
104
+ * Accepts an optional {@link EmailServiceConfig} so callers can pass
105
+ * explicit configuration instead of relying on environment variables.
106
+ * Any value not provided in `config` falls back to the corresponding
107
+ * `process.env` variable, preserving full backward-compatibility.
108
+ */
109
+ export const emailTransporter = (logger: any = console, config?: EmailServiceConfig) => {
110
+ const type = resolveTransportType(config);
111
+ const transportOptions = buildTransportOptions(type, config);
112
+ const defaults = buildDefaultOptions(type, config);
113
+
58
114
  const transporter = nodemailer.createTransport(
59
115
  {
60
- ...emailOptions,
61
- debug: true,
116
+ ...transportOptions,
117
+ debug: config?.debug ?? true,
62
118
  } as any,
63
- {
64
- ...(process.env.EMAIL_SOURCE_ARN
65
- ? {
66
- ses: {
67
- FromEmailAddressIdentityArn: process.env.EMAIL_SOURCE_ARN!,
68
- },
69
- }
70
- : undefined),
71
- } as any
119
+ defaults ? (defaults as any) : undefined
72
120
  );
73
121
 
122
+ const emailLogService = new EmailLogService(config?.logging);
123
+
74
124
  return {
75
125
  async sendMail(
76
126
  {
@@ -90,7 +140,6 @@ export const emailTransporter = (logger: any = console) => {
90
140
  return;
91
141
  }
92
142
  const sender = from;
93
- const emailLogService = new EmailLogService();
94
143
  logger.info(
95
144
  `Sending email to ${to} with subject: ${subject}, reference: ${reference}, idempotencyKey: ${idempotencyKey}`
96
145
  );
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './email';
2
2
  export * from './template';
3
+ export { EmailLogService } from './email-log';