@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.
- package/.turbo/turbo-build.log +13 -13
- package/.turbo/turbo-lint.log +2 -2
- package/.turbo/turbo-test.log +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +43 -27
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -27
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/email-log.ts +4 -3
- package/src/email.ts +101 -52
- package/src/index.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
> @xcelsior/email@1.0.
|
|
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
|
[34mCLI[39m Building entry: src/index.ts
|
|
6
6
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.
|
|
8
|
-
[34mCLI[39m Using tsup config: /Users/tuannguyen/Work/
|
|
7
|
+
[34mCLI[39m tsup v8.5.1
|
|
8
|
+
[34mCLI[39m Using tsup config: /Users/tuannguyen/Work/xcelsior-packages/packages/services/email/tsup.config.ts
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mCJS[39m Build start
|
|
12
12
|
[34mESM[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[32m5.
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[32m7.
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m5.72 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m12.54 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 40ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m7.95 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m12.80 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 40ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
21
|
-
DTS dist/index.d.ts
|
|
22
|
-
DTS dist/index.d.mts
|
|
20
|
+
DTS ⚡️ Build success in 1658ms
|
|
21
|
+
DTS dist/index.d.ts 818.00 B
|
|
22
|
+
DTS dist/index.d.mts 818.00 B
|
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @xcelsior/email@1.0.
|
|
2
|
+
> @xcelsior/email@1.0.3 lint /Users/tuannguyen/Work/xcelsior-packages/packages/services/email
|
|
3
3
|
> biome check .
|
|
4
4
|
|
|
5
|
-
[0m[34mChecked [0m[0m[34m9[0m[0m[34m [0m[0m[34mfiles[0m[0m[34m in [0m[0m[
|
|
5
|
+
[0m[34mChecked [0m[0m[34m9[0m[0m[34m [0m[0m[34mfiles[0m[0m[34m in [0m[0m[34m12[0m[0m[2m[34mms[0m[0m[34m.[0m[0m[34m No fixes applied.[0m
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @xcelsior/email@1.0.
|
|
2
|
+
> @xcelsior/email@1.0.3 test /Users/tuannguyen/Work/xcelsior-packages/packages/services/email
|
|
3
3
|
> jest --passWithNoTests
|
|
4
4
|
|
|
5
5
|
[1m[2mDetermining test suites to run...[22m[22m[999D[K[1mNo tests found, exiting with code 0[22m
|
package/CHANGELOG.md
ADDED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
};
|
|
83
|
-
|
|
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
|
-
...
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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