- 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
256 lines
7.6 KiB
Python
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)
|