@clairejs/server 3.28.8 → 3.28.10

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Change Log
2
2
 
3
+ #### 3.28.10
4
+
5
+ - allow email service to send attachments
6
+
7
+ #### 3.28.9
8
+
9
+ - update core and allow query fields with nullable
10
+
3
11
  #### 3.28.8
4
12
 
5
13
  - fix CrudHttpController getBody not cache validation
@@ -64,7 +64,12 @@ export class ModelRepository extends AbstractRepository {
64
64
  }
65
65
  if (fieldMetadata.pk || fieldMetadata.fk || fieldMetadata.isSymbol) {
66
66
  //-- belongs to array of ids
67
- result.push({ _in: { [fieldName]: queryValue } });
67
+ if (queryValue === null) {
68
+ result.push({ _eq: { [fieldName]: null } });
69
+ }
70
+ else {
71
+ result.push({ _in: { [fieldName]: queryValue } });
72
+ }
68
73
  }
69
74
  else {
70
75
  if (fieldMetadata.enum) {
@@ -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
- //-- write email to local folder
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, Errors, LogContext } from "@clairejs/core";
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
- try {
25
- const param = {
26
- Destination: {
27
- BccAddresses: email.bcc,
28
- CcAddresses: email.cc,
29
- ToAddresses: email.receivers,
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
- if (email.contentType === "html") {
41
- param.Message.Body.Html = {
42
- Charset: "UTF-8",
43
- Data: email.content,
44
- };
45
- }
46
- else if (email.contentType === "text") {
47
- param.Message.Body.Text = {
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.content,
50
- };
51
- }
52
- await this.emailClient.sendEmail(param).promise();
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
- catch (err) {
55
- this.logger.error(err);
56
- throw Errors.SYSTEM_ERROR("Cannot send email");
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,4 @@
1
+ import { Attachment, EmailInfo } from "../AbstractMailService";
2
+ export type ParsedAttachment = Attachment;
3
+ export declare function parseAttachments(attachments?: Attachment[]): ParsedAttachment[];
4
+ export declare function buildMimeMessage(email: EmailInfo): string;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clairejs/server",
3
- "version": "3.28.8",
3
+ "version": "3.28.10",
4
4
  "description": "Claire server NodeJs framework written in Typescript.",
5
5
  "types": "dist/index.d.ts",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@clairejs/client": "^3.5.1",
38
- "@clairejs/core": "^3.9.0",
38
+ "@clairejs/core": "^3.9.4",
39
39
  "@clairejs/orm": "^3.17.2"
40
40
  },
41
41
  "devDependencies": {