@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.
- package/.turbo/turbo-build.log +22 -0
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-test.log +12 -0
- package/biome.json +3 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +232 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +193 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.js +10 -0
- package/package.json +40 -0
- package/src/email-log.ts +78 -0
- package/src/email.ts +128 -0
- package/src/index.ts +2 -0
- package/src/template.ts +59 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
> @xcelsior/email@1.0.0 build /Users/tuannguyen/Work/excelsior-packages/packages/services/email
|
|
3
|
+
> tsup
|
|
4
|
+
|
|
5
|
+
[34mCLI[39m Building entry: src/index.ts
|
|
6
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
+
[34mCLI[39m tsup v8.5.0
|
|
8
|
+
[34mCLI[39m Using tsup config: /Users/tuannguyen/Work/excelsior-packages/packages/services/email/tsup.config.ts
|
|
9
|
+
[34mCLI[39m Target: es2020
|
|
10
|
+
[34mCLI[39m Cleaning output folder
|
|
11
|
+
[34mCJS[39m Build start
|
|
12
|
+
[34mESM[39m Build start
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m7.42 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m11.60 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 22ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m5.19 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m11.34 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ 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,12 @@
|
|
|
1
|
+
|
|
2
|
+
> @xcelsior/email@1.0.0 test /Users/tuannguyen/Work/excelsior-packages/packages/services/email
|
|
3
|
+
> jest --passWithNoTests
|
|
4
|
+
|
|
5
|
+
[1m[2mDetermining test suites to run...[22m[22m[999D[K[1mNo tests found, exiting with code 0[22m
|
|
6
|
+
[999D[K
|
|
7
|
+
|
|
8
|
+
[K
|
|
9
|
+
[1A
|
|
10
|
+
|
|
11
|
+
[K
|
|
12
|
+
[1A[999D[K
|
package/biome.json
ADDED
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
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
|
+
}
|
package/src/email-log.ts
ADDED
|
@@ -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
package/src/template.ts
ADDED
|
@@ -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