@credal/actions 0.2.75 → 0.2.76

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.
@@ -1,4 +1,4 @@
1
- export interface GmailMessage {
1
+ interface GmailMessage {
2
2
  payload: {
3
3
  mimeType: string;
4
4
  body?: {
@@ -6,22 +6,22 @@ export interface GmailMessage {
6
6
  size: number;
7
7
  };
8
8
  parts?: Array<{
9
- partId: string;
10
9
  mimeType: string;
11
- body: {
10
+ body?: {
12
11
  data?: string;
13
12
  size: number;
14
13
  };
15
- parts?: Array<{
16
- partId: string;
17
- mimeType: string;
18
- body: {
19
- data?: string;
20
- size: number;
21
- };
22
- }>;
14
+ parts?: GmailMessagePart[];
23
15
  }>;
24
16
  };
25
17
  }
26
- export declare function decodeGmailBase64(base64String: string): string;
18
+ interface GmailMessagePart {
19
+ mimeType: string;
20
+ body?: {
21
+ data?: string;
22
+ size: number;
23
+ };
24
+ parts?: GmailMessagePart[];
25
+ }
27
26
  export declare function getEmailContent(message: GmailMessage): string | null;
27
+ export {};
@@ -1,37 +1,53 @@
1
- export function decodeGmailBase64(base64String) {
2
- // Gmail API uses URL-safe base64 encoding
3
- const standardBase64 = base64String.replace(/-/g, "+").replace(/_/g, "/");
4
- // Add padding if needed
5
- const padded = standardBase64.padEnd(standardBase64.length + ((4 - (standardBase64.length % 4)) % 4), "=");
6
- // Only works for Node.js environment
7
- return Buffer.from(padded, "base64").toString("utf-8");
8
- }
1
+ import { convert } from "html-to-text";
9
2
  export function getEmailContent(message) {
10
3
  var _a;
11
- let textContent = null;
12
- // Function to recursively search for plain text content in parts
13
- function searchParts(parts) {
14
- var _a;
15
- if (!parts)
16
- return;
17
- for (const part of parts) {
18
- if (part.mimeType === "text/plain" && ((_a = part.body) === null || _a === void 0 ? void 0 : _a.data) && !textContent) {
19
- textContent = decodeGmailBase64(part.body.data);
20
- }
21
- else if (part.parts) {
22
- // Recursively search nested parts
23
- searchParts(part.parts);
24
- }
25
- }
4
+ const { mimeType, body, parts } = message.payload;
5
+ if (mimeType === "text/plain" && (body === null || body === void 0 ? void 0 : body.data)) {
6
+ return tryDecode(body.data);
7
+ }
8
+ if (mimeType === "text/html" && (body === null || body === void 0 ? void 0 : body.data)) {
9
+ const htmlRaw = tryDecode(body.data);
10
+ if (htmlRaw)
11
+ return convert(htmlRaw, { wordwrap: false });
12
+ }
13
+ const { plainText, htmlText } = searchParts(parts);
14
+ return (_a = plainText !== null && plainText !== void 0 ? plainText : htmlText) !== null && _a !== void 0 ? _a : null;
15
+ }
16
+ function tryDecode(data) {
17
+ if (!data)
18
+ return null;
19
+ try {
20
+ const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
21
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
22
+ return Buffer.from(padded, "base64").toString("utf-8");
26
23
  }
27
- // 1. Check if content is directly in the payload body (simple emails)
28
- if (((_a = message.payload.body) === null || _a === void 0 ? void 0 : _a.data) && message.payload.mimeType === "text/plain") {
29
- return decodeGmailBase64(message.payload.body.data);
24
+ catch (_a) {
25
+ return null;
30
26
  }
31
- // 2. Search through parts for plain text content
32
- if (message.payload.parts) {
33
- searchParts(message.payload.parts);
27
+ }
28
+ function searchParts(parts) {
29
+ let plainText = null;
30
+ let htmlText = null;
31
+ if (!parts)
32
+ return { plainText, htmlText };
33
+ for (const part of parts) {
34
+ const { mimeType, body, parts: subParts } = part;
35
+ if (mimeType === "text/plain" && !plainText) {
36
+ plainText = tryDecode(body === null || body === void 0 ? void 0 : body.data);
37
+ }
38
+ else if (mimeType === "text/html" && !htmlText) {
39
+ const htmlRaw = tryDecode(body === null || body === void 0 ? void 0 : body.data);
40
+ if (htmlRaw) {
41
+ htmlText = convert(htmlRaw, { wordwrap: false });
42
+ }
43
+ }
44
+ if (subParts === null || subParts === void 0 ? void 0 : subParts.length) {
45
+ const result = searchParts(subParts);
46
+ if (!plainText && result.plainText)
47
+ plainText = result.plainText;
48
+ if (!htmlText && result.htmlText)
49
+ htmlText = result.htmlText;
50
+ }
34
51
  }
35
- // 3. Return plain text content or null
36
- return textContent;
52
+ return { plainText, htmlText };
37
53
  }
@@ -7,68 +7,87 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import { RateLimiter } from "limiter";
10
11
  import { axiosClient } from "../../util/axiosClient.js";
11
12
  import { MISSING_AUTH_TOKEN } from "../../util/missingAuthConstants.js";
12
13
  import { getEmailContent } from "../google-oauth/utils/decodeMessage.js";
14
+ const MAX_EMAIL_CONTENTS_FETCHED = 50;
15
+ const DEFAULT_EMAIL_CONTENTS_FETCHED = 25;
16
+ const MAX_RESULTS_PER_REQUEST = 100;
17
+ const MAX_EMAILS_FETCHED_CONCURRENTLY = 5;
18
+ const limiter = new RateLimiter({ tokensPerInterval: MAX_EMAILS_FETCHED_CONCURRENTLY, interval: "second" });
19
+ function cleanAndTruncateEmail(text, maxLength = 2000) {
20
+ if (!text)
21
+ return "";
22
+ // Remove quoted replies (naive)
23
+ text = text.replace(/^>.*$/gm, "");
24
+ // Remove signatures
25
+ const signatureMarkers = ["\nBest", "\nRegards", "\nThanks", "\nSincerely"];
26
+ for (const marker of signatureMarkers) {
27
+ const index = text.indexOf(marker);
28
+ if (index !== -1) {
29
+ text = text.slice(0, index).trim();
30
+ break;
31
+ }
32
+ }
33
+ // Normalize whitespace
34
+ text = text
35
+ .replace(/\r\n|\r/g, "\n")
36
+ .replace(/\n{3,}/g, "\n\n")
37
+ .trim();
38
+ return text.slice(0, maxLength).trim();
39
+ }
13
40
  const searchGmailMessages = (_a) => __awaiter(void 0, [_a], void 0, function* ({ params, authParams, }) {
14
41
  if (!authParams.authToken) {
15
42
  return { success: false, error: MISSING_AUTH_TOKEN, messages: [] };
16
43
  }
17
44
  const { query, maxResults } = params;
45
+ const max = Math.min(maxResults !== null && maxResults !== void 0 ? maxResults : DEFAULT_EMAIL_CONTENTS_FETCHED, MAX_EMAIL_CONTENTS_FETCHED);
18
46
  const allMessages = [];
19
- const max = maxResults !== null && maxResults !== void 0 ? maxResults : 100;
20
47
  const errorMessages = [];
21
- let pageToken = undefined;
48
+ let pageToken;
22
49
  let fetched = 0;
23
50
  try {
24
51
  while (fetched < max) {
25
52
  const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages?q=${encodeURIComponent(query)}` +
26
53
  (pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : "") +
27
- `&maxResults=${Math.min(100, max - fetched)}`;
54
+ `&maxResults=${Math.min(MAX_RESULTS_PER_REQUEST, max - fetched)}`;
28
55
  const listRes = yield axiosClient.get(url, {
29
- headers: {
30
- Authorization: `Bearer ${authParams.authToken}`,
31
- },
56
+ headers: { Authorization: `Bearer ${authParams.authToken}` },
32
57
  });
33
58
  const { messages: messageList = [], nextPageToken } = listRes.data;
34
59
  if (!Array.isArray(messageList) || messageList.length === 0)
35
60
  break;
36
- const remaining = max - allMessages.length;
37
- const batch = messageList.slice(0, remaining);
61
+ const batch = messageList.slice(0, max - allMessages.length);
38
62
  const results = yield Promise.all(batch.map((msg) => __awaiter(void 0, void 0, void 0, function* () {
39
63
  try {
64
+ yield limiter.removeTokens(1);
40
65
  const msgRes = yield axiosClient.get(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${msg.id}?format=full`, {
41
- headers: {
42
- Authorization: `Bearer ${authParams.authToken}`,
43
- },
66
+ headers: { Authorization: `Bearer ${authParams.authToken}` },
67
+ validateStatus: () => true,
44
68
  });
45
69
  const { id, threadId, snippet, labelIds, internalDate } = msgRes.data;
46
- const emailBody = getEmailContent(msgRes.data) || "";
47
- return {
48
- id,
49
- threadId,
50
- snippet,
51
- labelIds,
52
- internalDate,
53
- emailBody,
54
- };
70
+ const rawBody = getEmailContent(msgRes.data) || "";
71
+ const emailBody = cleanAndTruncateEmail(rawBody);
72
+ return { id, threadId, snippet, labelIds, internalDate, emailBody };
55
73
  }
56
74
  catch (err) {
57
- errorMessages.push(err instanceof Error ? err.message : "Failed to fetch message details");
75
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch message details";
76
+ errorMessages.push(errorMessage);
58
77
  return {
59
78
  id: msg.id,
60
79
  threadId: "",
61
80
  snippet: "",
62
81
  labelIds: [],
63
82
  internalDate: "",
64
- payload: {},
65
- error: err instanceof Error ? err.message : "Failed to fetch message details",
83
+ emailBody: "",
84
+ error: errorMessage,
66
85
  };
67
86
  }
68
87
  })));
69
88
  allMessages.push(...results);
70
89
  fetched = allMessages.length;
71
- if (!nextPageToken || allMessages.length >= max)
90
+ if (!nextPageToken || fetched >= max)
72
91
  break;
73
92
  pageToken = nextPageToken;
74
93
  }
@@ -78,10 +97,10 @@ const searchGmailMessages = (_a) => __awaiter(void 0, [_a], void 0, function* ({
78
97
  error: errorMessages.join("; "),
79
98
  };
80
99
  }
81
- catch (error) {
100
+ catch (err) {
82
101
  return {
83
102
  success: false,
84
- error: error instanceof Error ? error.message : "Unknown error searching Gmail",
103
+ error: err instanceof Error ? err.message : "Unknown error searching Gmail",
85
104
  messages: [],
86
105
  };
87
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@credal/actions",
3
- "version": "0.2.75",
3
+ "version": "0.2.76",
4
4
  "type": "module",
5
5
  "description": "AI Actions by Credal AI",
6
6
  "sideEffects": false,
@@ -28,6 +28,7 @@
28
28
  "devDependencies": {
29
29
  "@eslint/js": "^9.16.0",
30
30
  "@jest/globals": "^29.7.0",
31
+ "@types/html-to-text": "^9.0.4",
31
32
  "@types/js-yaml": "^4.0.9",
32
33
  "@types/jsonwebtoken": "^9.0.9",
33
34
  "@types/node": "^22.10.1",
@@ -58,8 +59,10 @@
58
59
  "date-fns": "^4.1.0",
59
60
  "docx": "^9.3.0",
60
61
  "dotenv": "^16.4.7",
62
+ "html-to-text": "^9.0.5",
61
63
  "json-schema-to-zod": "^2.5.0",
62
64
  "jsonwebtoken": "^9.0.2",
65
+ "limiter": "^3.0.0",
63
66
  "mammoth": "^1.4.27",
64
67
  "mongodb": "^6.13.1",
65
68
  "node-forge": "^1.3.1",