Răsfoiți Sursa

Add draft creation, reply, and forward functionality

New features:
- create-draft: Save email to Drafts folder without sending
- reply-to-message: Reply to a message (with optional reply-all)
- forward-message: Forward a message to new recipients

Both reply and forward support:
- send=true (default): Send immediately
- send=false: Save as draft for review

All tools support specifying the sender account via the 'account' parameter.

Tested:
- Created draft successfully (appears in Drafts folder)
- Sent email from rob@superiortech.io to robert.b.sweet@gmail.com
- All 28 unit tests pass
Robert Sweet 5 luni în urmă
părinte
comite
1f185f6428
2 a modificat fișierele cu 237 adăugiri și 0 ștergeri
  1. 67 0
      src/index.ts
  2. 170 0
      src/services/appleMailManager.ts

+ 67 - 0
src/index.ts

@@ -186,6 +186,73 @@ server.tool(
   }, "Error sending email")
 );
 
+// --- create-draft ---
+
+server.tool(
+  "create-draft",
+  {
+    to: z.array(z.string()).min(1, "At least one recipient is required"),
+    subject: z.string().min(1, "Subject is required"),
+    body: z.string().min(1, "Body is required"),
+    cc: z.array(z.string()).optional().describe("CC recipients"),
+    bcc: z.array(z.string()).optional().describe("BCC recipients"),
+    account: z.string().optional().describe("Account to create draft in"),
+  },
+  withErrorHandling(({ to, subject, body, cc, bcc, account }) => {
+    const success = mailManager.createDraft(to, subject, body, cc, bcc, account);
+
+    if (!success) {
+      return errorResponse("Failed to create draft. Check Mail.app configuration.");
+    }
+
+    return successResponse(`Draft created for ${to.join(", ")}`);
+  }, "Error creating draft")
+);
+
+// --- reply-to-message ---
+
+server.tool(
+  "reply-to-message",
+  {
+    id: z.string().min(1, "Message ID is required"),
+    body: z.string().min(1, "Reply body is required"),
+    replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
+    send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
+  },
+  withErrorHandling(({ id, body, replyAll, send }) => {
+    const success = mailManager.replyToMessage(id, body, replyAll, send);
+
+    if (!success) {
+      return errorResponse(`Failed to reply to message "${id}"`);
+    }
+
+    return successResponse(send ? "Reply sent" : "Reply saved as draft");
+  }, "Error replying to message")
+);
+
+// --- forward-message ---
+
+server.tool(
+  "forward-message",
+  {
+    id: z.string().min(1, "Message ID is required"),
+    to: z.array(z.string()).min(1, "At least one recipient is required"),
+    body: z.string().optional().describe("Optional message to prepend"),
+    send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
+  },
+  withErrorHandling(({ id, to, body, send }) => {
+    const success = mailManager.forwardMessage(id, to, body, send);
+
+    if (!success) {
+      return errorResponse(`Failed to forward message "${id}"`);
+    }
+
+    return successResponse(
+      send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft"
+    );
+  }, "Error forwarding message")
+);
+
 // --- mark-as-read ---
 
 server.tool(

+ 170 - 0
src/services/appleMailManager.ts

@@ -435,6 +435,176 @@ export class AppleMailManager {
     return result.output.includes("sent");
   }
 
+  /**
+   * Create a draft email (saved to Drafts folder, not sent).
+   *
+   * @param to - Recipient email addresses
+   * @param subject - Email subject
+   * @param body - Email body (plain text)
+   * @param cc - CC recipients
+   * @param bcc - BCC recipients
+   * @param account - Account to create draft in
+   * @returns true if draft created successfully
+   */
+  createDraft(
+    to: string[],
+    subject: string,
+    body: string,
+    cc?: string[],
+    bcc?: string[],
+    account?: string
+  ): boolean {
+    const safeSubject = escapeForAppleScript(subject);
+    const safeBody = escapeForAppleScript(body);
+
+    // Build recipient additions
+    let recipientCommands = "";
+    for (const addr of to) {
+      recipientCommands += `make new to recipient at end of to recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+    }
+    if (cc) {
+      for (const addr of cc) {
+        recipientCommands += `make new cc recipient at end of cc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+      }
+    }
+    if (bcc) {
+      for (const addr of bcc) {
+        recipientCommands += `make new bcc recipient at end of bcc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+      }
+    }
+
+    let draftCommand: string;
+    if (account) {
+      const safeAccount = escapeForAppleScript(account);
+      draftCommand = `
+        set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:false}
+        tell newMessage
+          ${recipientCommands}
+          set sender to "${safeAccount}"
+        end tell
+        return "draft created"
+      `;
+    } else {
+      draftCommand = `
+        set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:false}
+        tell newMessage
+          ${recipientCommands}
+        end tell
+        return "draft created"
+      `;
+    }
+
+    const script = buildAppLevelScript(draftCommand);
+    const result = executeAppleScript(script);
+
+    if (!result.success) {
+      console.error(`Failed to create draft: ${result.error}`);
+      return false;
+    }
+
+    return result.output.includes("draft created");
+  }
+
+  /**
+   * Reply to a message.
+   *
+   * @param id - Message ID to reply to
+   * @param body - Reply body
+   * @param replyAll - If true, reply to all recipients
+   * @param send - If true, send immediately; if false, save as draft
+   * @returns true if reply created/sent successfully
+   */
+  replyToMessage(id: string, body: string, replyAll = false, send = true): boolean {
+    const safeBody = escapeForAppleScript(body);
+    const replyType = replyAll
+      ? "reply with opening window to msg with reply to all"
+      : "reply with opening window to msg";
+    const sendAction = send ? "send theReply" : "";
+
+    const script = buildAppLevelScript(`
+      try
+        repeat with acct in accounts
+          repeat with mb in mailboxes of acct
+            try
+              set matchingMsgs to (messages of mb whose id is ${id})
+              if (count of matchingMsgs) > 0 then
+                set msg to item 1 of matchingMsgs
+                set theReply to ${replyType}
+                set content of theReply to "${safeBody}" & return & return & content of theReply
+                ${sendAction}
+                return "ok"
+              end if
+            end try
+          end repeat
+        end repeat
+        return "error:Message not found"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script, { timeoutMs: 60000 });
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to reply to message: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Forward a message.
+   *
+   * @param id - Message ID to forward
+   * @param to - Recipients to forward to
+   * @param body - Optional body to prepend
+   * @param send - If true, send immediately; if false, save as draft
+   * @returns true if forward created/sent successfully
+   */
+  forwardMessage(id: string, to: string[], body?: string, send = true): boolean {
+    const safeBody = body ? escapeForAppleScript(body) : "";
+    const sendAction = send ? "send theForward" : "";
+
+    // Build recipient additions
+    let recipientCommands = "";
+    for (const addr of to) {
+      recipientCommands += `make new to recipient at end of to recipients of theForward with properties {address:"${escapeForAppleScript(addr)}"}\n`;
+    }
+
+    const script = buildAppLevelScript(`
+      try
+        repeat with acct in accounts
+          repeat with mb in mailboxes of acct
+            try
+              set matchingMsgs to (messages of mb whose id is ${id})
+              if (count of matchingMsgs) > 0 then
+                set msg to item 1 of matchingMsgs
+                set theForward to forward msg with opening window
+                ${recipientCommands}
+                ${safeBody ? `set content of theForward to "${safeBody}" & return & return & content of theForward` : ""}
+                ${sendAction}
+                return "ok"
+              end if
+            end try
+          end repeat
+        end repeat
+        return "error:Message not found"
+      on error errMsg
+        return "error:" & errMsg
+      end try
+    `);
+
+    const result = executeAppleScript(script, { timeoutMs: 60000 });
+
+    if (!result.success || result.output.startsWith("error:")) {
+      console.error(`Failed to forward message: ${result.error || result.output}`);
+      return false;
+    }
+
+    return true;
+  }
+
   /**
    * Helper to find and operate on a message by ID.
    */