using MIME instead of MML
This commit is contained in:
@@ -17,7 +17,11 @@ import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from icalendar import Calendar, Event, vText
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from icalendar import Calendar, Event, vCalAddress, vText
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
@@ -65,13 +69,27 @@ def _sync_calendar():
|
||||
print("Warning: CalDAV sync failed (will retry on next heartbeat)")
|
||||
|
||||
|
||||
def _send_mml(mml, account=None):
|
||||
"""Send an MML message via himalaya template send."""
|
||||
def _send_email(email_str, account=None):
|
||||
"""Send a raw MIME email via himalaya message send (stdin)."""
|
||||
cmd = ["himalaya"]
|
||||
if account:
|
||||
cmd += ["--account", account]
|
||||
cmd += ["template", "send"]
|
||||
subprocess.run(cmd, input=mml, text=True, check=True)
|
||||
cmd += ["message", "send"]
|
||||
subprocess.run(cmd, input=email_str, text=True, check=True)
|
||||
|
||||
|
||||
def _build_calendar_email(from_addr, to_addr, subject, body, ics_bytes, method="REQUEST"):
|
||||
"""Build a MIME email with a text/calendar attachment."""
|
||||
msg = MIMEMultipart('mixed')
|
||||
msg['From'] = from_addr
|
||||
msg['To'] = to_addr
|
||||
msg['Subject'] = subject
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
ics_part = MIMEBase('text', 'calendar', method=method, charset='utf-8')
|
||||
ics_part.set_payload(ics_bytes.decode('utf-8'))
|
||||
ics_part.add_header('Content-Disposition', 'attachment; filename="invite.ics"')
|
||||
msg.attach(ics_part)
|
||||
return msg.as_string()
|
||||
|
||||
|
||||
def _parse_iso_datetime(dt_str):
|
||||
@@ -111,8 +129,9 @@ def cmd_send(args):
|
||||
event.add("summary", args.summary)
|
||||
event.add("status", "CONFIRMED")
|
||||
event.add("sequence", 0)
|
||||
event["organizer"] = f"mailto:{args.sender}"
|
||||
event["organizer"].params["CN"] = vText(organizer_name)
|
||||
organizer = vCalAddress(f"mailto:{args.sender}")
|
||||
organizer.params["CN"] = vText(organizer_name)
|
||||
event.add("organizer", organizer)
|
||||
|
||||
if args.location:
|
||||
event.add("location", args.location)
|
||||
@@ -135,10 +154,6 @@ def cmd_send(args):
|
||||
cal.add_component(event)
|
||||
ics_bytes = cal.to_ical()
|
||||
|
||||
# Write ICS to temp file
|
||||
tmp_ics = Path(f"/tmp/openclaw-invite-{int(datetime.now().timestamp())}.ics")
|
||||
tmp_ics.write_bytes(ics_bytes)
|
||||
|
||||
# Build plain text body
|
||||
body = f"You're invited to: {args.summary}\n\nWhen: {args.start} - {args.end} ({args.timezone})"
|
||||
if args.location:
|
||||
@@ -149,29 +164,18 @@ def cmd_send(args):
|
||||
# Email goes to all attendees (including owner)
|
||||
all_to = ", ".join(all_attendees)
|
||||
|
||||
# Build MML message
|
||||
mml = (
|
||||
f"From: {args.sender}\n"
|
||||
f"To: {all_to}\n"
|
||||
f"Subject: {args.subject}\n"
|
||||
f"\n"
|
||||
f"<#multipart type=mixed>\n"
|
||||
f"<#part type=text/plain>\n"
|
||||
f"{body}\n"
|
||||
f"<#part type=text/calendar method=REQUEST filename={tmp_ics} name=invite.ics><#/part>\n"
|
||||
f"<#/multipart>"
|
||||
)
|
||||
# Build MIME email
|
||||
email_str = _build_calendar_email(args.sender, all_to, args.subject, body, ics_bytes, method="REQUEST")
|
||||
|
||||
if args.dry_run:
|
||||
print("=== ICS Content ===")
|
||||
print(ics_bytes.decode())
|
||||
print("=== MML Message ===")
|
||||
print(mml)
|
||||
tmp_ics.unlink(missing_ok=True)
|
||||
print("=== Email Message ===")
|
||||
print(email_str)
|
||||
return
|
||||
|
||||
# Send email
|
||||
_send_mml(mml, args.account)
|
||||
# Send email via himalaya message send (stdin)
|
||||
_send_email(email_str, args.account)
|
||||
print(f"Calendar invite sent to: {args.to}")
|
||||
|
||||
# Save to local calendar
|
||||
@@ -181,8 +185,6 @@ def cmd_send(args):
|
||||
print(f"Saved to local calendar: {dest}")
|
||||
_sync_calendar()
|
||||
|
||||
tmp_ics.unlink(missing_ok=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reply to invite
|
||||
@@ -305,10 +307,6 @@ def cmd_reply(args):
|
||||
reply_cal.add_component(reply_event)
|
||||
reply_ics_bytes = reply_cal.to_ical()
|
||||
|
||||
# Write reply ICS to temp file
|
||||
tmp_reply = Path(f"/tmp/openclaw-reply-{int(datetime.now().timestamp())}.ics")
|
||||
tmp_reply.write_bytes(reply_ics_bytes)
|
||||
|
||||
# Build email
|
||||
prefix = SUBJECT_PREFIX[partstat]
|
||||
subject = f"{prefix}: {summary}"
|
||||
@@ -317,17 +315,7 @@ def cmd_reply(args):
|
||||
if args.comment:
|
||||
body += f"\n\n{args.comment}"
|
||||
|
||||
mml = (
|
||||
f"From: {args.sender}\n"
|
||||
f"To: {organizer_email}\n"
|
||||
f"Subject: {subject}\n"
|
||||
f"\n"
|
||||
f"<#multipart type=mixed>\n"
|
||||
f"<#part type=text/plain>\n"
|
||||
f"{body}\n"
|
||||
f"<#part type=text/calendar method=REPLY filename={tmp_reply} name=invite.ics><#/part>\n"
|
||||
f"<#/multipart>"
|
||||
)
|
||||
email_str = _build_calendar_email(args.sender, organizer_email, subject, body, reply_ics_bytes, method="REPLY")
|
||||
|
||||
if args.dry_run:
|
||||
print("=== Original Event ===")
|
||||
@@ -337,9 +325,8 @@ def cmd_reply(args):
|
||||
print()
|
||||
print("=== Reply ICS ===")
|
||||
print(reply_ics_bytes.decode())
|
||||
print("=== MML Message ===")
|
||||
print(mml)
|
||||
tmp_reply.unlink(missing_ok=True)
|
||||
print("=== Email Message ===")
|
||||
print(email_str)
|
||||
if cleanup_dir:
|
||||
for f in cleanup_dir.iterdir():
|
||||
f.unlink()
|
||||
@@ -347,30 +334,22 @@ def cmd_reply(args):
|
||||
return
|
||||
|
||||
# Send reply
|
||||
_send_mml(mml, args.account)
|
||||
_send_email(email_str, args.account)
|
||||
print(f"Calendar invite {partstat.lower()}: {summary} (replied to {organizer_email})")
|
||||
|
||||
# Forward invite to owner on accept/tentative
|
||||
if partstat in ("ACCEPTED", "TENTATIVE"):
|
||||
tmp_fwd = Path(f"/tmp/openclaw-fwd-{int(datetime.now().timestamp())}.ics")
|
||||
tmp_fwd.write_bytes(ics_path.read_bytes())
|
||||
fwd_mml = (
|
||||
f"From: {args.sender}\n"
|
||||
f"To: {DEFAULT_OWNER_EMAIL}\n"
|
||||
f"Subject: {prefix}: {summary}\n"
|
||||
f"\n"
|
||||
f"<#multipart type=mixed>\n"
|
||||
f"<#part type=text/plain>\n"
|
||||
f"{prefix}: {summary}\n"
|
||||
f"<#part type=text/calendar method=REQUEST filename={tmp_fwd} name=invite.ics><#/part>\n"
|
||||
f"<#/multipart>"
|
||||
fwd_body = f"{prefix}: {summary}"
|
||||
fwd_email = _build_calendar_email(
|
||||
args.sender, DEFAULT_OWNER_EMAIL,
|
||||
f"{prefix}: {summary}", fwd_body,
|
||||
ics_path.read_bytes(), method="REQUEST",
|
||||
)
|
||||
try:
|
||||
_send_mml(fwd_mml, args.account)
|
||||
_send_email(fwd_email, args.account)
|
||||
print(f"Forwarded invite to {DEFAULT_OWNER_EMAIL}")
|
||||
except subprocess.CalledProcessError:
|
||||
print(f"Warning: Failed to forward invite to {DEFAULT_OWNER_EMAIL}")
|
||||
tmp_fwd.unlink(missing_ok=True)
|
||||
|
||||
# Save to / remove from local calendar
|
||||
if CALENDAR_DIR.is_dir():
|
||||
@@ -385,7 +364,6 @@ def cmd_reply(args):
|
||||
_sync_calendar()
|
||||
|
||||
# Cleanup
|
||||
tmp_reply.unlink(missing_ok=True)
|
||||
if cleanup_dir:
|
||||
for f in cleanup_dir.iterdir():
|
||||
f.unlink()
|
||||
|
||||
Reference in New Issue
Block a user