aerc sendmail wip
This commit is contained in:
@@ -6,8 +6,9 @@ import os
|
||||
import re
|
||||
import glob
|
||||
import asyncio
|
||||
from typing import Set
|
||||
import aiohttp
|
||||
from email.parser import Parser
|
||||
from email.utils import getaddresses
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from .client import (
|
||||
fetch_with_aiohttp,
|
||||
@@ -43,7 +44,6 @@ async def fetch_mail_async(
|
||||
None
|
||||
"""
|
||||
from src.utils.mail_utils.maildir import save_mime_to_maildir_async
|
||||
from src.utils.mail_utils.helpers import truncate_id
|
||||
|
||||
mail_url = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?$top=100&$orderby=receivedDateTime asc&$select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,isRead"
|
||||
messages = []
|
||||
@@ -559,3 +559,276 @@ async def synchronize_maildir_async(
|
||||
)
|
||||
else:
|
||||
progress.console.print("[DRY-RUN] Would save sync timestamp.")
|
||||
|
||||
|
||||
def parse_email_for_graph_api(email_content: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse email content and convert to Microsoft Graph API message format.
|
||||
|
||||
Args:
|
||||
email_content: Raw email content (RFC 5322 format)
|
||||
|
||||
Returns:
|
||||
Dictionary formatted for Microsoft Graph API send message
|
||||
"""
|
||||
parser = Parser()
|
||||
msg = parser.parsestr(email_content)
|
||||
|
||||
# Parse recipients
|
||||
def parse_recipients(header_value: str) -> List[Dict[str, Any]]:
|
||||
if not header_value:
|
||||
return []
|
||||
addresses = getaddresses([header_value])
|
||||
return [
|
||||
{"emailAddress": {"address": addr, "name": name if name else addr}}
|
||||
for name, addr in addresses
|
||||
if addr
|
||||
]
|
||||
|
||||
to_recipients = parse_recipients(msg.get("To", ""))
|
||||
cc_recipients = parse_recipients(msg.get("Cc", ""))
|
||||
bcc_recipients = parse_recipients(msg.get("Bcc", ""))
|
||||
|
||||
# Get body content
|
||||
body_content = ""
|
||||
body_type = "text"
|
||||
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() == "text/plain":
|
||||
body_content = part.get_payload(decode=True).decode(
|
||||
"utf-8", errors="ignore"
|
||||
)
|
||||
body_type = "text"
|
||||
break
|
||||
elif part.get_content_type() == "text/html":
|
||||
body_content = part.get_payload(decode=True).decode(
|
||||
"utf-8", errors="ignore"
|
||||
)
|
||||
body_type = "html"
|
||||
else:
|
||||
body_content = msg.get_payload(decode=True).decode("utf-8", errors="ignore")
|
||||
if msg.get_content_type() == "text/html":
|
||||
body_type = "html"
|
||||
|
||||
# Build Graph API message
|
||||
message = {
|
||||
"subject": msg.get("Subject", ""),
|
||||
"body": {"contentType": body_type, "content": body_content},
|
||||
"toRecipients": to_recipients,
|
||||
"ccRecipients": cc_recipients,
|
||||
"bccRecipients": bcc_recipients,
|
||||
}
|
||||
|
||||
# Add reply-to if present
|
||||
reply_to = msg.get("Reply-To", "")
|
||||
if reply_to:
|
||||
message["replyTo"] = parse_recipients(reply_to)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
async def send_email_async(
|
||||
email_content: str, headers: Dict[str, str], dry_run: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Send email using Microsoft Graph API.
|
||||
|
||||
Args:
|
||||
email_content: Raw email content (RFC 5322 format)
|
||||
headers: Authentication headers for Microsoft Graph API
|
||||
dry_run: If True, don't actually send the email
|
||||
|
||||
Returns:
|
||||
True if email was sent successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Parse email content for Graph API
|
||||
message_data = parse_email_for_graph_api(email_content)
|
||||
|
||||
if dry_run:
|
||||
print(f"[DRY-RUN] Would send email: {message_data['subject']}")
|
||||
print(
|
||||
f"[DRY-RUN] To: {[r['emailAddress']['address'] for r in message_data['toRecipients']]}"
|
||||
)
|
||||
return True
|
||||
|
||||
# Send email via Graph API
|
||||
send_url = "https://graph.microsoft.com/v1.0/me/sendMail"
|
||||
|
||||
# Log attempt
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(
|
||||
os.path.expanduser("~/Mail/sendmail.log"), mode="a"
|
||||
),
|
||||
],
|
||||
)
|
||||
logging.info(
|
||||
f"Attempting to send email: {message_data['subject']} to {[r['emailAddress']['address'] for r in message_data['toRecipients']]}"
|
||||
)
|
||||
|
||||
response = await post_with_aiohttp(send_url, headers, {"message": message_data})
|
||||
|
||||
# Microsoft Graph sendMail returns 202 Accepted on success
|
||||
if response == 202:
|
||||
logging.info(f"Successfully sent email: {message_data['subject']}")
|
||||
return True
|
||||
else:
|
||||
logging.error(
|
||||
f"Unexpected response code {response} when sending email: {message_data['subject']}"
|
||||
)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(
|
||||
os.path.expanduser("~/Mail/sendmail.log"), mode="a"
|
||||
),
|
||||
],
|
||||
)
|
||||
logging.error(f"Exception sending email: {e}", exc_info=True)
|
||||
print(f"Error sending email: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def process_outbox_async(
|
||||
maildir_path: str,
|
||||
org: str,
|
||||
headers: Dict[str, str],
|
||||
progress,
|
||||
task_id,
|
||||
dry_run: bool = False,
|
||||
) -> tuple[int, int]:
|
||||
"""
|
||||
Process outbound emails in the outbox queue.
|
||||
|
||||
Args:
|
||||
maildir_path: Base maildir path
|
||||
org: Organization name
|
||||
headers: Authentication headers for Microsoft Graph API
|
||||
progress: Progress instance for updating progress bars
|
||||
task_id: ID of the task in the progress bar
|
||||
dry_run: If True, don't actually send emails
|
||||
|
||||
Returns:
|
||||
Tuple of (successful_sends, failed_sends)
|
||||
"""
|
||||
outbox_path = os.path.join(maildir_path, org, "outbox")
|
||||
new_dir = os.path.join(outbox_path, "new")
|
||||
cur_dir = os.path.join(outbox_path, "cur")
|
||||
failed_dir = os.path.join(outbox_path, "failed")
|
||||
|
||||
# Ensure directories exist
|
||||
from src.utils.mail_utils.helpers import ensure_directory_exists
|
||||
|
||||
ensure_directory_exists(failed_dir)
|
||||
|
||||
# Get pending emails
|
||||
pending_emails = []
|
||||
if os.path.exists(new_dir):
|
||||
pending_emails = [f for f in os.listdir(new_dir) if not f.startswith(".")]
|
||||
|
||||
if not pending_emails:
|
||||
progress.update(task_id, total=0, completed=0)
|
||||
return 0, 0
|
||||
|
||||
progress.update(task_id, total=len(pending_emails))
|
||||
progress.console.print(
|
||||
f"Processing {len(pending_emails)} outbound emails for {org}"
|
||||
)
|
||||
|
||||
successful_sends = 0
|
||||
failed_sends = 0
|
||||
|
||||
for email_file in pending_emails:
|
||||
email_path = os.path.join(new_dir, email_file)
|
||||
|
||||
try:
|
||||
# Read email content
|
||||
with open(email_path, "r", encoding="utf-8") as f:
|
||||
email_content = f.read()
|
||||
|
||||
# Send email
|
||||
if await send_email_async(email_content, headers, dry_run):
|
||||
# Move to cur directory on success
|
||||
if not dry_run:
|
||||
cur_path = os.path.join(cur_dir, email_file)
|
||||
os.rename(email_path, cur_path)
|
||||
progress.console.print(f"✓ Sent email: {email_file}")
|
||||
else:
|
||||
progress.console.print(f"[DRY-RUN] Would send email: {email_file}")
|
||||
successful_sends += 1
|
||||
else:
|
||||
# Move to failed directory on failure
|
||||
if not dry_run:
|
||||
failed_path = os.path.join(failed_dir, email_file)
|
||||
os.rename(email_path, failed_path)
|
||||
progress.console.print(f"✗ Failed to send email: {email_file}")
|
||||
|
||||
# Log the failure
|
||||
import logging
|
||||
|
||||
logging.error(f"Failed to send email: {email_file}")
|
||||
|
||||
# Send notification about failure
|
||||
from src.utils.notifications import send_notification
|
||||
|
||||
parser = Parser()
|
||||
msg = parser.parsestr(email_content)
|
||||
subject = msg.get("Subject", "Unknown")
|
||||
send_notification(
|
||||
title="Email Send Failed",
|
||||
message=f"Failed to send: {subject}",
|
||||
subtitle=f"Check {failed_dir}",
|
||||
sound="default",
|
||||
)
|
||||
failed_sends += 1
|
||||
|
||||
except Exception as e:
|
||||
progress.console.print(f"✗ Error processing {email_file}: {e}")
|
||||
if not dry_run:
|
||||
# Move to failed directory
|
||||
failed_path = os.path.join(failed_dir, email_file)
|
||||
try:
|
||||
os.rename(email_path, failed_path)
|
||||
except (OSError, FileNotFoundError):
|
||||
pass # File might already be moved or deleted
|
||||
failed_sends += 1
|
||||
|
||||
progress.advance(task_id, 1)
|
||||
|
||||
if not dry_run and successful_sends > 0:
|
||||
progress.console.print(f"✓ Successfully sent {successful_sends} emails")
|
||||
|
||||
# Send success notification
|
||||
from src.utils.notifications import send_notification
|
||||
|
||||
if successful_sends == 1:
|
||||
send_notification(
|
||||
title="Email Sent",
|
||||
message="1 email sent successfully",
|
||||
subtitle=f"from {org}",
|
||||
sound="default",
|
||||
)
|
||||
else:
|
||||
send_notification(
|
||||
title="Emails Sent",
|
||||
message=f"{successful_sends} emails sent successfully",
|
||||
subtitle=f"from {org}",
|
||||
sound="default",
|
||||
)
|
||||
|
||||
if failed_sends > 0:
|
||||
progress.console.print(f"✗ Failed to send {failed_sends} emails")
|
||||
|
||||
return successful_sends, failed_sends
|
||||
|
||||
Reference in New Issue
Block a user