@xcelsior/email 1.0.0

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.
@@ -0,0 +1,22 @@
1
+
2
+ > @xcelsior/email@1.0.0 build /Users/tuannguyen/Work/excelsior-packages/packages/services/email
3
+ > tsup
4
+
5
+ CLI Building entry: src/index.ts
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
9
+ CLI Target: es2020
10
+ CLI Cleaning output folder
11
+ CJS Build start
12
+ ESM Build start
13
+ CJS dist/index.js 7.42 KB
14
+ CJS dist/index.js.map 11.60 KB
15
+ CJS ⚡️ Build success in 22ms
16
+ ESM dist/index.mjs 5.19 KB
17
+ ESM dist/index.mjs.map 11.34 KB
18
+ ESM ⚡️ Build success in 23ms
19
+ DTS Build start
20
+ DTS ⚡️ Build success in 1149ms
21
+ DTS dist/index.d.ts 849.00 B
22
+ DTS dist/index.d.mts 849.00 B
@@ -0,0 +1,5 @@
1
+
2
+ > @xcelsior/email@1.0.0 lint /Users/tuannguyen/Work/excelsior-packages/packages/services/email
3
+ > biome check .
4
+
5
+ Checked 13 files in 9ms. No fixes applied.
@@ -0,0 +1,12 @@
1
+
2
+ > @xcelsior/email@1.0.0 test /Users/tuannguyen/Work/excelsior-packages/packages/services/email
3
+ > jest --passWithNoTests
4
+
5
+ Determining test suites to run...No tests found, exiting with code 0
6
+ 
7
+
8
+ 
9
+ 
10
+
11
+ 
12
+ 
package/biome.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "//"
3
+ }
@@ -0,0 +1,21 @@
1
+ import * as nodemailer_lib_smtp_pool from 'nodemailer/lib/smtp-pool';
2
+
3
+ interface Email {
4
+ to?: string | null;
5
+ from?: string;
6
+ reference?: string;
7
+ template?: string;
8
+ idempotencyKey?: string;
9
+ replyTo?: string | null;
10
+ text?: string;
11
+ attachments?: any[];
12
+ subject: string;
13
+ }
14
+ declare const emailTransporter: (sourceArn: string | undefined, logger?: any) => {
15
+ sendMail({ to, reference, from, replyTo, template, text, attachments, idempotencyKey, subject, }: Email, idempotentCheck?: boolean): Promise<nodemailer_lib_smtp_pool.SentMessageInfo | undefined>;
16
+ };
17
+
18
+ declare function renderTemplate(content: string, data: Record<string, any>): string;
19
+ declare function renderTemplateFile(file: string, data: Record<string, any>): Promise<string>;
20
+
21
+ export { type Email, emailTransporter, renderTemplate, renderTemplateFile };
@@ -0,0 +1,21 @@
1
+ import * as nodemailer_lib_smtp_pool from 'nodemailer/lib/smtp-pool';
2
+
3
+ interface Email {
4
+ to?: string | null;
5
+ from?: string;
6
+ reference?: string;
7
+ template?: string;
8
+ idempotencyKey?: string;
9
+ replyTo?: string | null;
10
+ text?: string;
11
+ attachments?: any[];
12
+ subject: string;
13
+ }
14
+ declare const emailTransporter: (sourceArn: string | undefined, logger?: any) => {
15
+ sendMail({ to, reference, from, replyTo, template, text, attachments, idempotencyKey, subject, }: Email, idempotentCheck?: boolean): Promise<nodemailer_lib_smtp_pool.SentMessageInfo | undefined>;
16
+ };
17
+
18
+ declare function renderTemplate(content: string, data: Record<string, any>): string;
19
+ declare function renderTemplateFile(file: string, data: Record<string, any>): Promise<string>;
20
+
21
+ export { type Email, emailTransporter, renderTemplate, renderTemplateFile };
package/dist/index.js ADDED
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ emailTransporter: () => emailTransporter,
34
+ renderTemplate: () => renderTemplate,
35
+ renderTemplateFile: () => renderTemplateFile
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/email.ts
40
+ var import_client_sesv2 = require("@aws-sdk/client-sesv2");
41
+ var import_nodemailer = __toESM(require("nodemailer"));
42
+
43
+ // src/email-log.ts
44
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
45
+ var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
46
+ var import_uuid = require("uuid");
47
+ var EmailLogService = class {
48
+ constructor() {
49
+ this.tableName = process.env.EMAIL_TABLE_NAME;
50
+ this.dynamoClient = new import_client_dynamodb.DynamoDBClient({
51
+ region: process.env.AWS_REGION
52
+ });
53
+ this.docClient = import_lib_dynamodb.DynamoDBDocumentClient.from(this.dynamoClient);
54
+ }
55
+ async logEmail({
56
+ subject,
57
+ sender,
58
+ receiver,
59
+ status,
60
+ error,
61
+ idempotencyKey,
62
+ reference = (0, import_uuid.v4)()
63
+ }) {
64
+ const timestamp = Date.now();
65
+ const log = {
66
+ reference,
67
+ timestamp,
68
+ subject,
69
+ sender,
70
+ receiver,
71
+ status,
72
+ idempotencyKey,
73
+ ...error && { error }
74
+ };
75
+ await this.docClient.send(
76
+ new import_lib_dynamodb.PutCommand({
77
+ TableName: this.tableName,
78
+ Item: log
79
+ })
80
+ );
81
+ return reference;
82
+ }
83
+ async getEmailByIdempotencyKey(idempotencyKey) {
84
+ if (!idempotencyKey) {
85
+ return null;
86
+ }
87
+ const result = await this.docClient.send(
88
+ new import_lib_dynamodb.QueryCommand({
89
+ TableName: this.tableName,
90
+ IndexName: "idempotency-key-index",
91
+ KeyConditionExpression: "idempotencyKey = :idempotencyKey",
92
+ ExpressionAttributeValues: {
93
+ ":idempotencyKey": idempotencyKey
94
+ },
95
+ Limit: 1
96
+ })
97
+ );
98
+ return result.Items?.[0];
99
+ }
100
+ };
101
+
102
+ // src/email.ts
103
+ var sesClient = new import_client_sesv2.SESv2Client({
104
+ region: process.env.AWS_REGION
105
+ });
106
+ var emailOptions = process.env.NODE_ENV === "development" ? {
107
+ port: 1025,
108
+ host: process.env.SMTP_HOST
109
+ } : {
110
+ SES: { sesClient, SendEmailCommand: import_client_sesv2.SendEmailCommand }
111
+ };
112
+ var emailTransporter = (sourceArn, logger = console) => {
113
+ const transporter = import_nodemailer.default.createTransport(
114
+ {
115
+ ...emailOptions,
116
+ debug: true
117
+ },
118
+ {
119
+ ses: {
120
+ FromEmailAddressIdentityArn: sourceArn
121
+ }
122
+ }
123
+ );
124
+ return {
125
+ async sendMail({
126
+ to,
127
+ reference,
128
+ from,
129
+ replyTo,
130
+ template,
131
+ text,
132
+ attachments = [],
133
+ idempotencyKey,
134
+ subject
135
+ }, idempotentCheck) {
136
+ if (!to) {
137
+ return;
138
+ }
139
+ const sender = from;
140
+ const emailLogService = new EmailLogService();
141
+ try {
142
+ if (idempotentCheck) {
143
+ const existingEmail = await emailLogService.getEmailByIdempotencyKey(idempotencyKey);
144
+ if (existingEmail) {
145
+ logger.info(`Email with idempotency key ${idempotencyKey} already sent`);
146
+ return;
147
+ }
148
+ }
149
+ const result = await transporter.sendMail({
150
+ to,
151
+ from: sender,
152
+ replyTo: replyTo ?? void 0,
153
+ html: template,
154
+ text,
155
+ subject,
156
+ attachments
157
+ });
158
+ await emailLogService.logEmail({
159
+ subject: subject || "No Subject",
160
+ reference,
161
+ sender: sender || "Unknown Sender",
162
+ idempotencyKey,
163
+ receiver: to,
164
+ status: "success"
165
+ });
166
+ return result;
167
+ } catch (error) {
168
+ await emailLogService.logEmail({
169
+ subject: subject || "No Subject",
170
+ sender: sender || "Unknown Sender",
171
+ receiver: to,
172
+ status: "failed",
173
+ error: error instanceof Error ? error.message : String(error)
174
+ });
175
+ throw error;
176
+ }
177
+ }
178
+ };
179
+ };
180
+
181
+ // src/template.ts
182
+ var import_node_fs = __toESM(require("fs"));
183
+ var import_utils = require("@excelsior/utils");
184
+ var import_handlebars = __toESM(require("handlebars"));
185
+ var import_lodash = __toESM(require("lodash"));
186
+ var import_moment = __toESM(require("moment"));
187
+ var { getDateTime } = import_utils.dates;
188
+ var { convertMoneyFormat, moneyFormat } = import_utils.formatters;
189
+ import_handlebars.default.registerHelper("multiply", (a, b) => a * b);
190
+ import_handlebars.default.registerHelper("datetime", (a) => (0, import_moment.default)(getDateTime(a)).format("DD/MM/YYYY HH:mm"));
191
+ import_handlebars.default.registerHelper("moneyFormat", (a, ...others) => {
192
+ let currency = "AUD";
193
+ if (others.length > 1) {
194
+ currency = others[0];
195
+ }
196
+ return moneyFormat(a, currency);
197
+ });
198
+ import_handlebars.default.registerHelper(
199
+ "convertMoneyFormat",
200
+ (amount, currency, rate = 1) => convertMoneyFormat(amount, currency, rate)
201
+ );
202
+ import_handlebars.default.registerHelper("titleCase", (string) => {
203
+ if (!string) return "";
204
+ return import_lodash.default.startCase(import_lodash.default.camelCase(string)).replace(/\s/g, " ");
205
+ });
206
+ import_handlebars.default.registerHelper("choose", (a, b) => a ? a : b);
207
+ import_handlebars.default.registerHelper(
208
+ "phoneFormat",
209
+ (phone) => `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`
210
+ );
211
+ import_handlebars.default.registerHelper("title", (title) => import_lodash.default.startCase(title));
212
+ import_handlebars.default.registerHelper("eq", (a, b) => a === b);
213
+ import_handlebars.default.registerHelper("lowercase", (title) => title.toLowerCase());
214
+ import_handlebars.default.registerHelper("JSONparse", (string, key) => JSON.parse(string)[key]);
215
+ import_handlebars.default.registerHelper("incr", (a) => a + 1);
216
+ import_handlebars.default.registerHelper("gt", (a, b) => a > b);
217
+ function renderTemplate(content, data) {
218
+ return import_handlebars.default.compile(content)(data);
219
+ }
220
+ async function renderTemplateFile(file, data) {
221
+ const content = await import_node_fs.default.promises.readFile(file, {
222
+ encoding: "utf-8"
223
+ });
224
+ return renderTemplate(content, data);
225
+ }
226
+ // Annotate the CommonJS export names for ESM import in node:
227
+ 0 && (module.exports = {
228
+ emailTransporter,
229
+ renderTemplate,
230
+ renderTemplateFile
231
+ });
232
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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.NODE_ENV === 'development'\n ? {\n port: 1025,\n host: process.env.SMTP_HOST,\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 = (sourceArn: string | undefined, logger: any = console) => {\n const transporter = nodemailer.createTransport(\n {\n ...emailOptions,\n debug: true,\n } as any,\n {\n ses: {\n FromEmailAddressIdentityArn: sourceArn,\n },\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\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;\n private readonly docClient: DynamoDBDocumentClient;\n\n constructor() {\n this.tableName = process.env.EMAIL_TABLE_NAME!;\n this.dynamoClient = new DynamoDBClient({\n region: process.env.AWS_REGION,\n });\n this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);\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 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) {\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 '@excelsior/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 ADDED
@@ -0,0 +1,193 @@
1
+ // src/email.ts
2
+ import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2";
3
+ import nodemailer from "nodemailer";
4
+
5
+ // src/email-log.ts
6
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
7
+ import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
8
+ import { v4 as uuidv4 } from "uuid";
9
+ var EmailLogService = class {
10
+ constructor() {
11
+ this.tableName = process.env.EMAIL_TABLE_NAME;
12
+ this.dynamoClient = new DynamoDBClient({
13
+ region: process.env.AWS_REGION
14
+ });
15
+ this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);
16
+ }
17
+ async logEmail({
18
+ subject,
19
+ sender,
20
+ receiver,
21
+ status,
22
+ error,
23
+ idempotencyKey,
24
+ reference = uuidv4()
25
+ }) {
26
+ const timestamp = Date.now();
27
+ const log = {
28
+ reference,
29
+ timestamp,
30
+ subject,
31
+ sender,
32
+ receiver,
33
+ status,
34
+ idempotencyKey,
35
+ ...error && { error }
36
+ };
37
+ await this.docClient.send(
38
+ new PutCommand({
39
+ TableName: this.tableName,
40
+ Item: log
41
+ })
42
+ );
43
+ return reference;
44
+ }
45
+ async getEmailByIdempotencyKey(idempotencyKey) {
46
+ if (!idempotencyKey) {
47
+ return null;
48
+ }
49
+ const result = await this.docClient.send(
50
+ new QueryCommand({
51
+ TableName: this.tableName,
52
+ IndexName: "idempotency-key-index",
53
+ KeyConditionExpression: "idempotencyKey = :idempotencyKey",
54
+ ExpressionAttributeValues: {
55
+ ":idempotencyKey": idempotencyKey
56
+ },
57
+ Limit: 1
58
+ })
59
+ );
60
+ return result.Items?.[0];
61
+ }
62
+ };
63
+
64
+ // src/email.ts
65
+ var sesClient = new SESv2Client({
66
+ region: process.env.AWS_REGION
67
+ });
68
+ var emailOptions = process.env.NODE_ENV === "development" ? {
69
+ port: 1025,
70
+ host: process.env.SMTP_HOST
71
+ } : {
72
+ SES: { sesClient, SendEmailCommand }
73
+ };
74
+ var emailTransporter = (sourceArn, logger = console) => {
75
+ const transporter = nodemailer.createTransport(
76
+ {
77
+ ...emailOptions,
78
+ debug: true
79
+ },
80
+ {
81
+ ses: {
82
+ FromEmailAddressIdentityArn: sourceArn
83
+ }
84
+ }
85
+ );
86
+ return {
87
+ async sendMail({
88
+ to,
89
+ reference,
90
+ from,
91
+ replyTo,
92
+ template,
93
+ text,
94
+ attachments = [],
95
+ idempotencyKey,
96
+ subject
97
+ }, idempotentCheck) {
98
+ if (!to) {
99
+ return;
100
+ }
101
+ const sender = from;
102
+ const emailLogService = new EmailLogService();
103
+ try {
104
+ if (idempotentCheck) {
105
+ const existingEmail = await emailLogService.getEmailByIdempotencyKey(idempotencyKey);
106
+ if (existingEmail) {
107
+ logger.info(`Email with idempotency key ${idempotencyKey} already sent`);
108
+ return;
109
+ }
110
+ }
111
+ const result = await transporter.sendMail({
112
+ to,
113
+ from: sender,
114
+ replyTo: replyTo ?? void 0,
115
+ html: template,
116
+ text,
117
+ subject,
118
+ attachments
119
+ });
120
+ await emailLogService.logEmail({
121
+ subject: subject || "No Subject",
122
+ reference,
123
+ sender: sender || "Unknown Sender",
124
+ idempotencyKey,
125
+ receiver: to,
126
+ status: "success"
127
+ });
128
+ return result;
129
+ } catch (error) {
130
+ await emailLogService.logEmail({
131
+ subject: subject || "No Subject",
132
+ sender: sender || "Unknown Sender",
133
+ receiver: to,
134
+ status: "failed",
135
+ error: error instanceof Error ? error.message : String(error)
136
+ });
137
+ throw error;
138
+ }
139
+ }
140
+ };
141
+ };
142
+
143
+ // src/template.ts
144
+ import fs from "fs";
145
+ import { formatters, dates } from "@excelsior/utils";
146
+ import handlebars from "handlebars";
147
+ import _ from "lodash";
148
+ import moment from "moment";
149
+ var { getDateTime } = dates;
150
+ var { convertMoneyFormat, moneyFormat } = formatters;
151
+ handlebars.registerHelper("multiply", (a, b) => a * b);
152
+ handlebars.registerHelper("datetime", (a) => moment(getDateTime(a)).format("DD/MM/YYYY HH:mm"));
153
+ handlebars.registerHelper("moneyFormat", (a, ...others) => {
154
+ let currency = "AUD";
155
+ if (others.length > 1) {
156
+ currency = others[0];
157
+ }
158
+ return moneyFormat(a, currency);
159
+ });
160
+ handlebars.registerHelper(
161
+ "convertMoneyFormat",
162
+ (amount, currency, rate = 1) => convertMoneyFormat(amount, currency, rate)
163
+ );
164
+ handlebars.registerHelper("titleCase", (string) => {
165
+ if (!string) return "";
166
+ return _.startCase(_.camelCase(string)).replace(/\s/g, " ");
167
+ });
168
+ handlebars.registerHelper("choose", (a, b) => a ? a : b);
169
+ handlebars.registerHelper(
170
+ "phoneFormat",
171
+ (phone) => `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`
172
+ );
173
+ handlebars.registerHelper("title", (title) => _.startCase(title));
174
+ handlebars.registerHelper("eq", (a, b) => a === b);
175
+ handlebars.registerHelper("lowercase", (title) => title.toLowerCase());
176
+ handlebars.registerHelper("JSONparse", (string, key) => JSON.parse(string)[key]);
177
+ handlebars.registerHelper("incr", (a) => a + 1);
178
+ handlebars.registerHelper("gt", (a, b) => a > b);
179
+ function renderTemplate(content, data) {
180
+ return handlebars.compile(content)(data);
181
+ }
182
+ async function renderTemplateFile(file, data) {
183
+ const content = await fs.promises.readFile(file, {
184
+ encoding: "utf-8"
185
+ });
186
+ return renderTemplate(content, data);
187
+ }
188
+ export {
189
+ emailTransporter,
190
+ renderTemplate,
191
+ renderTemplateFile
192
+ };
193
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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.NODE_ENV === 'development'\n ? {\n port: 1025,\n host: process.env.SMTP_HOST,\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 = (sourceArn: string | undefined, logger: any = console) => {\n const transporter = nodemailer.createTransport(\n {\n ...emailOptions,\n debug: true,\n } as any,\n {\n ses: {\n FromEmailAddressIdentityArn: sourceArn,\n },\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\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;\n private readonly docClient: DynamoDBDocumentClient;\n\n constructor() {\n this.tableName = process.env.EMAIL_TABLE_NAME!;\n this.dynamoClient = new DynamoDBClient({\n region: process.env.AWS_REGION,\n });\n this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);\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 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) {\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 '@excelsior/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/jest.config.js ADDED
@@ -0,0 +1,10 @@
1
+ const baseConfig = require('../../../jest.config.base');
2
+
3
+ /** @type {import('jest').Config} */
4
+ const config = {
5
+ ...baseConfig,
6
+ displayName: '@xcelsior/email',
7
+ rootDir: '.',
8
+ };
9
+
10
+ module.exports = config;
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@xcelsior/email",
3
+ "version": "1.0.0",
4
+ "exports": {
5
+ ".": {
6
+ "import": "./src/index.ts",
7
+ "require": "./dist/index.js"
8
+ }
9
+ },
10
+ "main": "src/index.ts",
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "description": "",
15
+ "peerDependencies": {
16
+ "@aws-sdk/client-dynamodb": "^3.782.0",
17
+ "@aws-sdk/lib-dynamodb": "^3.782.0"
18
+ },
19
+ "dependencies": {
20
+ "lodash": "^4.17.21",
21
+ "handlebars": "^4.7.8",
22
+ "moment": "^2.30.1",
23
+ "nodemailer": "^7.0.4",
24
+ "@aws-sdk/client-sesv2": "^3.840.0",
25
+ "uuid": "^11.1.0",
26
+ "@xcelsior/utils": "1.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/lodash": "^4.17.20",
30
+ "@types/node": "^24.0.10",
31
+ "@types/nodemailer": "^6.4.17",
32
+ "@types/uuid": "^10.0.0"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "test": "jest --passWithNoTests",
38
+ "lint": "biome check ."
39
+ }
40
+ }
@@ -0,0 +1,78 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { DynamoDBDocumentClient, PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ interface EmailLog {
6
+ reference?: string;
7
+ timestamp: number;
8
+ subject: string;
9
+ sender: string;
10
+ receiver: string;
11
+ idempotencyKey?: string;
12
+ status: 'success' | 'failed';
13
+ error?: string;
14
+ }
15
+
16
+ export class EmailLogService {
17
+ private readonly tableName: string;
18
+ private readonly dynamoClient: DynamoDBClient;
19
+ private readonly docClient: DynamoDBDocumentClient;
20
+
21
+ constructor() {
22
+ this.tableName = process.env.EMAIL_TABLE_NAME!;
23
+ this.dynamoClient = new DynamoDBClient({
24
+ region: process.env.AWS_REGION,
25
+ });
26
+ this.docClient = DynamoDBDocumentClient.from(this.dynamoClient);
27
+ }
28
+
29
+ async logEmail({
30
+ subject,
31
+ sender,
32
+ receiver,
33
+ status,
34
+ error,
35
+ idempotencyKey,
36
+ reference = uuidv4(),
37
+ }: Omit<EmailLog, 'timestamp'>): Promise<string> {
38
+ const timestamp = Date.now();
39
+
40
+ const log: EmailLog = {
41
+ reference,
42
+ timestamp,
43
+ subject,
44
+ sender,
45
+ receiver,
46
+ status,
47
+ idempotencyKey,
48
+ ...(error && { error }),
49
+ };
50
+
51
+ await this.docClient.send(
52
+ new PutCommand({
53
+ TableName: this.tableName,
54
+ Item: log,
55
+ })
56
+ );
57
+ return reference;
58
+ }
59
+
60
+ async getEmailByIdempotencyKey(idempotencyKey: string | undefined) {
61
+ if (!idempotencyKey) {
62
+ return null;
63
+ }
64
+
65
+ const result = await this.docClient.send(
66
+ new QueryCommand({
67
+ TableName: this.tableName,
68
+ IndexName: 'idempotency-key-index',
69
+ KeyConditionExpression: 'idempotencyKey = :idempotencyKey',
70
+ ExpressionAttributeValues: {
71
+ ':idempotencyKey': idempotencyKey,
72
+ },
73
+ Limit: 1,
74
+ })
75
+ );
76
+ return result.Items?.[0] as EmailLog | null;
77
+ }
78
+ }
package/src/email.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2';
2
+ import nodemailer from 'nodemailer';
3
+ import { EmailLogService } from './email-log';
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.NODE_ENV === 'development'
30
+ ? {
31
+ port: 1025,
32
+ host: process.env.SMTP_HOST,
33
+ }
34
+ : {
35
+ SES: { sesClient, SendEmailCommand },
36
+ };
37
+
38
+ export interface Email {
39
+ to?: string | null;
40
+ from?: string;
41
+ reference?: string;
42
+ template?: string;
43
+ idempotencyKey?: string;
44
+ replyTo?: string | null;
45
+ text?: string;
46
+ attachments?: any[];
47
+ subject: string;
48
+ }
49
+
50
+ export const emailTransporter = (sourceArn: string | undefined, logger: any = console) => {
51
+ const transporter = nodemailer.createTransport(
52
+ {
53
+ ...emailOptions,
54
+ debug: true,
55
+ } as any,
56
+ {
57
+ ses: {
58
+ FromEmailAddressIdentityArn: sourceArn,
59
+ },
60
+ } as any
61
+ );
62
+
63
+ return {
64
+ async sendMail(
65
+ {
66
+ to,
67
+ reference,
68
+ from,
69
+ replyTo,
70
+ template,
71
+ text,
72
+ attachments = [],
73
+ idempotencyKey,
74
+ subject,
75
+ }: Email,
76
+ idempotentCheck?: boolean
77
+ ) {
78
+ if (!to) {
79
+ return;
80
+ }
81
+ const sender = from;
82
+ const emailLogService = new EmailLogService();
83
+
84
+ try {
85
+ if (idempotentCheck) {
86
+ const existingEmail =
87
+ await emailLogService.getEmailByIdempotencyKey(idempotencyKey);
88
+ if (existingEmail) {
89
+ logger.info(`Email with idempotency key ${idempotencyKey} already sent`);
90
+ return;
91
+ }
92
+ }
93
+ const result = await transporter.sendMail({
94
+ to,
95
+ from: sender,
96
+ replyTo: replyTo ?? undefined,
97
+ html: template,
98
+ text,
99
+ subject,
100
+ attachments,
101
+ });
102
+
103
+ // Log successful email
104
+ await emailLogService.logEmail({
105
+ subject: subject || 'No Subject',
106
+ reference,
107
+ sender: sender || 'Unknown Sender',
108
+ idempotencyKey,
109
+ receiver: to,
110
+ status: 'success',
111
+ });
112
+
113
+ return result;
114
+ } catch (error) {
115
+ // Log failed email
116
+ await emailLogService.logEmail({
117
+ subject: subject || 'No Subject',
118
+ sender: sender || 'Unknown Sender',
119
+ receiver: to,
120
+ status: 'failed',
121
+ error: error instanceof Error ? error.message : String(error),
122
+ });
123
+
124
+ throw error;
125
+ }
126
+ },
127
+ };
128
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './email';
2
+ export * from './template';
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs';
2
+ import { formatters, dates } from '@xcelsior/utils';
3
+ import handlebars from 'handlebars';
4
+ import _ from 'lodash';
5
+ import moment from 'moment';
6
+
7
+ const { getDateTime } = dates;
8
+ const { convertMoneyFormat, moneyFormat } = formatters;
9
+
10
+ handlebars.registerHelper('multiply', (a, b) => a * b);
11
+
12
+ handlebars.registerHelper('datetime', (a) => moment(getDateTime(a)).format('DD/MM/YYYY HH:mm'));
13
+
14
+ handlebars.registerHelper('moneyFormat', (a, ...others) => {
15
+ let currency = 'AUD' as const;
16
+ if (others.length > 1) {
17
+ currency = others[0] as any;
18
+ }
19
+ return moneyFormat(a, currency);
20
+ });
21
+
22
+ handlebars.registerHelper('convertMoneyFormat', (amount, currency, rate = 1) =>
23
+ convertMoneyFormat(amount, currency, rate)
24
+ );
25
+
26
+ handlebars.registerHelper('titleCase', (string) => {
27
+ if (!string) return '';
28
+ return _.startCase(_.camelCase(string)).replace(/\s/g, ' ');
29
+ });
30
+
31
+ handlebars.registerHelper('choose', (a, b) => (a ? a : b));
32
+
33
+ handlebars.registerHelper(
34
+ 'phoneFormat',
35
+ (phone) =>
36
+ `${phone.substring(0, phone.length / 2 - 2)}****${phone.substring(phone.length / 2 + 2)}`
37
+ );
38
+
39
+ handlebars.registerHelper('title', (title) => _.startCase(title));
40
+
41
+ handlebars.registerHelper('eq', (a, b) => a === b);
42
+ handlebars.registerHelper('lowercase', (title: string) => title.toLowerCase());
43
+
44
+ handlebars.registerHelper('JSONparse', (string, key) => JSON.parse(string)[key]);
45
+
46
+ handlebars.registerHelper('incr', (a) => a + 1);
47
+
48
+ handlebars.registerHelper('gt', (a, b) => a > b);
49
+
50
+ export function renderTemplate(content: string, data: Record<string, any>): string {
51
+ return handlebars.compile(content)(data);
52
+ }
53
+
54
+ export async function renderTemplateFile(file: string, data: Record<string, any>) {
55
+ const content = await fs.promises.readFile(file, {
56
+ encoding: 'utf-8',
57
+ });
58
+ return renderTemplate(content, data);
59
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });