diff --git a/ews_client.ts b/client/ews_client.ts similarity index 59% rename from ews_client.ts rename to client/ews_client.ts index 75fb0f1..d3f96ae 100644 --- a/ews_client.ts +++ b/client/ews_client.ts @@ -1,36 +1,19 @@ import { XMLParser } from "fast-xml-parser"; -import fs from "node:fs"; import https from "node:https"; import { createRequire } from "node:module"; -import path from "node:path"; import { promisify } from "node:util"; -import type { Email } from "./types/email.ts"; -import type { Folder } from "./types/folder.ts"; -import type { LoginConfig } from "./types/login_config.ts"; - -let inMemoryPassword: string | null = null; -let dataDir: string = "./data"; - -export function initDataDir(dir: string): void { - dataDir = path.resolve(dir); - fs.mkdirSync(dataDir, { recursive: true }); -} - -export function setPassword(password: string): void { - inMemoryPassword = password; -} - -export function getPassword(): string | null { - return inMemoryPassword; -} - -export function hasPassword(): boolean { - return inMemoryPassword !== null; -} - -export function clearPassword(): void { - inMemoryPassword = null; -} +import { extractEmail, type Email } from "../utils/extract_email.ts"; +import { extractFolders, type Folder } from "../utils/extract_folder.ts"; +import { + loadConfig, + saveConfig, + clearConfig, + setPassword, + getPassword, + hasPassword, + initDataDir, + type LoginConfig, +} from "../utils/config.ts"; const ntlm: any = createRequire(import.meta.url)("httpntlm"); const postAsync = promisify(ntlm.post.bind(ntlm)); @@ -43,13 +26,6 @@ const AGENT = new https.Agent({ secureOptions: SSL_OP_LEGACY_SERVER_CONNECT, }); -const PARSER = new XMLParser({ - ignoreAttributes: false, - attributeNamePrefix: "@_", - removeNSPrefix: true, - textNodeName: "#text", -}); - const DISTINGUISHED_FOLDERS: Record = { inbox: "inbox", "входящие": "inbox", @@ -78,30 +54,12 @@ ${body} `; } -function configFilePath(): string { - return path.join(dataDir, "config.json"); -} - -export function loadConfig(): LoginConfig { - const filePath = configFilePath(); - if (!fs.existsSync(filePath)) { - throw new Error("Not logged in. Use the login tool first."); - } - return JSON.parse(fs.readFileSync(filePath, "utf-8")) as LoginConfig; -} - -export function saveConfig(config: LoginConfig): void { - const filePath = configFilePath(); - fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8"); -} - -export function clearConfig(): void { - const filePath = configFilePath(); - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - clearPassword(); -} +const PARSER = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", + removeNSPrefix: true, + textNodeName: "#text", +}); export class EwsClient { private config: LoginConfig; @@ -110,10 +68,7 @@ export class EwsClient { constructor(config: LoginConfig, password?: string) { this.config = config; this.ewsUrl = `${config.serverUrl.replace(/\/+$/, "")}/EWS/Exchange.asmx`; - - if (password) { - setPassword(password); - } + if (password) setPassword(password); } private get domain(): string { @@ -144,8 +99,7 @@ export class EwsClient { throw new Error(`NTLM request failed, status=${res.statusCode}`); } - const parsed = PARSER.parse(res.body); - return parsed; + return PARSER.parse(res.body); } private extractResponseMessages(data: any): any[] { @@ -176,7 +130,7 @@ export class EwsClient { async getFolderId(folderName: string): Promise { const lower = folderName.toLowerCase(); - const distinguished = DISTINGUISHED_FOLDERS[lower]; + const distinguished = (DISTINGUISHED_FOLDERS as Record)[lower]; if (distinguished) { const soap = buildSoapEnvelope(`\ @@ -251,7 +205,7 @@ export class EwsClient { } = options; const isDistinguished = - DISTINGUISHED_FOLDERS[folderId.toLowerCase()] !== undefined; + (DISTINGUISHED_FOLDERS as Record)[folderId.toLowerCase()] !== undefined; const folderIdXml = isDistinguished ? `` @@ -331,198 +285,17 @@ ${restriction} parentFolderId: string, recursive: boolean = false, ): Promise { - const traversal = recursive ? "Deep" : "Shallow"; - const isDistinguished = - DISTINGUISHED_FOLDERS[parentFolderId.toLowerCase()] !== undefined - || ["msgfolderroot"].includes(parentFolderId.toLowerCase()); - - const folderIdXml = isDistinguished - ? `` - : ``; - - const soap = buildSoapEnvelope(`\ - - - Default - - - ${folderIdXml} - - - `); - - const data = await this.soapRequest( - soap, - "http://schemas.microsoft.com/exchange/services/2006/messages/FindFolder", + return extractFolders( + (body, soapAction) => this.soapRequest(body, soapAction), + parentFolderId, + recursive, ); - - const folders: Folder[] = []; - - for (const msg of this.extractResponseMessages(data)) { - const folderList = msg?.RootFolder?.Folders?.Folder; - if (!folderList) continue; - const list = Array.isArray(folderList) ? folderList : [folderList]; - - for (const f of list) { - folders.push({ - name: f.DisplayName ?? "Unknown", - id: f.FolderId?.["@_Id"] ?? "", - totalCount: f.TotalCount ?? 0, - unreadCount: f.UnreadCount ?? 0, - childFolderCount: f.ChildFolderCount ?? 0, - }); - } - } - - return folders; } extractEmail(item: any): Email { - const itemType = item["@_xsi_type"] ?? item.__type ?? ""; - const isMeeting = - /MeetingRequest|MeetingResponse|MeetingCancellation|CalendarItem/i.test( - itemType, - ); - - const fromMailbox = item.From?.Mailbox - ?? item.Organizer?.Mailbox - ?? item.Sender?.Mailbox - ?? {}; - - const email: Email = { - subject: item.Subject ?? "(No subject)", - from: fromMailbox.EmailAddress ?? "", - fromName: fromMailbox.Name ?? "", - date: item.DateTimeSent ?? item.DateTimeReceived ?? item.DateTimeCreated - ?? "", - isRead: item.IsRead === "true" || item.IsRead === true, - hasAttachments: item.HasAttachments === "true" - || item.HasAttachments === true, - hasLinks: false, - itemId: item.ItemId?.["@_Id"] ?? "", - size: item.Size ? Number(item.Size) : 0, - isMeeting, - itemType: itemType || "Message", - to: [], - cc: [], - body: "", - bodyType: "Text", - attachments: [], - preview: item.Preview ?? "", - }; - - if (item.DisplayTo) { - email.to = item.DisplayTo.split(";").map((t: string) => t.trim()).filter( - Boolean, - ); - } - if (item.DisplayCc) { - email.cc = item.DisplayCc.split(";").map((c: string) => c.trim()).filter( - Boolean, - ); - } - - if (isMeeting) { - email.location = item.Location ?? ""; - email.start = item.Start ?? item.StartWallClock ?? item.ReminderDueBy - ?? ""; - email.end = item.End ?? item.EndWallClock ?? ""; - } - - const bodyVal = item.Body?.Value ?? item.Body?.["#text"] ?? ""; - const bodyType = item.Body?.["@_BodyType"] ?? item.Body?.BodyType ?? "Text"; - - if (bodyType === "HTML") { - email.hasLinks = / { - const name = r.Name ?? ""; - const addr = r.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - - const ccRecipients = item.CcRecipients?.Mailbox ?? []; - email.cc = (Array.isArray(ccRecipients) ? ccRecipients : [ccRecipients]) - .map((r: any) => { - const name = r.Name ?? ""; - const addr = r.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - - const attachments = item.Attachments?.FileAttachment ?? []; - const attList = Array.isArray(attachments) ? attachments : [attachments]; - email.attachments = attList - .filter((a: any) => a) - .map((a: any) => ({ - name: a.Name ?? "", - size: a.Size ? Number(a.Size) : 0, - contentType: a.ContentType ?? "", - attachmentId: a.AttachmentId?.["@_Id"] ?? "", - isInline: a.IsInline === "true" || a.IsInline === true, - })); - - if (isMeeting) { - email.location = item.Location ?? item.EnhancedLocation?.DisplayName - ?? ""; - email.start = item.Start ?? ""; - email.end = item.End ?? ""; - - const reqAtt = item.RequiredAttendees?.Attendee ?? []; - email.requiredAttendees = (Array.isArray(reqAtt) ? reqAtt : [reqAtt]) - .map((a: any) => { - const mb = a.Mailbox ?? {}; - const name = mb.Name ?? ""; - const addr = mb.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - - const optAtt = item.OptionalAttendees?.Attendee ?? []; - email.optionalAttendees = (Array.isArray(optAtt) ? optAtt : [optAtt]) - .map((a: any) => { - const mb = a.Mailbox ?? {}; - const name = mb.Name ?? ""; - const addr = mb.EmailAddress ?? ""; - return name && addr ? `${name} <${addr}>` : addr; - }) - .filter(Boolean); - } - - return email; + return extractEmail(item); } - private htmlToText(html: string): string { - if (!html) return ""; - let text = html.replace(/]*>.*?<\/script>/gis, ""); - text = text.replace(/]*>.*?<\/style>/gis, ""); - text = text.replace(//gi, "\n"); - text = text.replace(/]*>/gi, "\n"); - text = text.replace(/<\/p>/gi, ""); - text = text.replace(/]*>/gi, "\n"); - text = text.replace(/<\/div>/gi, ""); - text = text.replace(/<[^>]+>/g, ""); - text = text.replace(/ /g, " "); - text = text.replace(/&/g, "&"); - text = text.replace(/</g, "<"); - text = text.replace(/>/g, ">"); - text = text.replace(/"/g, "\""); - text = text.replace(/'/g, "'"); - text = text.replace(/\n\s*\n/g, "\n\n"); - text = text.replace(/[ \t]+/g, " "); - return text.trim(); - } - - // ── UpdateItem (for marking read/unread, etc.) ────────────────────────── - async updateItems( itemIds: string[], updates: { fieldUri: string; value: string | boolean }[], @@ -572,8 +345,6 @@ ${changesXml} return this.extractResponseMessages(data); } - // ── GetAttachment ─────────────────────────────────────────────────────── - async getAttachment( attachmentId: string, ): Promise<{ content: Buffer; name: string; contentType: string }> { @@ -607,8 +378,6 @@ ${changesXml} throw new Error(`Attachment '${attachmentId}' not found`); } - // ── FindItem with CalendarView ────────────────────────────────────────── - async findCalendarItems( folderId: string, startDate: string, @@ -640,8 +409,6 @@ ${changesXml} return []; } - // ── GetUserAvailability ───────────────────────────────────────────────── - async getUserAvailability( emails: string[], startDate: string, @@ -689,8 +456,6 @@ ${mailboxDataXml} return data; } - // ── ResolveNames (directory search) ───────────────────────────────────── - async resolveNames( query: string, fullContact: boolean = true, @@ -717,3 +482,14 @@ ${mailboxDataXml} return []; } } + +// Re-export for backward compatibility +export { + initDataDir, + loadConfig, + saveConfig, + clearConfig, + setPassword, + getPassword, + hasPassword, +} from "../utils/config.ts"; diff --git a/main.ts b/main.ts index 3ac4048..7f54e75 100644 --- a/main.ts +++ b/main.ts @@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { Command } from "commander"; import crypto from "node:crypto"; import http from "node:http"; -import { initDataDir } from "./ews_client.ts"; +import { initDataDir } from "./client/ews_client.ts"; import { registerAuthTools } from "./tools/auth.ts"; import { registerAvailabilityTools } from "./tools/availability.ts"; import { registerCalendarTools } from "./tools/calendar.ts"; diff --git a/tools/auth.ts b/tools/auth.ts index a8c6308..b92abd8 100644 --- a/tools/auth.ts +++ b/tools/auth.ts @@ -7,8 +7,8 @@ import { loadConfig, saveConfig, setPassword, -} from "../ews_client.ts"; -import type { LoginConfig } from "../types/login_config.ts"; +} from "../client/ews_client.ts"; +import { type LoginConfig } from "../utils/config.ts"; export function registerAuthTools(server: McpServer): void { server.registerTool( diff --git a/tools/availability.ts b/tools/availability.ts index 2cd8a91..a164a16 100644 --- a/tools/availability.ts +++ b/tools/availability.ts @@ -1,6 +1,6 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerAvailabilityTools(server: McpServer): void { server.registerTool( diff --git a/tools/calendar.ts b/tools/calendar.ts index aecd30c..b881534 100644 --- a/tools/calendar.ts +++ b/tools/calendar.ts @@ -2,7 +2,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import fs from "node:fs"; import path from "node:path"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerCalendarTools(server: McpServer): void { server.registerTool( diff --git a/tools/email.ts b/tools/email.ts index 4b455a6..7926d60 100644 --- a/tools/email.ts +++ b/tools/email.ts @@ -2,7 +2,7 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import fs from "node:fs"; import path from "node:path"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; function getClient(): EwsClient { return new EwsClient(loadConfig()); diff --git a/tools/folders.ts b/tools/folders.ts index 539284b..103c425 100644 --- a/tools/folders.ts +++ b/tools/folders.ts @@ -1,6 +1,6 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerFolderTools(server: McpServer): void { server.registerTool( diff --git a/tools/people.ts b/tools/people.ts index ffdd2eb..946d632 100644 --- a/tools/people.ts +++ b/tools/people.ts @@ -1,6 +1,6 @@ import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod/v4"; -import { EwsClient, loadConfig } from "../ews_client.ts"; +import { EwsClient, loadConfig } from "../client/ews_client.ts"; export function registerPeopleTools(server: McpServer): void { server.registerTool( diff --git a/types/email.ts b/types/email.ts deleted file mode 100644 index 1ac1b24..0000000 --- a/types/email.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface Email { - subject: string; - from: string; - fromName: string; - date: string; - isRead: boolean; - hasAttachments: boolean; - hasLinks: boolean; - itemId: string; - size: number; - isMeeting: boolean; - itemType: string; - to: string[]; - cc: string[]; - body: string; - bodyType: string; - attachments: Attachment[]; - preview: string; - location?: string; - start?: string; - end?: string; - requiredAttendees?: string[]; - optionalAttendees?: string[]; -} - -export interface Attachment { - name: string; - size: number; - contentType: string; - attachmentId: string; - isInline: boolean; -} diff --git a/types/folder.ts b/types/folder.ts deleted file mode 100644 index 6bef6e0..0000000 --- a/types/folder.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Folder { - name: string; - id: string; - totalCount: number; - unreadCount: number; - childFolderCount: number; -} diff --git a/types/login_config.ts b/types/login_config.ts deleted file mode 100644 index ef2c777..0000000 --- a/types/login_config.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface LoginConfig { - serverUrl: string; - email: string; - username: string; - domain?: string; -} diff --git a/utils/config.ts b/utils/config.ts new file mode 100644 index 0000000..62dd440 --- /dev/null +++ b/utils/config.ts @@ -0,0 +1,59 @@ +export interface LoginConfig { + serverUrl: string; + email: string; + username: string; + domain?: string; +} + +import fs from "node:fs"; +import path from "node:path"; + +let dataDir: string = "./data"; + +export function initDataDir(dir: string): void { + dataDir = path.resolve(dir); + fs.mkdirSync(dataDir, { recursive: true }); +} + +let inMemoryPassword: string | null = null; + +export function setPassword(password: string): void { + inMemoryPassword = password; +} + +export function getPassword(): string | null { + return inMemoryPassword; +} + +export function hasPassword(): boolean { + return inMemoryPassword !== null; +} + +export function clearPassword(): void { + inMemoryPassword = null; +} + +function configFilePath(): string { + return path.join(dataDir, "config.json"); +} + +export function loadConfig(): LoginConfig { + const filePath = configFilePath(); + if (!fs.existsSync(filePath)) { + throw new Error("Not logged in. Use the login tool first."); + } + return JSON.parse(fs.readFileSync(filePath, "utf-8")) as LoginConfig; +} + +export function saveConfig(config: LoginConfig): void { + const filePath = configFilePath(); + fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8"); +} + +export function clearConfig(): void { + const filePath = configFilePath(); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + clearPassword(); +} diff --git a/utils/extract_email.ts b/utils/extract_email.ts new file mode 100644 index 0000000..45572f3 --- /dev/null +++ b/utils/extract_email.ts @@ -0,0 +1,156 @@ +import { htmlToText } from "./html_to_text.ts"; + +export interface Attachment { + name: string; + size: number; + contentType: string; + attachmentId: string; + isInline: boolean; +} + +export interface Email { + subject: string; + from: string; + fromName: string; + date: string; + isRead: boolean; + hasAttachments: boolean; + hasLinks: boolean; + itemId: string; + size: number; + isMeeting: boolean; + itemType: string; + to: string[]; + cc: string[]; + body: string; + bodyType: string; + attachments: Attachment[]; + preview: string; + location?: string; + start?: string; + end?: string; + requiredAttendees?: string[]; + optionalAttendees?: string[]; +} + +export function extractEmail(item: any): Email { + const itemType = item["@_xsi_type"] ?? item.__type ?? ""; + const isMeeting = + /MeetingRequest|MeetingResponse|MeetingCancellation|CalendarItem/i.test( + itemType, + ); + + const fromMailbox = item.From?.Mailbox + ?? item.Organizer?.Mailbox + ?? item.Sender?.Mailbox + ?? {}; + + const email: Email = { + subject: item.Subject ?? "(No subject)", + from: fromMailbox.EmailAddress ?? "", + fromName: fromMailbox.Name ?? "", + date: item.DateTimeSent ?? item.DateTimeReceived ?? item.DateTimeCreated + ?? "", + isRead: item.IsRead === "true" || item.IsRead === true, + hasAttachments: item.HasAttachments === "true" + || item.HasAttachments === true, + hasLinks: false, + itemId: item.ItemId?.["@_Id"] ?? "", + size: item.Size ? Number(item.Size) : 0, + isMeeting, + itemType: itemType || "Message", + to: [], + cc: [], + body: "", + bodyType: "Text", + attachments: [], + preview: item.Preview ?? "", + }; + + if (item.DisplayTo) { + email.to = item.DisplayTo.split(";").map((t: string) => t.trim()).filter( + Boolean, + ); + } + if (item.DisplayCc) { + email.cc = item.DisplayCc.split(";").map((c: string) => c.trim()).filter( + Boolean, + ); + } + + if (isMeeting) { + email.location = item.Location ?? ""; + email.start = item.Start ?? item.StartWallClock ?? item.ReminderDueBy + ?? ""; + email.end = item.End ?? item.EndWallClock ?? ""; + } + + const bodyVal = item.Body?.Value ?? item.Body?.["#text"] ?? ""; + const bodyType = item.Body?.["@_BodyType"] ?? item.Body?.BodyType ?? "Text"; + + if (bodyType === "HTML") { + email.hasLinks = / { + const name = r.Name ?? ""; + const addr = r.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + + const ccRecipients = item.CcRecipients?.Mailbox ?? []; + email.cc = (Array.isArray(ccRecipients) ? ccRecipients : [ccRecipients]) + .map((r: any) => { + const name = r.Name ?? ""; + const addr = r.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + + const attachments = item.Attachments?.FileAttachment ?? []; + const attList = Array.isArray(attachments) ? attachments : [attachments]; + email.attachments = attList + .filter((a: any) => a) + .map((a: any) => ({ + name: a.Name ?? "", + size: a.Size ? Number(a.Size) : 0, + contentType: a.ContentType ?? "", + attachmentId: a.AttachmentId?.["@_Id"] ?? "", + isInline: a.IsInline === "true" || a.IsInline === true, + })); + + if (isMeeting) { + email.location = item.Location ?? item.EnhancedLocation?.DisplayName ?? ""; + email.start = item.Start ?? ""; + email.end = item.End ?? ""; + + const reqAtt = item.RequiredAttendees?.Attendee ?? []; + email.requiredAttendees = (Array.isArray(reqAtt) ? reqAtt : [reqAtt]) + .map((a: any) => { + const mb = a.Mailbox ?? {}; + const name = mb.Name ?? ""; + const addr = mb.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + + const optAtt = item.OptionalAttendees?.Attendee ?? []; + email.optionalAttendees = (Array.isArray(optAtt) ? optAtt : [optAtt]) + .map((a: any) => { + const mb = a.Mailbox ?? {}; + const name = mb.Name ?? ""; + const addr = mb.EmailAddress ?? ""; + return name && addr ? `${name} <${addr}>` : addr; + }) + .filter(Boolean); + } + + return email; +} diff --git a/utils/extract_folder.ts b/utils/extract_folder.ts new file mode 100644 index 0000000..464e428 --- /dev/null +++ b/utils/extract_folder.ts @@ -0,0 +1,101 @@ +export interface Folder { + name: string; + id: string; + totalCount: number; + unreadCount: number; + childFolderCount: number; +} + +type SoapRequest = (body: string, soapAction: string) => Promise; + +export async function extractFolders( + soapRequest: SoapRequest, + parentFolderId: string, + recursive: boolean = false, +): Promise { + const DISTINGUISHED_FOLDERS: Record = { + inbox: "inbox", + "входящие": "inbox", + sent: "sentitems", + "отправленные": "sentitems", + drafts: "drafts", + "черновики": "drafts", + deleted: "deleteditems", + "удаленные": "deleteditems", + junk: "junkemail", + "нежелательная почта": "junkemail", + outbox: "outbox", + "исходящие": "outbox", + calendar: "calendar", + "календарь": "calendar", + }; + + function buildSoapEnvelope(body: string): string { + return ` + + +${body} + +`; + } + + function extractResponseMessages(data: any): any[] { + const body = data?.Envelope?.Body; + if (!body) return []; + const firstKey = Object.keys(body).find((k) => k.endsWith("Response")); + if (!firstKey) return []; + const rm = body[firstKey]?.ResponseMessages; + if (!rm) return []; + const msgKey = Object.keys(rm).find((k) => k.endsWith("ResponseMessage")); + if (!msgKey) return []; + const msgs = rm[msgKey]; + return Array.isArray(msgs) ? msgs : [msgs]; + } + + const traversal = recursive ? "Deep" : "Shallow"; + const isDistinguished = + DISTINGUISHED_FOLDERS[parentFolderId.toLowerCase()] !== undefined + || ["msgfolderroot"].includes(parentFolderId.toLowerCase()); + + const folderIdXml = isDistinguished + ? `` + : ``; + + const soap = buildSoapEnvelope(`\ + + + Default + + + ${folderIdXml} + + + `); + + const data = await soapRequest( + soap, + "http://schemas.microsoft.com/exchange/services/2006/messages/FindFolder", + ); + + const folders: Folder[] = []; + + for (const msg of extractResponseMessages(data)) { + const folderList = msg?.RootFolder?.Folders?.Folder; + if (!folderList) continue; + const list = Array.isArray(folderList) ? folderList : [folderList]; + + for (const f of list) { + folders.push({ + name: f.DisplayName ?? "Unknown", + id: f.FolderId?.["@_Id"] ?? "", + totalCount: f.TotalCount ?? 0, + unreadCount: f.UnreadCount ?? 0, + childFolderCount: f.ChildFolderCount ?? 0, + }); + } + } + + return folders; +} diff --git a/utils/html_to_text.ts b/utils/html_to_text.ts new file mode 100644 index 0000000..579520b --- /dev/null +++ b/utils/html_to_text.ts @@ -0,0 +1,20 @@ +export function htmlToText(html: string): string { + if (!html) return ""; + let text = html.replace(/]*>.*?<\/script>/gis, ""); + text = text.replace(/]*>.*?<\/style>/gis, ""); + text = text.replace(//gi, "\n"); + text = text.replace(/]*>/gi, "\n"); + text = text.replace(/<\/p>/gi, ""); + text = text.replace(/]*>/gi, "\n"); + text = text.replace(/<\/div>/gi, ""); + text = text.replace(/<[^>]+>/g, ""); + text = text.replace(/ /g, " "); + text = text.replace(/&/g, "&"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/"/g, "\""); + text = text.replace(/'/g, "'"); + text = text.replace(/\n\s*\n/g, "\n\n"); + text = text.replace(/[ \t]+/g, " "); + return text.trim(); +}