@clairejs/server 3.28.9 → 3.28.11
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/README.md +8 -0
- package/dist/job/LocalJobScheduler.js +2 -2
- package/dist/services/AbstractMailService.d.ts +7 -0
- package/dist/services/implementations/LocalMailService.js +8 -1
- package/dist/services/implementations/SesMailService.js +39 -30
- package/dist/services/utils/MimeBuilder.d.ts +4 -0
- package/dist/services/utils/MimeBuilder.js +93 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
63
63
|
this.keyRetentionDurationSeconds = keyRetentionDurationSeconds;
|
|
64
64
|
}
|
|
65
65
|
sendJob(type, data) {
|
|
66
|
-
this.
|
|
66
|
+
this.redisClient.publish(this.multiClientChannel, JSON.stringify({ type, data }));
|
|
67
67
|
}
|
|
68
68
|
async processMessage(type, data) {
|
|
69
69
|
if (!this.isActive) {
|
|
@@ -110,7 +110,7 @@ let LocalJobScheduler = class LocalJobScheduler extends AbstractJobScheduler {
|
|
|
110
110
|
this.logger.debug("LocalJobScheduler init");
|
|
111
111
|
//-- subscribe to multi client channel
|
|
112
112
|
this.logger.debug("Listening on multi client channel");
|
|
113
|
-
this.
|
|
113
|
+
this.subscribeClient.on("message", (channel, message) => {
|
|
114
114
|
if (channel === this.multiClientChannel) {
|
|
115
115
|
//-- process message
|
|
116
116
|
const payload = JSON.parse(message);
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export interface Attachment {
|
|
2
|
+
filename: string;
|
|
3
|
+
contentType: string;
|
|
4
|
+
contentBase64: string;
|
|
5
|
+
contentId?: string;
|
|
6
|
+
}
|
|
1
7
|
export interface EmailInfo {
|
|
2
8
|
sender: string;
|
|
3
9
|
receivers: string[];
|
|
@@ -6,6 +12,7 @@ export interface EmailInfo {
|
|
|
6
12
|
subject: string;
|
|
7
13
|
content: string;
|
|
8
14
|
contentType: "html" | "text";
|
|
15
|
+
attachments?: Attachment[];
|
|
9
16
|
}
|
|
10
17
|
export declare abstract class AbstractMailService {
|
|
11
18
|
abstract send(email: EmailInfo): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { AbstractMailService } from "../AbstractMailService";
|
|
4
|
+
import { buildMimeMessage } from "../utils/MimeBuilder";
|
|
4
5
|
export class LocalMailService extends AbstractMailService {
|
|
5
6
|
folderPath;
|
|
6
7
|
constructor(folderPath) {
|
|
@@ -11,7 +12,13 @@ export class LocalMailService extends AbstractMailService {
|
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
async send(email) {
|
|
14
|
-
|
|
15
|
+
if (!!email.attachments?.length) {
|
|
16
|
+
const emlFilePath = path.join(this.folderPath, `${Date.now()}.eml`);
|
|
17
|
+
const raw = buildMimeMessage(email);
|
|
18
|
+
fs.writeFileSync(emlFilePath, raw, { encoding: "utf-8" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
//-- write simple email to local folder
|
|
15
22
|
const mailFilePath = path.join(this.folderPath, `${Date.now()}.html`);
|
|
16
23
|
fs.writeFileSync(mailFilePath, `
|
|
17
24
|
From: ${email.sender}\n
|
|
@@ -7,9 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import { AbstractLogger,
|
|
10
|
+
import { AbstractLogger, LogContext } from "@clairejs/core";
|
|
11
11
|
import aws from "aws-sdk";
|
|
12
12
|
import { AbstractMailService } from "../AbstractMailService";
|
|
13
|
+
import { buildMimeMessage } from "../utils/MimeBuilder";
|
|
13
14
|
let SesMailService = class SesMailService extends AbstractMailService {
|
|
14
15
|
config;
|
|
15
16
|
logger;
|
|
@@ -21,40 +22,48 @@ let SesMailService = class SesMailService extends AbstractMailService {
|
|
|
21
22
|
this.emailClient = new aws.SES({ apiVersion: "2010-12-01", region: this.config.SES_REGION });
|
|
22
23
|
}
|
|
23
24
|
async send(email) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
Message: {
|
|
32
|
-
Body: {},
|
|
33
|
-
Subject: {
|
|
34
|
-
Charset: "UTF-8",
|
|
35
|
-
Data: email.subject,
|
|
36
|
-
},
|
|
37
|
-
},
|
|
25
|
+
const hasAttachments = !!(email.attachments && email.attachments.length > 0);
|
|
26
|
+
if (hasAttachments) {
|
|
27
|
+
const raw = buildMimeMessage(email);
|
|
28
|
+
const destinations = [...(email.receivers || []), ...(email.cc || []), ...(email.bcc || [])];
|
|
29
|
+
const rawParams = {
|
|
30
|
+
Destinations: destinations,
|
|
38
31
|
Source: email.sender,
|
|
32
|
+
RawMessage: {
|
|
33
|
+
Data: Buffer.from(raw, "utf-8"),
|
|
34
|
+
},
|
|
39
35
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
await this.emailClient.sendRawEmail(rawParams).promise();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const param = {
|
|
40
|
+
Destination: {
|
|
41
|
+
BccAddresses: email.bcc,
|
|
42
|
+
CcAddresses: email.cc,
|
|
43
|
+
ToAddresses: email.receivers,
|
|
44
|
+
},
|
|
45
|
+
Message: {
|
|
46
|
+
Body: {},
|
|
47
|
+
Subject: {
|
|
48
48
|
Charset: "UTF-8",
|
|
49
|
-
Data: email.
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
49
|
+
Data: email.subject,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
Source: email.sender,
|
|
53
|
+
};
|
|
54
|
+
if (email.contentType === "html") {
|
|
55
|
+
param.Message.Body.Html = {
|
|
56
|
+
Charset: "UTF-8",
|
|
57
|
+
Data: email.content,
|
|
58
|
+
};
|
|
53
59
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
else if (email.contentType === "text") {
|
|
61
|
+
param.Message.Body.Text = {
|
|
62
|
+
Charset: "UTF-8",
|
|
63
|
+
Data: email.content,
|
|
64
|
+
};
|
|
57
65
|
}
|
|
66
|
+
await this.emailClient.sendEmail(param).promise();
|
|
58
67
|
}
|
|
59
68
|
};
|
|
60
69
|
SesMailService = __decorate([
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function parseAttachments(attachments) {
|
|
2
|
+
if (!attachments || attachments.length === 0)
|
|
3
|
+
return [];
|
|
4
|
+
return attachments
|
|
5
|
+
.map((att) => {
|
|
6
|
+
if (!att)
|
|
7
|
+
return undefined;
|
|
8
|
+
const filename = att.filename || att.name;
|
|
9
|
+
const contentType = att.contentType || att.mimeType || "application/octet-stream";
|
|
10
|
+
let contentBase64 = att.contentBase64 || att.base64 || att.content;
|
|
11
|
+
const contentId = att.contentId;
|
|
12
|
+
if (!filename || !contentBase64)
|
|
13
|
+
return undefined;
|
|
14
|
+
// Strip potential data URL prefixes
|
|
15
|
+
const commaIdx = contentBase64.indexOf(",");
|
|
16
|
+
if (contentBase64.startsWith("data:") && commaIdx !== -1) {
|
|
17
|
+
contentBase64 = contentBase64.substring(commaIdx + 1);
|
|
18
|
+
}
|
|
19
|
+
// Remove whitespace
|
|
20
|
+
contentBase64 = contentBase64.replace(/\s+/g, "");
|
|
21
|
+
return { filename, contentType, contentBase64, contentId };
|
|
22
|
+
})
|
|
23
|
+
.filter((x) => !!x);
|
|
24
|
+
}
|
|
25
|
+
function foldBase64(input, lineLength = 76) {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for (let i = 0; i < input.length; i += lineLength) {
|
|
28
|
+
chunks.push(input.substring(i, i + lineLength));
|
|
29
|
+
}
|
|
30
|
+
return chunks.join("\r\n");
|
|
31
|
+
}
|
|
32
|
+
function encodeHeaderValue(value) {
|
|
33
|
+
// Simple header escaping for newlines
|
|
34
|
+
return value.replace(/\r?\n/g, " ");
|
|
35
|
+
}
|
|
36
|
+
function toAddressHeader(name, emails) {
|
|
37
|
+
if (!emails || emails.length === 0)
|
|
38
|
+
return "";
|
|
39
|
+
return `${name}: ${emails.join(", ")}\r\n`;
|
|
40
|
+
}
|
|
41
|
+
function generateBoundary(prefix) {
|
|
42
|
+
return `${prefix}-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
|
43
|
+
}
|
|
44
|
+
export function buildMimeMessage(email) {
|
|
45
|
+
const attachments = parseAttachments(email.attachments);
|
|
46
|
+
const from = `From: ${encodeHeaderValue(email.sender)}\r\n`;
|
|
47
|
+
const to = toAddressHeader("To", email.receivers);
|
|
48
|
+
const cc = toAddressHeader("Cc", email.cc);
|
|
49
|
+
const subject = `Subject: ${encodeHeaderValue(email.subject)}\r\n`;
|
|
50
|
+
const mimeVersion = `MIME-Version: 1.0\r\n`;
|
|
51
|
+
// Build body part
|
|
52
|
+
const mainContentType = email.contentType === "html" ? "text/html" : "text/plain";
|
|
53
|
+
const bodyBase64 = Buffer.from(email.content, "utf-8").toString("base64");
|
|
54
|
+
const bodyPart = `Content-Type: ${mainContentType}; charset="UTF-8"\r\n` +
|
|
55
|
+
`Content-Transfer-Encoding: base64\r\n\r\n` +
|
|
56
|
+
`${foldBase64(bodyBase64)}\r\n`;
|
|
57
|
+
if (attachments.length === 0) {
|
|
58
|
+
// Simple single-part message
|
|
59
|
+
const headers = from +
|
|
60
|
+
to +
|
|
61
|
+
cc +
|
|
62
|
+
subject +
|
|
63
|
+
mimeVersion +
|
|
64
|
+
`Content-Type: ${mainContentType}; charset="UTF-8"\r\n` +
|
|
65
|
+
`Content-Transfer-Encoding: base64\r\n\r\n`;
|
|
66
|
+
return headers + foldBase64(bodyBase64) + "\r\n";
|
|
67
|
+
}
|
|
68
|
+
// Multipart/mixed with attachments
|
|
69
|
+
const mixedBoundary = generateBoundary("mixed");
|
|
70
|
+
let mime = "";
|
|
71
|
+
mime += from;
|
|
72
|
+
mime += to;
|
|
73
|
+
mime += cc;
|
|
74
|
+
mime += subject;
|
|
75
|
+
mime += mimeVersion;
|
|
76
|
+
mime += `Content-Type: multipart/mixed; boundary="${mixedBoundary}"\r\n\r\n`;
|
|
77
|
+
// Body part
|
|
78
|
+
mime += `--${mixedBoundary}\r\n`;
|
|
79
|
+
mime += bodyPart;
|
|
80
|
+
// Attachments
|
|
81
|
+
for (const att of attachments) {
|
|
82
|
+
mime += `--${mixedBoundary}\r\n`;
|
|
83
|
+
mime += `Content-Type: ${att.contentType}; name="${encodeHeaderValue(att.filename)}"\r\n`;
|
|
84
|
+
mime += `Content-Disposition: attachment; filename="${encodeHeaderValue(att.filename)}"\r\n`;
|
|
85
|
+
if (att.contentId) {
|
|
86
|
+
mime += `Content-ID: <${encodeHeaderValue(att.contentId)}>\r\n`;
|
|
87
|
+
}
|
|
88
|
+
mime += `Content-Transfer-Encoding: base64\r\n\r\n`;
|
|
89
|
+
mime += `${foldBase64(att.contentBase64)}\r\n`;
|
|
90
|
+
}
|
|
91
|
+
mime += `--${mixedBoundary}--\r\n`;
|
|
92
|
+
return mime;
|
|
93
|
+
}
|