"""Apple Mail integration utilities. Provides functions for opening emails in Apple Mail and optionally auto-sending them via AppleScript. """ import logging import os import subprocess import tempfile import time from typing import Optional, Tuple logger = logging.getLogger(__name__) def open_eml_in_apple_mail( email_content: str, auto_send: bool = False, subject: str = "", ) -> Tuple[bool, str]: """ Open an email in Apple Mail, optionally auto-sending it. Args: email_content: The raw email content (RFC 5322 format) auto_send: If True, automatically send the email after opening subject: Email subject for logging purposes Returns: Tuple of (success, message) """ try: # Create a temp .eml file with tempfile.NamedTemporaryFile( mode="w", suffix=".eml", delete=False, encoding="utf-8" ) as tmp: tmp.write(email_content) tmp_path = tmp.name logger.info(f"Created temp .eml file: {tmp_path}") # Open with Apple Mail result = subprocess.run( ["open", "-a", "Mail", tmp_path], capture_output=True, text=True ) if result.returncode != 0: logger.error(f"Failed to open Mail: {result.stderr}") return False, f"Failed to open Mail: {result.stderr}" if auto_send: # Wait for Mail to open the message time.sleep(1.5) # Use AppleScript to send the frontmost message success, message = _applescript_send_frontmost_message() if success: logger.info(f"Auto-sent email: {subject}") # Clean up temp file after sending try: os.unlink(tmp_path) except OSError: pass return True, "Email sent successfully" else: logger.warning( f"Auto-send failed, email opened for manual sending: {message}" ) return True, f"Email opened (auto-send failed: {message})" else: logger.info(f"Opened email in Mail for manual sending: {subject}") return True, "Email opened in Mail - please send manually" except Exception as e: logger.error(f"Error opening email in Apple Mail: {e}", exc_info=True) return False, f"Error: {str(e)}" def _applescript_send_frontmost_message() -> Tuple[bool, str]: """ Use AppleScript to send the frontmost message in Apple Mail. When an .eml file is opened, Mail shows it as a "view" not a compose window. We need to use Message > Send Again to convert it to a compose window, then send it. Returns: Tuple of (success, message) """ # AppleScript to: # 1. Activate Mail # 2. Use "Send Again" menu item to convert viewed message to compose # 3. Send the message with Cmd+Shift+D applescript = """ tell application "Mail" activate delay 0.3 end tell tell application "System Events" tell process "Mail" -- First, trigger "Send Again" from Message menu to convert to compose window -- Menu: Message > Send Again (Cmd+Shift+D also works for this in some contexts) try click menu item "Send Again" of menu "Message" of menu bar 1 delay 0.5 on error -- If Send Again fails, window might already be a compose window end try -- Now send the message with Cmd+Shift+D keystroke "d" using {command down, shift down} delay 0.3 return "sent" end tell end tell """ try: result = subprocess.run( ["osascript", "-e", applescript], capture_output=True, text=True, timeout=15 ) if result.returncode == 0: output = result.stdout.strip() if output == "sent": return True, "Message sent" else: return False, output else: return False, result.stderr.strip() except subprocess.TimeoutExpired: return False, "AppleScript timed out" except Exception as e: return False, str(e) def compose_new_email( to: str = "", subject: str = "", body: str = "", auto_send: bool = False, ) -> Tuple[bool, str]: """ Open a new compose window in Apple Mail using mailto: URL. Args: to: Recipient email address subject: Email subject body: Email body text auto_send: Ignored - no AppleScript automation for compose Returns: Tuple of (success, message) """ import urllib.parse try: # Build mailto: URL params = {} if subject: params["subject"] = subject if body: params["body"] = body query_string = urllib.parse.urlencode(params) mailto_url = f"mailto:{to}" if query_string: mailto_url += f"?{query_string}" # Open mailto: URL - this will open the default mail client result = subprocess.run(["open", mailto_url], capture_output=True, text=True) if result.returncode != 0: logger.error(f"Failed to open mailto: {result.stderr}") return False, f"Failed to open compose: {result.stderr}" return True, "Compose window opened" except Exception as e: logger.error(f"Error composing email: {e}", exc_info=True) return False, str(e) def reply_to_email( original_message_path: str, reply_all: bool = False, ) -> Tuple[bool, str]: """ Open an email in Apple Mail for the user to manually reply. This just opens the .eml file in Mail. The user can then use Mail's Reply button (Cmd+R) or Reply All (Cmd+Shift+R) themselves. Args: original_message_path: Path to the original .eml file reply_all: Ignored - user will manually choose reply type Returns: Tuple of (success, message) """ try: # Just open the message in Mail - no AppleScript automation result = subprocess.run( ["open", "-a", "Mail", original_message_path], capture_output=True, text=True, ) if result.returncode != 0: return False, f"Failed to open message: {result.stderr}" reply_type = "Reply All" if reply_all else "Reply" return ( True, f"Message opened - use {reply_type} (Cmd+{'Shift+' if reply_all else ''}R)", ) except Exception as e: logger.error(f"Error opening message for reply: {e}", exc_info=True) return False, str(e) def forward_email(original_message_path: str) -> Tuple[bool, str]: """ Open an email in Apple Mail for the user to manually forward. This just opens the .eml file in Mail. The user can then use Mail's Forward button (Cmd+Shift+F) themselves. Args: original_message_path: Path to the original .eml file Returns: Tuple of (success, message) """ try: # Just open the message in Mail - no AppleScript automation result = subprocess.run( ["open", "-a", "Mail", original_message_path], capture_output=True, text=True, ) if result.returncode != 0: return False, f"Failed to open message: {result.stderr}" return True, "Message opened - use Forward (Cmd+Shift+F)" except Exception as e: logger.error(f"Error opening message for forward: {e}", exc_info=True) return False, str(e)