Files
luk/src/mail/utils/apple_mail.py
Bendt b52a06f2cf feat: Add compose/reply/forward email actions via Apple Mail
- Add compose.py with async actions that export messages via himalaya
- Add apple_mail.py utilities using mailto: URLs and open command
- No AppleScript automation for compose/reply/forward (only calendar replies)
- Update app.py to call async reply/forward actions
- Add SMTP OAuth2 support (disabled by default) in mail.py and auth.py
- Add config options for SMTP send and auto-send via AppleScript
2026-01-02 12:11:44 -05:00

256 lines
7.6 KiB
Python

"""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)