feat: add email, scheduler, and Zulip integration services

- Add email service for sending meeting invites with ICS attachments
- Add scheduler for background calendar sync jobs
- Add Zulip service for meeting notifications
- Make ics_url optional for participants
- Add /api/schedule endpoint with 2-hour lead time validation
- Update frontend to support scheduling flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Joyce
2026-01-21 13:52:02 -05:00
parent d585cf8613
commit 26311c867a
17 changed files with 403 additions and 59 deletions

View File

@@ -0,0 +1,82 @@
import logging
import uuid
from datetime import datetime, timezone
import aiosmtplib
from email.message import EmailMessage
from icalendar import Calendar, Event, vCalAddress, vText
from app.config import settings
logger = logging.getLogger(__name__)
async def send_meeting_invite(
participants: list[dict],
title: str,
description: str,
start_time: datetime,
end_time: datetime,
) -> bool:
if not settings.smtp_host or not settings.smtp_user or not settings.smtp_password:
logger.warning("SMTP credentials not configured. Skipping email invite.")
return False
# Create the ICS content
cal = Calendar()
cal.add('prodid', '-//Common Availability//m.com//')
cal.add('version', '2.0')
cal.add('method', 'REQUEST')
event = Event()
event.add('summary', title)
event.add('dtstart', start_time)
event.add('dtend', end_time)
event.add('dtstamp', datetime.now(timezone.utc))
event.add('description', description)
event.add('uid', str(uuid.uuid4()))
event.add('organizer', vCalAddress(f'MAILTO:{settings.smtp_user}'))
attendee_emails = []
for p in participants:
attendee = vCalAddress(f'MAILTO:{p["email"]}')
attendee.params['cn'] = vText(p["name"])
attendee.params['ROLE'] = vText('REQ-PARTICIPANT')
event.add('attendee', attendee, encode=0)
attendee_emails.append(p["email"])
cal.add_component(event)
ics_data = cal.to_ical()
# Create Email
msg = EmailMessage()
msg["Subject"] = f"Invitation: {title}"
msg["From"] = settings.smtp_user
msg["To"] = ", ".join(attendee_emails)
msg.set_content(
f"You have been invited to: {title}\n"
f"When: {start_time.strftime('%Y-%m-%d %H:%M %Z')}\n\n"
f"{description}"
)
# Attach ICS
msg.add_attachment(
ics_data,
maintype="text",
subtype="calendar",
filename="invite.ics",
params={"method": "REQUEST"}
)
# Send
try:
await aiosmtplib.send(
msg,
hostname=settings.smtp_host,
port=settings.smtp_port,
username=settings.smtp_user,
password=settings.smtp_password,
start_tls=True
)
logger.info(f"Sent meeting invites to {len(attendee_emails)} participants")
return True
except Exception as e:
logger.error(f"Failed to send email invite: {e}")
return False