aerc sendmail wip
This commit is contained in:
152
sendmail
Executable file
152
sendmail
Executable file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sendmail-compatible wrapper for Microsoft Graph email sending.
|
||||
Queues emails in maildir format for processing by the sync daemon.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from email.parser import Parser
|
||||
from email.utils import parseaddr
|
||||
|
||||
# Add the project root to Python path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.utils.mail_utils.helpers import ensure_directory_exists
|
||||
|
||||
|
||||
def extract_org_from_email(email_address: str) -> str:
|
||||
"""
|
||||
Extract organization name from email address domain.
|
||||
|
||||
Args:
|
||||
email_address: Email address like "user@corteva.com"
|
||||
|
||||
Returns:
|
||||
Organization name (e.g., "corteva")
|
||||
"""
|
||||
if "@" not in email_address:
|
||||
return "default"
|
||||
|
||||
domain = email_address.split("@")[1].lower()
|
||||
|
||||
# Map known domains to org names
|
||||
domain_to_org = {
|
||||
"corteva.com": "corteva",
|
||||
# Add more domain mappings as needed
|
||||
}
|
||||
|
||||
return domain_to_org.get(domain, domain.split(".")[0])
|
||||
|
||||
|
||||
def create_outbox_structure(base_path: str, org: str):
|
||||
"""
|
||||
Create maildir structure for outbox.
|
||||
|
||||
Args:
|
||||
base_path: Base maildir path (e.g., ~/Mail)
|
||||
org: Organization name
|
||||
"""
|
||||
org_path = os.path.join(base_path, org, "outbox")
|
||||
ensure_directory_exists(os.path.join(org_path, "new"))
|
||||
ensure_directory_exists(os.path.join(org_path, "cur"))
|
||||
ensure_directory_exists(os.path.join(org_path, "tmp"))
|
||||
ensure_directory_exists(os.path.join(org_path, "failed"))
|
||||
|
||||
|
||||
def queue_email(email_content: str, org: str) -> bool:
|
||||
"""
|
||||
Queue email in maildir outbox for sending.
|
||||
|
||||
Args:
|
||||
email_content: Raw email content
|
||||
org: Organization name
|
||||
|
||||
Returns:
|
||||
True if queued successfully, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Get base maildir path
|
||||
base_path = os.path.expanduser(os.getenv("MAILDIR_PATH", "~/Mail"))
|
||||
|
||||
# Create outbox structure
|
||||
create_outbox_structure(base_path, org)
|
||||
|
||||
# Generate unique filename
|
||||
timestamp = str(int(time.time() * 1000000))
|
||||
hostname = os.uname().nodename
|
||||
filename = f"{timestamp}.{os.getpid()}.{hostname}"
|
||||
|
||||
# Write to tmp first, then move to new (atomic operation)
|
||||
tmp_path = os.path.join(base_path, org, "outbox", "tmp", filename)
|
||||
new_path = os.path.join(base_path, org, "outbox", "new", filename)
|
||||
|
||||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||||
f.write(email_content)
|
||||
|
||||
os.rename(tmp_path, new_path)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to queue email: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main sendmail wrapper function.
|
||||
Reads email from stdin and queues it for sending.
|
||||
"""
|
||||
# Set up basic logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(os.path.expanduser("~/Mail/sendmail.log")),
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
# Read email from stdin
|
||||
email_content = sys.stdin.read()
|
||||
|
||||
if not email_content.strip():
|
||||
logging.error("No email content received")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse email to extract From header
|
||||
parser = Parser()
|
||||
msg = parser.parsestr(email_content)
|
||||
|
||||
from_header = msg.get("From", "")
|
||||
if not from_header:
|
||||
logging.error("No From header found in email")
|
||||
sys.exit(1)
|
||||
|
||||
# Extract email address from From header
|
||||
_, from_email = parseaddr(from_header)
|
||||
if not from_email:
|
||||
logging.error(f"Could not parse email address from From header: {from_header}")
|
||||
sys.exit(1)
|
||||
|
||||
# Determine organization from email domain
|
||||
org = extract_org_from_email(from_email)
|
||||
|
||||
# Queue the email
|
||||
if queue_email(email_content, org):
|
||||
logging.info(f"Email queued successfully for org: {org}, from: {from_email}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
logging.error("Failed to queue email")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Sendmail wrapper error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -26,10 +26,10 @@
|
||||
gsub(/mailto:[^[:space:]]*/, "")
|
||||
|
||||
# Clean up email headers - make them bold
|
||||
if (/^From:/) { gsub(/^From:[[:space:]]*/, "**From:** ") }
|
||||
if (/^To:/) { gsub(/^To:[[:space:]]*/, "**To:** ") }
|
||||
if (/^Subject:/) { gsub(/^Subject:[[:space:]]*/, "**Subject:** ") }
|
||||
if (/^Date:/) { gsub(/^Date:[[:space:]]*/, "**Date:** ") }
|
||||
# if (/^From:/) { gsub(/^From:[[:space:]]*/, "**From:** ") }
|
||||
# if (/^To:/) { gsub(/^To:[[:space:]]*/, "**To:** ") }
|
||||
# if (/^Subject:/) { gsub(/^Subject:[[:space:]]*/, "**Subject:** ") }
|
||||
# if (/^Date:/) { gsub(/^Date:[[:space:]]*/, "**Date:** ") }
|
||||
|
||||
# Skip empty lines
|
||||
if (/^[[:space:]]*$/) next
|
||||
|
||||
@@ -18,6 +18,7 @@ from src.services.microsoft_graph.mail import (
|
||||
archive_mail_async,
|
||||
delete_mail_async,
|
||||
synchronize_maildir_async,
|
||||
process_outbox_async,
|
||||
)
|
||||
from src.services.microsoft_graph.auth import get_access_token
|
||||
|
||||
@@ -38,6 +39,11 @@ def create_maildir_structure(base_path):
|
||||
ensure_directory_exists(os.path.join(base_path, "tmp"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Archives"))
|
||||
ensure_directory_exists(os.path.join(base_path, ".Trash", "cur"))
|
||||
# Create outbox structure for sending emails
|
||||
ensure_directory_exists(os.path.join(base_path, "outbox", "new"))
|
||||
ensure_directory_exists(os.path.join(base_path, "outbox", "cur"))
|
||||
ensure_directory_exists(os.path.join(base_path, "outbox", "tmp"))
|
||||
ensure_directory_exists(os.path.join(base_path, "outbox", "failed"))
|
||||
|
||||
|
||||
async def fetch_calendar_async(
|
||||
@@ -228,7 +234,8 @@ async def _sync_outlook_data(
|
||||
vdir = os.path.expanduser(vdir)
|
||||
|
||||
# Save emails to Maildir
|
||||
maildir_path = os.getenv("MAILDIR_PATH", os.path.expanduser("~/Mail")) + f"/{org}"
|
||||
base_maildir_path = os.getenv("MAILDIR_PATH", os.path.expanduser("~/Mail"))
|
||||
maildir_path = base_maildir_path + f"/{org}"
|
||||
attachments_dir = os.path.join(maildir_path, "attachments")
|
||||
ensure_directory_exists(attachments_dir)
|
||||
create_maildir_structure(maildir_path)
|
||||
@@ -256,6 +263,9 @@ async def _sync_outlook_data(
|
||||
task_read = progress.add_task("[blue]Marking as read...", total=0)
|
||||
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
|
||||
task_delete = progress.add_task("[red]Deleting mail...", total=0)
|
||||
task_outbox = progress.add_task(
|
||||
"[bright_green]Sending outbound mail...", total=0
|
||||
)
|
||||
|
||||
# Stage 1: Synchronize local changes (read, archive, delete, calendar) to the server
|
||||
progress.console.print(
|
||||
@@ -273,13 +283,16 @@ async def _sync_outlook_data(
|
||||
headers, org_vdir_path, progress, task_local_calendar, dry_run
|
||||
)
|
||||
|
||||
# Handle mail changes in parallel
|
||||
# Handle mail changes and outbound email in parallel
|
||||
await asyncio.gather(
|
||||
synchronize_maildir_async(
|
||||
maildir_path, headers, progress, task_read, dry_run
|
||||
),
|
||||
archive_mail_async(maildir_path, headers, progress, task_archive, dry_run),
|
||||
delete_mail_async(maildir_path, headers, progress, task_delete, dry_run),
|
||||
process_outbox_async(
|
||||
base_maildir_path, org, headers, progress, task_outbox, dry_run
|
||||
),
|
||||
)
|
||||
progress.console.print("[bold green]Step 1: Local changes synced.[/bold green]")
|
||||
|
||||
@@ -628,14 +641,50 @@ async def daemon_mode(
|
||||
vdir, org
|
||||
)
|
||||
|
||||
# Check for outbound emails in outbox
|
||||
base_maildir_path = os.getenv(
|
||||
"MAILDIR_PATH", os.path.expanduser("~/Mail")
|
||||
)
|
||||
outbox_new_dir = os.path.join(base_maildir_path, org, "outbox", "new")
|
||||
outbox_changes = False
|
||||
pending_email_count = 0
|
||||
|
||||
if os.path.exists(outbox_new_dir):
|
||||
pending_emails = [
|
||||
f for f in os.listdir(outbox_new_dir) if not f.startswith(".")
|
||||
]
|
||||
pending_email_count = len(pending_emails)
|
||||
outbox_changes = pending_email_count > 0
|
||||
|
||||
# Determine what changed and show appropriate status
|
||||
if mail_changes and calendar_changes:
|
||||
if mail_changes and calendar_changes and outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc} | Outbox: {pending_email_count} pending. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif mail_changes and calendar_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Calendar: {calendar_change_desc}. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif mail_changes and outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Mail: Remote {remote_message_count}, Local {local_message_count} | Outbox: {pending_email_count} pending. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif calendar_changes and outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Changes detected! Calendar: {calendar_change_desc} | Outbox: {pending_email_count} pending. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif mail_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
@@ -650,9 +699,16 @@ async def daemon_mode(
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
elif outbox_changes:
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"Outbound emails detected! {pending_email_count} emails pending. Starting sync...",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
|
||||
# Sync if any changes detected
|
||||
if mail_changes or calendar_changes:
|
||||
if mail_changes or calendar_changes or outbox_changes:
|
||||
await _sync_outlook_data(
|
||||
dry_run,
|
||||
vdir,
|
||||
@@ -674,6 +730,8 @@ async def daemon_mode(
|
||||
if two_way_calendar:
|
||||
status_parts.append(f"Calendar: {calendar_change_desc}")
|
||||
|
||||
status_parts.append(f"Outbox: {pending_email_count} pending")
|
||||
|
||||
console.print(
|
||||
create_status_display(
|
||||
f"No changes detected ({', '.join(status_parts)})",
|
||||
|
||||
@@ -81,8 +81,13 @@ def get_access_token(scopes):
|
||||
|
||||
token_response = app.acquire_token_by_device_flow(flow)
|
||||
|
||||
if token_response is None:
|
||||
raise Exception("Token response is None - authentication failed")
|
||||
|
||||
if "access_token" not in token_response:
|
||||
raise Exception("Failed to acquire token")
|
||||
error_description = token_response.get("error_description", "Unknown error")
|
||||
error_code = token_response.get("error", "unknown_error")
|
||||
raise Exception(f"Failed to acquire token - {error_code}: {error_description}")
|
||||
|
||||
# Save token cache
|
||||
with open(cache_file, "w") as f:
|
||||
|
||||
@@ -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
|
||||
|
||||
60
test_aerc_integration.sh
Executable file
60
test_aerc_integration.sh
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script to demonstrate aerc integration with the sendmail wrapper
|
||||
# This shows how aerc would interact with our email sending system
|
||||
|
||||
echo "=== Testing Email Sending Daemon ==="
|
||||
echo
|
||||
|
||||
# Get the full path to our sendmail wrapper
|
||||
SENDMAIL_PATH="$(pwd)/sendmail"
|
||||
echo "Sendmail wrapper: $SENDMAIL_PATH"
|
||||
echo
|
||||
|
||||
# Show current queue status
|
||||
echo "Current outbox queue:"
|
||||
find ~/Mail/*/outbox/new -type f 2>/dev/null | wc -l | xargs echo "Pending emails:"
|
||||
echo
|
||||
|
||||
# Create a test email that aerc might send
|
||||
echo "Creating test email as aerc would..."
|
||||
cat << 'EOF' | $SENDMAIL_PATH
|
||||
From: user@corteva.com
|
||||
To: colleague@example.com
|
||||
Cc: team@example.com
|
||||
Subject: Project Update from aerc
|
||||
|
||||
Hi team,
|
||||
|
||||
This email was composed in aerc using helix editor and queued
|
||||
for sending through our Microsoft Graph adapter.
|
||||
|
||||
The email will be sent when the sync daemon processes the outbox.
|
||||
|
||||
Best regards,
|
||||
User
|
||||
EOF
|
||||
|
||||
echo "Email queued successfully!"
|
||||
echo
|
||||
|
||||
# Show updated queue status
|
||||
echo "Updated outbox queue:"
|
||||
find ~/Mail/*/outbox/new -type f 2>/dev/null | wc -l | xargs echo "Pending emails:"
|
||||
echo
|
||||
|
||||
echo "To process the queue, run:"
|
||||
echo " python3 -m src.cli sync --daemon --notify"
|
||||
echo
|
||||
echo "Or for a one-time sync:"
|
||||
echo " python3 -m src.cli sync --dry-run"
|
||||
echo
|
||||
|
||||
echo "=== aerc Configuration ==="
|
||||
echo "Add this to your aerc config:"
|
||||
echo
|
||||
echo "[outgoing]"
|
||||
echo "sendmail = $SENDMAIL_PATH"
|
||||
echo
|
||||
echo "Then compose emails in aerc as usual - they'll be queued offline"
|
||||
echo "and sent when you run the sync daemon."
|
||||
Reference in New Issue
Block a user