using MIME instead of MML
This commit is contained in:
@@ -8,6 +8,10 @@ metadata: {"clawdbot":{"emoji":"📅","requires":{"bins":["himalaya","vdirsyncer
|
|||||||
|
|
||||||
Send, accept, and decline calendar invitations via email using himalaya. Events are saved to local calendar and synced to CalDAV (Migadu) via vdirsyncer.
|
Send, accept, and decline calendar invitations via email using himalaya. Events are saved to local calendar and synced to CalDAV (Migadu) via vdirsyncer.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
See `TESTING.md` for dry-run and live test steps, verification checklists, and troubleshooting.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `himalaya` configured and working (see the `himalaya` skill)
|
- `himalaya` configured and working (see the `himalaya` skill)
|
||||||
@@ -71,7 +75,7 @@ $SKILL_DIR/scripts/calendar-invite.sh send \
|
|||||||
| `--organizer` | No | Organizer display name (defaults to `--from`) |
|
| `--organizer` | No | Organizer display name (defaults to `--from`) |
|
||||||
| `--uid` | No | Custom event UID (auto-generated if omitted) |
|
| `--uid` | No | Custom event UID (auto-generated if omitted) |
|
||||||
| `--account` | No | Himalaya account name (if not default) |
|
| `--account` | No | Himalaya account name (if not default) |
|
||||||
| `--dry-run` | No | Print ICS + MML without sending |
|
| `--dry-run` | No | Print ICS + MIME without sending |
|
||||||
|
|
||||||
### Send Examples
|
### Send Examples
|
||||||
|
|
||||||
@@ -153,8 +157,8 @@ $SKILL_DIR/scripts/calendar-invite.sh reply \
|
|||||||
|
|
||||||
**Sending invites:**
|
**Sending invites:**
|
||||||
1. Generates an RFC 5545 ICS file with `METHOD:REQUEST` (via `icalendar` library)
|
1. Generates an RFC 5545 ICS file with `METHOD:REQUEST` (via `icalendar` library)
|
||||||
2. Builds an MML email with a `text/calendar` attachment
|
2. Builds a MIME email with a `text/calendar` attachment (via Python `email.mime`)
|
||||||
3. Sends via `himalaya template send`
|
3. Sends via `himalaya message send` (piped through stdin)
|
||||||
4. Saves the event to `~/.openclaw/workspace/calendars/home/`
|
4. Saves the event to `~/.openclaw/workspace/calendars/home/`
|
||||||
5. Runs `vdirsyncer sync` to push to Migadu CalDAV
|
5. Runs `vdirsyncer sync` to push to Migadu CalDAV
|
||||||
|
|
||||||
@@ -162,7 +166,7 @@ $SKILL_DIR/scripts/calendar-invite.sh reply \
|
|||||||
1. Extracts the `.ics` attachment from the email (via `himalaya attachment download`)
|
1. Extracts the `.ics` attachment from the email (via `himalaya attachment download`)
|
||||||
2. Parses the original event with the `icalendar` library
|
2. Parses the original event with the `icalendar` library
|
||||||
3. Generates a reply ICS with `METHOD:REPLY` and the correct `PARTSTAT`
|
3. Generates a reply ICS with `METHOD:REPLY` and the correct `PARTSTAT`
|
||||||
4. Sends the reply to the organizer via `himalaya template send`
|
4. Sends the reply to the organizer via `himalaya message send` (stdin)
|
||||||
5. On accept/tentative: saves event to local calendar. On decline: removes it
|
5. On accept/tentative: saves event to local calendar. On decline: removes it
|
||||||
6. Runs `vdirsyncer sync` to push changes to Migadu CalDAV
|
6. Runs `vdirsyncer sync` to push changes to Migadu CalDAV
|
||||||
|
|
||||||
@@ -206,7 +210,7 @@ Common IANA timezones:
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Invite shows as attachment instead of calendar event?**
|
**Invite shows as attachment instead of calendar event?**
|
||||||
- Ensure `type=text/calendar method=REQUEST` is set on the MML part
|
- Ensure the MIME part has `Content-Type: text/calendar; method=REQUEST`
|
||||||
- Some clients require the `METHOD:REQUEST` line in the ICS body
|
- Some clients require the `METHOD:REQUEST` line in the ICS body
|
||||||
|
|
||||||
**Times are wrong?**
|
**Times are wrong?**
|
||||||
|
|||||||
170
skills/calendar-invite/TESTING.md
Normal file
170
skills/calendar-invite/TESTING.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Testing the Calendar Invite Skill
|
||||||
|
|
||||||
|
End-to-end tests for send, reply, calendar sync, and local calendar. All commands use `--dry-run` first, then live.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKILL_DIR=~/.openclaw/workspace/skills/calendar-invite
|
||||||
|
|
||||||
|
# Use a date 3 days from now for test events
|
||||||
|
TEST_DATE=$(date -d "+3 days" +%Y-%m-%d)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Dry Run: Send Invite
|
||||||
|
|
||||||
|
Generates the ICS and MIME email without sending. Check that:
|
||||||
|
- ICS has `METHOD:REQUEST`
|
||||||
|
- MIME has `Content-Type: text/calendar; method=REQUEST`
|
||||||
|
- `mail@luyx.org` appears as attendee (auto-added)
|
||||||
|
- Times and timezone look correct
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$SKILL_DIR/scripts/calendar-invite.sh send \
|
||||||
|
--to "mail@luyx.org" \
|
||||||
|
--subject "Test Invite" \
|
||||||
|
--summary "Test Event" \
|
||||||
|
--start "${TEST_DATE}T15:00:00" \
|
||||||
|
--end "${TEST_DATE}T16:00:00" \
|
||||||
|
--dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Live Send: Self-Invite
|
||||||
|
|
||||||
|
Send a real invite to `mail@luyx.org` only (no confirmation needed per email rules).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$SKILL_DIR/scripts/calendar-invite.sh send \
|
||||||
|
--to "mail@luyx.org" \
|
||||||
|
--subject "Calendar Skill Test" \
|
||||||
|
--summary "Calendar Skill Test" \
|
||||||
|
--start "${TEST_DATE}T15:00:00" \
|
||||||
|
--end "${TEST_DATE}T16:00:00" \
|
||||||
|
--location "Test Location"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
- [ ] Script exits without error
|
||||||
|
- [ ] Email arrives at `mail@luyx.org`
|
||||||
|
- [ ] Email shows Accept/Decline/Tentative buttons (not just an attachment)
|
||||||
|
- [ ] `.ics` file saved to `~/.openclaw/workspace/calendars/home/`
|
||||||
|
|
||||||
|
## 3. Verify Calendar Sync and Local Calendar
|
||||||
|
|
||||||
|
After sending in step 2, check that the event synced and appears locally.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check vdirsyncer sync ran (should have printed "Synced to CalDAV server" in step 2)
|
||||||
|
# If not, run manually:
|
||||||
|
vdirsyncer sync
|
||||||
|
|
||||||
|
# List .ics files in local calendar
|
||||||
|
ls ~/.openclaw/workspace/calendars/home/
|
||||||
|
|
||||||
|
# Check the event shows up in khal
|
||||||
|
khal list "$TEST_DATE"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
- [ ] `vdirsyncer sync` completes without errors
|
||||||
|
- [ ] `.ics` file exists in `~/.openclaw/workspace/calendars/home/`
|
||||||
|
- [ ] `khal list` shows "Calendar Skill Test" on the test date
|
||||||
|
|
||||||
|
## 4. Reply: Accept the Self-Invite
|
||||||
|
|
||||||
|
The invite sent in step 2 should be in the inbox. Find it, then accept it. This tests the full reply flow without needing an external sender.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find the test invite in inbox
|
||||||
|
himalaya envelope list
|
||||||
|
|
||||||
|
# Confirm it's the calendar invite
|
||||||
|
himalaya message read <envelope-id>
|
||||||
|
|
||||||
|
# Accept it
|
||||||
|
$SKILL_DIR/scripts/calendar-invite.sh reply \
|
||||||
|
--envelope-id <envelope-id> \
|
||||||
|
--action accept
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
- [ ] Reply sent to organizer (youlu@luyanxin.com, i.e. ourselves)
|
||||||
|
- [ ] Original invite forwarded to `mail@luyx.org`
|
||||||
|
- [ ] Event still in `~/.openclaw/workspace/calendars/home/`
|
||||||
|
- [ ] `vdirsyncer sync` ran
|
||||||
|
- [ ] `khal list "$TEST_DATE"` still shows the event
|
||||||
|
|
||||||
|
## 5. Reply: Decline an Invite
|
||||||
|
|
||||||
|
Send another self-invite, then decline it. This verifies decline removes the event from local calendar.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Send a second test invite
|
||||||
|
$SKILL_DIR/scripts/calendar-invite.sh send \
|
||||||
|
--to "mail@luyx.org" \
|
||||||
|
--subject "Decline Test" \
|
||||||
|
--summary "Decline Test Event" \
|
||||||
|
--start "${TEST_DATE}T17:00:00" \
|
||||||
|
--end "${TEST_DATE}T18:00:00"
|
||||||
|
|
||||||
|
# Find it in inbox
|
||||||
|
himalaya envelope list
|
||||||
|
|
||||||
|
# Decline it
|
||||||
|
$SKILL_DIR/scripts/calendar-invite.sh reply \
|
||||||
|
--envelope-id <envelope-id> \
|
||||||
|
--action decline \
|
||||||
|
--comment "Testing decline flow."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify:**
|
||||||
|
- [ ] Reply sent to organizer with comment
|
||||||
|
- [ ] Event NOT forwarded to `mail@luyx.org`
|
||||||
|
- [ ] Event removed from local calendar
|
||||||
|
- [ ] `khal list "$TEST_DATE"` does NOT show "Decline Test Event"
|
||||||
|
|
||||||
|
## 6. Verify Final Calendar State
|
||||||
|
|
||||||
|
After all tests, confirm the calendar is in a clean state.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sync one more time
|
||||||
|
vdirsyncer sync
|
||||||
|
|
||||||
|
# Only the accepted event should remain
|
||||||
|
khal list "$TEST_DATE"
|
||||||
|
|
||||||
|
# List all upcoming events
|
||||||
|
khal list today 7d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Health Checks
|
||||||
|
|
||||||
|
Run these first if any step fails.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# icalendar library is available
|
||||||
|
uv run --project $SKILL_DIR python -c "import icalendar; print('ok')"
|
||||||
|
|
||||||
|
# himalaya can list emails
|
||||||
|
himalaya envelope list --page-size 5
|
||||||
|
|
||||||
|
# vdirsyncer can sync
|
||||||
|
vdirsyncer sync
|
||||||
|
|
||||||
|
# khal can read local calendar
|
||||||
|
khal list today 7d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Failures
|
||||||
|
|
||||||
|
| Symptom | Likely Cause |
|
||||||
|
|---------|-------------|
|
||||||
|
| `himalaya message send` errors | SMTP config issue, check `~/.config/himalaya/config.toml` |
|
||||||
|
| No `.ics` attachment found | Email doesn't have a calendar invite, or himalaya can't download attachments |
|
||||||
|
| `vdirsyncer sync` fails | Check credentials in `~/.config/vdirsyncer/config`, or server is unreachable |
|
||||||
|
| `ModuleNotFoundError: icalendar` | Run `uv sync --project $SKILL_DIR` to install dependencies |
|
||||||
|
| Invite shows as attachment (no Accept/Decline) | Check MIME `Content-Type` includes `method=REQUEST` |
|
||||||
|
| Event not in `khal list` after sync | Check `.ics` file exists in `~/.openclaw/workspace/calendars/home/` |
|
||||||
@@ -17,7 +17,11 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
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
|
# Config
|
||||||
@@ -65,13 +69,27 @@ def _sync_calendar():
|
|||||||
print("Warning: CalDAV sync failed (will retry on next heartbeat)")
|
print("Warning: CalDAV sync failed (will retry on next heartbeat)")
|
||||||
|
|
||||||
|
|
||||||
def _send_mml(mml, account=None):
|
def _send_email(email_str, account=None):
|
||||||
"""Send an MML message via himalaya template send."""
|
"""Send a raw MIME email via himalaya message send (stdin)."""
|
||||||
cmd = ["himalaya"]
|
cmd = ["himalaya"]
|
||||||
if account:
|
if account:
|
||||||
cmd += ["--account", account]
|
cmd += ["--account", account]
|
||||||
cmd += ["template", "send"]
|
cmd += ["message", "send"]
|
||||||
subprocess.run(cmd, input=mml, text=True, check=True)
|
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):
|
def _parse_iso_datetime(dt_str):
|
||||||
@@ -111,8 +129,9 @@ def cmd_send(args):
|
|||||||
event.add("summary", args.summary)
|
event.add("summary", args.summary)
|
||||||
event.add("status", "CONFIRMED")
|
event.add("status", "CONFIRMED")
|
||||||
event.add("sequence", 0)
|
event.add("sequence", 0)
|
||||||
event["organizer"] = f"mailto:{args.sender}"
|
organizer = vCalAddress(f"mailto:{args.sender}")
|
||||||
event["organizer"].params["CN"] = vText(organizer_name)
|
organizer.params["CN"] = vText(organizer_name)
|
||||||
|
event.add("organizer", organizer)
|
||||||
|
|
||||||
if args.location:
|
if args.location:
|
||||||
event.add("location", args.location)
|
event.add("location", args.location)
|
||||||
@@ -135,10 +154,6 @@ def cmd_send(args):
|
|||||||
cal.add_component(event)
|
cal.add_component(event)
|
||||||
ics_bytes = cal.to_ical()
|
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
|
# Build plain text body
|
||||||
body = f"You're invited to: {args.summary}\n\nWhen: {args.start} - {args.end} ({args.timezone})"
|
body = f"You're invited to: {args.summary}\n\nWhen: {args.start} - {args.end} ({args.timezone})"
|
||||||
if args.location:
|
if args.location:
|
||||||
@@ -149,29 +164,18 @@ def cmd_send(args):
|
|||||||
# Email goes to all attendees (including owner)
|
# Email goes to all attendees (including owner)
|
||||||
all_to = ", ".join(all_attendees)
|
all_to = ", ".join(all_attendees)
|
||||||
|
|
||||||
# Build MML message
|
# Build MIME email
|
||||||
mml = (
|
email_str = _build_calendar_email(args.sender, all_to, args.subject, body, ics_bytes, method="REQUEST")
|
||||||
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>"
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("=== ICS Content ===")
|
print("=== ICS Content ===")
|
||||||
print(ics_bytes.decode())
|
print(ics_bytes.decode())
|
||||||
print("=== MML Message ===")
|
print("=== Email Message ===")
|
||||||
print(mml)
|
print(email_str)
|
||||||
tmp_ics.unlink(missing_ok=True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Send email
|
# Send email via himalaya message send (stdin)
|
||||||
_send_mml(mml, args.account)
|
_send_email(email_str, args.account)
|
||||||
print(f"Calendar invite sent to: {args.to}")
|
print(f"Calendar invite sent to: {args.to}")
|
||||||
|
|
||||||
# Save to local calendar
|
# Save to local calendar
|
||||||
@@ -181,8 +185,6 @@ def cmd_send(args):
|
|||||||
print(f"Saved to local calendar: {dest}")
|
print(f"Saved to local calendar: {dest}")
|
||||||
_sync_calendar()
|
_sync_calendar()
|
||||||
|
|
||||||
tmp_ics.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Reply to invite
|
# Reply to invite
|
||||||
@@ -305,10 +307,6 @@ def cmd_reply(args):
|
|||||||
reply_cal.add_component(reply_event)
|
reply_cal.add_component(reply_event)
|
||||||
reply_ics_bytes = reply_cal.to_ical()
|
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
|
# Build email
|
||||||
prefix = SUBJECT_PREFIX[partstat]
|
prefix = SUBJECT_PREFIX[partstat]
|
||||||
subject = f"{prefix}: {summary}"
|
subject = f"{prefix}: {summary}"
|
||||||
@@ -317,17 +315,7 @@ def cmd_reply(args):
|
|||||||
if args.comment:
|
if args.comment:
|
||||||
body += f"\n\n{args.comment}"
|
body += f"\n\n{args.comment}"
|
||||||
|
|
||||||
mml = (
|
email_str = _build_calendar_email(args.sender, organizer_email, subject, body, reply_ics_bytes, method="REPLY")
|
||||||
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>"
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("=== Original Event ===")
|
print("=== Original Event ===")
|
||||||
@@ -337,9 +325,8 @@ def cmd_reply(args):
|
|||||||
print()
|
print()
|
||||||
print("=== Reply ICS ===")
|
print("=== Reply ICS ===")
|
||||||
print(reply_ics_bytes.decode())
|
print(reply_ics_bytes.decode())
|
||||||
print("=== MML Message ===")
|
print("=== Email Message ===")
|
||||||
print(mml)
|
print(email_str)
|
||||||
tmp_reply.unlink(missing_ok=True)
|
|
||||||
if cleanup_dir:
|
if cleanup_dir:
|
||||||
for f in cleanup_dir.iterdir():
|
for f in cleanup_dir.iterdir():
|
||||||
f.unlink()
|
f.unlink()
|
||||||
@@ -347,30 +334,22 @@ def cmd_reply(args):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Send reply
|
# Send reply
|
||||||
_send_mml(mml, args.account)
|
_send_email(email_str, args.account)
|
||||||
print(f"Calendar invite {partstat.lower()}: {summary} (replied to {organizer_email})")
|
print(f"Calendar invite {partstat.lower()}: {summary} (replied to {organizer_email})")
|
||||||
|
|
||||||
# Forward invite to owner on accept/tentative
|
# Forward invite to owner on accept/tentative
|
||||||
if partstat in ("ACCEPTED", "TENTATIVE"):
|
if partstat in ("ACCEPTED", "TENTATIVE"):
|
||||||
tmp_fwd = Path(f"/tmp/openclaw-fwd-{int(datetime.now().timestamp())}.ics")
|
fwd_body = f"{prefix}: {summary}"
|
||||||
tmp_fwd.write_bytes(ics_path.read_bytes())
|
fwd_email = _build_calendar_email(
|
||||||
fwd_mml = (
|
args.sender, DEFAULT_OWNER_EMAIL,
|
||||||
f"From: {args.sender}\n"
|
f"{prefix}: {summary}", fwd_body,
|
||||||
f"To: {DEFAULT_OWNER_EMAIL}\n"
|
ics_path.read_bytes(), method="REQUEST",
|
||||||
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>"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
_send_mml(fwd_mml, args.account)
|
_send_email(fwd_email, args.account)
|
||||||
print(f"Forwarded invite to {DEFAULT_OWNER_EMAIL}")
|
print(f"Forwarded invite to {DEFAULT_OWNER_EMAIL}")
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
print(f"Warning: Failed to forward invite to {DEFAULT_OWNER_EMAIL}")
|
print(f"Warning: Failed to forward invite to {DEFAULT_OWNER_EMAIL}")
|
||||||
tmp_fwd.unlink(missing_ok=True)
|
|
||||||
|
|
||||||
# Save to / remove from local calendar
|
# Save to / remove from local calendar
|
||||||
if CALENDAR_DIR.is_dir():
|
if CALENDAR_DIR.is_dir():
|
||||||
@@ -385,7 +364,6 @@ def cmd_reply(args):
|
|||||||
_sync_calendar()
|
_sync_calendar()
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
tmp_reply.unlink(missing_ok=True)
|
|
||||||
if cleanup_dir:
|
if cleanup_dir:
|
||||||
for f in cleanup_dir.iterdir():
|
for f in cleanup_dir.iterdir():
|
||||||
f.unlink()
|
f.unlink()
|
||||||
|
|||||||
Reference in New Issue
Block a user