calendar: remove self-notification emails (CalDAV sync replaces them)
Remove three redundant email flows since all devices sync via CalDAV: - Auto-adding mail@luyx.org as attendee on outgoing invites - Forwarding invites to mail@luyx.org on accept/tentative - Emailing todos to mail@luyx.org on todo add
This commit is contained in:
@@ -5,7 +5,7 @@ _这份文件记录持续性项目和重要状态,跨会话保留。_
|
|||||||
## 📝 重要规则
|
## 📝 重要规则
|
||||||
|
|
||||||
### 邮件发送规则(v2)
|
### 邮件发送规则(v2)
|
||||||
- **youlu@luyanxin.com → mail@luyx.org**: 直接发送,无需确认(用户 SimpleLogin 别名,日历邀请自动抄送)
|
- **youlu@luyanxin.com → mail@luyx.org**: 直接发送,无需确认(用户 SimpleLogin 别名)
|
||||||
- 其他所有对外邮件: 仍需确认
|
- 其他所有对外邮件: 仍需确认
|
||||||
|
|
||||||
### 代码审查规则
|
### 代码审查规则
|
||||||
@@ -88,8 +88,8 @@ _这份文件记录持续性项目和重要状态,跨会话保留。_
|
|||||||
- 运行方式: `uv run`(依赖 `icalendar` 库)
|
- 运行方式: `uv run`(依赖 `icalendar` 库)
|
||||||
|
|
||||||
**功能**:
|
**功能**:
|
||||||
- 发送日历邀请(自动添加 mail@luyx.org 为参与者)
|
- 发送日历邀请给外部收件人
|
||||||
- 接受/拒绝/暂定回复邀请(自动转发给 mail@luyx.org)
|
- 接受/拒绝/暂定回复邀请(回复给 organizer)
|
||||||
- VTODO 待办管理(add/list/complete/delete/check)
|
- VTODO 待办管理(add/list/complete/delete/check)
|
||||||
- 发送/回复/待办操作后自动 `vdirsyncer sync` 同步到 CalDAV
|
- 发送/回复/待办操作后自动 `vdirsyncer sync` 同步到 CalDAV
|
||||||
- 心跳定期同步日历
|
- 心跳定期同步日历
|
||||||
|
|||||||
3
TOOLS.md
3
TOOLS.md
@@ -154,8 +154,7 @@ khal list today 7d
|
|||||||
**支持操作**: 发送邀请 (`send`)、接受/拒绝/暂定 (`reply`)、待办管理 (`todo add/list/complete/delete/check`)
|
**支持操作**: 发送邀请 (`send`)、接受/拒绝/暂定 (`reply`)、待办管理 (`todo add/list/complete/delete/check`)
|
||||||
**依赖**: himalaya(邮件)、vdirsyncer(CalDAV 同步)、khal(查看日历)、todoman(待办管理)
|
**依赖**: himalaya(邮件)、vdirsyncer(CalDAV 同步)、khal(查看日历)、todoman(待办管理)
|
||||||
**同步**: 发送/回复后自动 `vdirsyncer sync`,心跳也会定期同步
|
**同步**: 发送/回复后自动 `vdirsyncer sync`,心跳也会定期同步
|
||||||
**自动抄送**: mail@luyx.org(用户别名)自动加入所有邀请
|
**注意**: 发送日历邀请属于对外邮件,需先确认
|
||||||
**注意**: 发送日历邀请属于对外邮件,除 mail@luyx.org 外需先确认
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ Calendar invites are outbound emails. Follow the workspace email rules:
|
|||||||
- **youlu@luyanxin.com -> mail@luyx.org**: send directly, no confirmation needed
|
- **youlu@luyanxin.com -> mail@luyx.org**: send directly, no confirmation needed
|
||||||
- **All other recipients**: confirm with user before sending
|
- **All other recipients**: confirm with user before sending
|
||||||
|
|
||||||
## Owner Auto-Attendee
|
|
||||||
|
|
||||||
When sending invites, `mail@luyx.org` (owner's SimpleLogin alias) is **always added as an attendee automatically**. All invites include a **1-day reminder** (VALARM) by default. This ensures the owner receives every invite and can Accept/Decline from their own email client. No need to include it in `--to` — it's added by the script.
|
|
||||||
|
|
||||||
When accepting or tentatively accepting a received invite, the original invite is **automatically forwarded to `mail@luyx.org`** so the event lands on the owner's calendar too.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
All commands go through the wrapper script:
|
All commands go through the wrapper script:
|
||||||
@@ -167,7 +161,7 @@ Manage tasks as RFC 5545 VTODO components, stored in `~/.openclaw/workspace/cale
|
|||||||
|
|
||||||
### Sync Model
|
### Sync Model
|
||||||
|
|
||||||
The agent's local CalDAV is the **source of truth** (no two-way sync). When a todo is created, it's saved locally and emailed to `mail@luyx.org` as a delivery copy. When the user completes a task, they tell the agent, and the agent runs `todo complete`. The daily `todo check` cron reads from local files.
|
The agent's local CalDAV is the **source of truth**. When a todo is created, it's saved locally and synced to Migadu CalDAV via vdirsyncer — all connected devices (DAVx5, etc.) pick it up automatically. When the user completes a task, they tell the agent, and the agent runs `todo complete`. The daily `todo check` cron reads from local files.
|
||||||
|
|
||||||
### Priority Mapping (RFC 5545)
|
### Priority Mapping (RFC 5545)
|
||||||
|
|
||||||
@@ -195,7 +189,7 @@ $SKILL_DIR/scripts/calendar.sh todo add \
|
|||||||
| `--priority` | No | `high`, `medium`, or `low` (default: `medium`) |
|
| `--priority` | No | `high`, `medium`, or `low` (default: `medium`) |
|
||||||
| `--description` | No | Notes / description |
|
| `--description` | No | Notes / description |
|
||||||
| `--alarm` | No | Reminder trigger: `1d`, `2h`, `30m` (default: `1d`) |
|
| `--alarm` | No | Reminder trigger: `1d`, `2h`, `30m` (default: `1d`) |
|
||||||
| `--dry-run` | No | Preview ICS + email without saving |
|
| `--dry-run` | No | Preview ICS without saving |
|
||||||
|
|
||||||
### `todo list` — List Todos
|
### `todo list` — List Todos
|
||||||
|
|
||||||
@@ -246,7 +240,7 @@ Same as `todo list` but only NEEDS-ACTION items. Exits silently when no pending
|
|||||||
6. Runs `vdirsyncer sync` to push changes to Migadu CalDAV
|
6. Runs `vdirsyncer sync` to push changes to Migadu CalDAV
|
||||||
|
|
||||||
**Managing todos:**
|
**Managing todos:**
|
||||||
1. `todo add`: Creates a VTODO ICS file (via `icalendar` library), saves to `calendars/tasks/`, emails to owner, syncs
|
1. `todo add`: Creates a VTODO ICS file (via `icalendar` library), saves to `calendars/tasks/`, syncs to CalDAV
|
||||||
2. `todo list/complete/delete/check`: Delegates to `todoman` CLI for robust RFC 5545 VTODO parsing
|
2. `todo list/complete/delete/check`: Delegates to `todoman` CLI for robust RFC 5545 VTODO parsing
|
||||||
3. Runs `vdirsyncer sync` after mutations to push changes to Migadu CalDAV
|
3. Runs `vdirsyncer sync` after mutations to push changes to Migadu CalDAV
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ TEST_DATE=$(date -d "+3 days" +%Y-%m-%d)
|
|||||||
Generates the ICS and MIME email without sending. Check that:
|
Generates the ICS and MIME email without sending. Check that:
|
||||||
- ICS has `METHOD:REQUEST`
|
- ICS has `METHOD:REQUEST`
|
||||||
- MIME has `Content-Type: text/calendar; method=REQUEST`
|
- MIME has `Content-Type: text/calendar; method=REQUEST`
|
||||||
- `mail@luyx.org` appears as attendee (auto-added)
|
- Only `--to` recipients appear as attendees
|
||||||
- Times and timezone look correct
|
- Times and timezone look correct
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -89,8 +89,7 @@ $SKILL_DIR/scripts/calendar.sh reply \
|
|||||||
|
|
||||||
**Verify:**
|
**Verify:**
|
||||||
- [ ] Reply sent to organizer (youlu@luyanxin.com, i.e. ourselves)
|
- [ ] Reply sent to organizer (youlu@luyanxin.com, i.e. ourselves)
|
||||||
- [ ] Original invite forwarded to `mail@luyx.org`
|
- [ ] Event saved to `~/.openclaw/workspace/calendars/home/`
|
||||||
- [ ] Event still in `~/.openclaw/workspace/calendars/home/`
|
|
||||||
- [ ] `vdirsyncer sync` ran
|
- [ ] `vdirsyncer sync` ran
|
||||||
- [ ] `khal list "$TEST_DATE"` still shows the event
|
- [ ] `khal list "$TEST_DATE"` still shows the event
|
||||||
|
|
||||||
@@ -119,7 +118,6 @@ $SKILL_DIR/scripts/calendar.sh reply \
|
|||||||
|
|
||||||
**Verify:**
|
**Verify:**
|
||||||
- [ ] Reply sent to organizer with comment
|
- [ ] Reply sent to organizer with comment
|
||||||
- [ ] Event NOT forwarded to `mail@luyx.org`
|
|
||||||
- [ ] Event removed from local calendar
|
- [ ] Event removed from local calendar
|
||||||
- [ ] `khal list "$TEST_DATE"` does NOT show "Decline Test Event"
|
- [ ] `khal list "$TEST_DATE"` does NOT show "Decline Test Event"
|
||||||
|
|
||||||
@@ -142,12 +140,11 @@ khal list today 7d
|
|||||||
|
|
||||||
## 7. Dry Run: Add Todo
|
## 7. Dry Run: Add Todo
|
||||||
|
|
||||||
Generates the VTODO ICS and MIME email without saving. Check that:
|
Generates the VTODO ICS without saving. Check that:
|
||||||
- ICS has `BEGIN:VTODO`
|
- ICS has `BEGIN:VTODO`
|
||||||
- ICS has correct `PRIORITY` value (1 for high)
|
- ICS has correct `PRIORITY` value (1 for high)
|
||||||
- ICS has `STATUS:NEEDS-ACTION`
|
- ICS has `STATUS:NEEDS-ACTION`
|
||||||
- ICS has `BEGIN:VALARM`
|
- ICS has `BEGIN:VALARM`
|
||||||
- MIME has `Content-Type: text/calendar`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$SKILL_DIR/scripts/calendar.sh todo add \
|
$SKILL_DIR/scripts/calendar.sh todo add \
|
||||||
@@ -171,7 +168,6 @@ $SKILL_DIR/scripts/calendar.sh todo add \
|
|||||||
- [ ] Script exits without error
|
- [ ] Script exits without error
|
||||||
- [ ] `.ics` file created in `~/.openclaw/workspace/calendars/tasks/`
|
- [ ] `.ics` file created in `~/.openclaw/workspace/calendars/tasks/`
|
||||||
- [ ] `todo list` (todoman directly) shows "Test Todo"
|
- [ ] `todo list` (todoman directly) shows "Test Todo"
|
||||||
- [ ] Email arrives at `mail@luyx.org` with .ics attachment
|
|
||||||
- [ ] `vdirsyncer sync` ran
|
- [ ] `vdirsyncer sync` ran
|
||||||
|
|
||||||
## 9. List Todos
|
## 9. List Todos
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ from icalendar import Alarm, Calendar, Event, Todo, vCalAddress, vText
|
|||||||
|
|
||||||
DEFAULT_TIMEZONE = "America/Los_Angeles"
|
DEFAULT_TIMEZONE = "America/Los_Angeles"
|
||||||
DEFAULT_FROM = "youlu@luyanxin.com"
|
DEFAULT_FROM = "youlu@luyanxin.com"
|
||||||
DEFAULT_OWNER_EMAIL = "mail@luyx.org" # Always added as attendee
|
|
||||||
CALENDAR_DIR = Path.home() / ".openclaw" / "workspace" / "calendars" / "home"
|
CALENDAR_DIR = Path.home() / ".openclaw" / "workspace" / "calendars" / "home"
|
||||||
TASKS_DIR = Path.home() / ".openclaw" / "workspace" / "calendars" / "tasks"
|
TASKS_DIR = Path.home() / ".openclaw" / "workspace" / "calendars" / "tasks"
|
||||||
PRODID = "-//OpenClaw//Calendar//EN"
|
PRODID = "-//OpenClaw//Calendar//EN"
|
||||||
@@ -148,12 +147,7 @@ def cmd_send(args):
|
|||||||
|
|
||||||
recipients = [addr.strip() for addr in args.to.split(",")]
|
recipients = [addr.strip() for addr in args.to.split(",")]
|
||||||
|
|
||||||
# Always include owner as attendee
|
for addr in recipients:
|
||||||
all_attendees = list(recipients)
|
|
||||||
if DEFAULT_OWNER_EMAIL not in all_attendees:
|
|
||||||
all_attendees.append(DEFAULT_OWNER_EMAIL)
|
|
||||||
|
|
||||||
for addr in all_attendees:
|
|
||||||
event.add("attendee", f"mailto:{addr}", parameters={
|
event.add("attendee", f"mailto:{addr}", parameters={
|
||||||
"ROLE": "REQ-PARTICIPANT",
|
"ROLE": "REQ-PARTICIPANT",
|
||||||
"RSVP": "TRUE",
|
"RSVP": "TRUE",
|
||||||
@@ -176,11 +170,8 @@ def cmd_send(args):
|
|||||||
if args.description:
|
if args.description:
|
||||||
body += f"\n\n{args.description}"
|
body += f"\n\n{args.description}"
|
||||||
|
|
||||||
# Email goes to all attendees (including owner)
|
|
||||||
all_to = ", ".join(all_attendees)
|
|
||||||
|
|
||||||
# Build MIME email
|
# Build MIME email
|
||||||
email_str = _build_calendar_email(args.sender, all_to, args.subject, body, ics_bytes, method="REQUEST")
|
email_str = _build_calendar_email(args.sender, ", ".join(recipients), args.subject, body, ics_bytes, method="REQUEST")
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("=== ICS Content ===")
|
print("=== ICS Content ===")
|
||||||
@@ -352,20 +343,6 @@ def cmd_reply(args):
|
|||||||
_send_email(email_str, 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
|
|
||||||
if partstat in ("ACCEPTED", "TENTATIVE"):
|
|
||||||
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_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}")
|
|
||||||
|
|
||||||
# Save to / remove from local calendar
|
# Save to / remove from local calendar
|
||||||
if CALENDAR_DIR.is_dir():
|
if CALENDAR_DIR.is_dir():
|
||||||
dest = CALENDAR_DIR / f"{uid}.ics"
|
dest = CALENDAR_DIR / f"{uid}.ics"
|
||||||
@@ -526,24 +503,11 @@ def cmd_todo_add(args):
|
|||||||
cal.add_component(todo)
|
cal.add_component(todo)
|
||||||
ics_bytes = cal.to_ical()
|
ics_bytes = cal.to_ical()
|
||||||
|
|
||||||
# Build email body
|
|
||||||
prio_label = PRIORITY_LABELS.get(priority, "中")
|
prio_label = PRIORITY_LABELS.get(priority, "中")
|
||||||
body = f"待办事项: {args.summary}\n截止日期: {due_date}\n优先级: {prio_label}"
|
|
||||||
if args.description:
|
|
||||||
body += f"\n\n{args.description}"
|
|
||||||
|
|
||||||
# Build MIME email
|
|
||||||
email_str = _build_calendar_email(
|
|
||||||
DEFAULT_FROM, DEFAULT_OWNER_EMAIL,
|
|
||||||
f"📋 待办: {args.summary}",
|
|
||||||
body, ics_bytes, method="REQUEST",
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print("=== ICS Content ===")
|
print("=== ICS Content ===")
|
||||||
print(ics_bytes.decode())
|
print(ics_bytes.decode())
|
||||||
print("=== Email Message ===")
|
|
||||||
print(email_str)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Save to TASKS_DIR (without METHOD for CalDAV)
|
# Save to TASKS_DIR (without METHOD for CalDAV)
|
||||||
@@ -555,13 +519,6 @@ def cmd_todo_add(args):
|
|||||||
# Sync
|
# Sync
|
||||||
_sync_calendar()
|
_sync_calendar()
|
||||||
|
|
||||||
# Email the VTODO to owner
|
|
||||||
try:
|
|
||||||
_send_email(email_str)
|
|
||||||
print(f"Emailed todo to {DEFAULT_OWNER_EMAIL}")
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print(f"Warning: Failed to email todo to {DEFAULT_OWNER_EMAIL}")
|
|
||||||
|
|
||||||
|
|
||||||
def _format_todo_digest(todos):
|
def _format_todo_digest(todos):
|
||||||
"""Format todos into the Chinese priority-grouped digest. Returns string or None."""
|
"""Format todos into the Chinese priority-grouped digest. Returns string or None."""
|
||||||
|
|||||||
Reference in New Issue
Block a user