Răsfoiți Sursa

feat: add send-serial-email tool for mail merge

Add a new send-serial-email tool that sends individual personalized
emails to a list of recipients. Supports {{placeholder}} tokens in
subject and body that are replaced with per-recipient variable values.

Includes a configurable delay between sends to avoid overwhelming
Mail.app's AppleScript bridge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Michael Henze 3 luni în urmă
părinte
comite
0c1f2ef1be
3 a modificat fișierele cu 148 adăugiri și 0 ștergeri
  1. 48 0
      src/index.ts
  2. 71 0
      src/services/appleMailManager.ts
  3. 29 0
      src/types.ts

+ 48 - 0
src/index.ts

@@ -208,6 +208,54 @@ server.tool(
   }, "Error sending email")
 );
 
+// --- send-serial-email ---
+
+server.tool(
+  "send-serial-email",
+  {
+    recipients: z
+      .array(
+        z.object({
+          email: z.string().min(1, "Recipient email is required"),
+          variables: z
+            .record(z.string())
+            .describe("Placeholder values, e.g. { Name: 'Alice', Company: 'Acme' }"),
+        })
+      )
+      .min(1, "At least one recipient is required")
+      .describe("List of recipients with personalization variables"),
+    subject: z
+      .string()
+      .min(1, "Subject is required")
+      .describe("Subject line — use {{Key}} for placeholders"),
+    body: z
+      .string()
+      .min(1, "Body is required")
+      .describe("Email body — use {{Key}} for placeholders"),
+    account: z.string().optional().describe("Account to send from"),
+    delayMs: z.number().optional().describe("Delay between sends in ms (default: 500)"),
+  },
+  withErrorHandling(({ recipients, subject, body, account, delayMs }) => {
+    const results = mailManager.sendSerialEmail(recipients, subject, body, account, delayMs);
+    const successCount = results.filter((r) => r.success).length;
+    const failCount = results.length - successCount;
+
+    const details = results
+      .map((r) => `  - ${r.email}: ${r.success ? "sent" : `FAILED (${r.error})`}`)
+      .join("\n");
+
+    if (failCount === 0) {
+      return successResponse(`Successfully sent ${successCount} email(s):\n${details}`);
+    } else if (successCount === 0) {
+      return errorResponse(`Failed to send all ${failCount} email(s):\n${details}`);
+    } else {
+      return successResponse(
+        `Sent ${successCount} of ${results.length} email(s), ${failCount} failed:\n${details}`
+      );
+    }
+  }, "Error sending serial emails")
+);
+
 // --- create-draft ---
 
 server.tool(

+ 71 - 0
src/services/appleMailManager.ts

@@ -31,6 +31,8 @@ import type {
   MailRule,
   Contact,
   EmailTemplate,
+  SerialEmailRecipient,
+  SerialEmailResult,
 } from "@/types.js";
 
 // =============================================================================
@@ -704,6 +706,75 @@ export class AppleMailManager {
     return result.output.includes("sent");
   }
 
+  /**
+   * Send individual personalized emails to a list of recipients (mail merge).
+   *
+   * Replaces {{placeholder}} tokens in subject and body with per-recipient values.
+   * Each recipient receives their own individual email.
+   *
+   * @param recipients - List of recipient objects with email and variable values
+   * @param subject - Email subject (may contain {{placeholders}})
+   * @param body - Email body (may contain {{placeholders}})
+   * @param account - Account to send from
+   * @param attachments - Optional file paths to attach to each email
+   * @param delayMs - Delay between sends in milliseconds (default: 500)
+   * @returns Array of per-recipient results
+   */
+  sendSerialEmail(
+    recipients: SerialEmailRecipient[],
+    subject: string,
+    body: string,
+    account?: string,
+    delayMs: number = 500
+  ): SerialEmailResult[] {
+    const results: SerialEmailResult[] = [];
+
+    for (const recipient of recipients) {
+      try {
+        // Replace all {{Key}} placeholders with recipient's values
+        let personalizedSubject = subject;
+        let personalizedBody = body;
+        for (const [key, value] of Object.entries(recipient.variables)) {
+          const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+          const placeholder = new RegExp(`\\{\\{${safeKey}\\}\\}`, "g");
+          personalizedSubject = personalizedSubject.replace(placeholder, value);
+          personalizedBody = personalizedBody.replace(placeholder, value);
+        }
+
+        const success = this.sendEmail(
+          [recipient.email],
+          personalizedSubject,
+          personalizedBody,
+          undefined,
+          undefined,
+          account
+        );
+
+        results.push({
+          email: recipient.email,
+          success,
+          error: success ? undefined : "Failed to send email",
+        });
+      } catch (error) {
+        results.push({
+          email: recipient.email,
+          success: false,
+          error: error instanceof Error ? error.message : "Unknown error",
+        });
+      }
+
+      // Brief delay between sends to avoid overwhelming Mail.app
+      if (delayMs > 0 && recipients.indexOf(recipient) < recipients.length - 1) {
+        const start = Date.now();
+        while (Date.now() - start < delayMs) {
+          /* busy wait - sync context */
+        }
+      }
+    }
+
+    return results;
+  }
+
   /**
    * Create a draft email (saved to Drafts folder, not sent).
    *

+ 29 - 0
src/types.ts

@@ -286,6 +286,35 @@ export interface MoveMessageParams {
   account?: string;
 }
 
+// =============================================================================
+// Serial Email (Mail Merge)
+// =============================================================================
+
+/**
+ * A single recipient in a serial email (mail merge) operation.
+ */
+export interface SerialEmailRecipient {
+  /** Recipient email address */
+  email: string;
+
+  /** Variable values for placeholder replacement (e.g., { Name: "Alice", Company: "Acme" }) */
+  variables: Record<string, string>;
+}
+
+/**
+ * Result of sending a serial email to a single recipient.
+ */
+export interface SerialEmailResult {
+  /** Recipient email address */
+  email: string;
+
+  /** Whether the email was sent successfully */
+  success: boolean;
+
+  /** Error message if sending failed */
+  error?: string;
+}
+
 // =============================================================================
 // Health Check
 // =============================================================================