feat(n8n-webhook): add gmail draft list/delete/send approval flows
This commit is contained in:
@@ -34,6 +34,12 @@ Keep the integration narrow: let OpenClaw decide what to do, and let n8n execute
|
||||
- `assets/test-append-log.json`
|
||||
- `assets/test-notify.json`
|
||||
- `assets/test-send-notification-draft.json`
|
||||
- `assets/test-send-email-draft.json`
|
||||
- `assets/test-list-email-drafts.json`
|
||||
- `assets/test-delete-email-draft.json`
|
||||
- `assets/test-send-gmail-draft.json`
|
||||
- `assets/test-send-approved-email.json`
|
||||
- `assets/test-create-calendar-event.json`
|
||||
|
||||
## Quick usage
|
||||
|
||||
@@ -54,6 +60,7 @@ Call the preferred action-bus route:
|
||||
```bash
|
||||
scripts/call-action.sh append_log --args '{"text":"backup complete"}' --request-id auto
|
||||
scripts/call-action.sh get_logs --args '{"limit":5}' --pretty
|
||||
scripts/call-action.sh list_email_drafts --args '{"max":10}' --pretty
|
||||
```
|
||||
|
||||
Call a test webhook while editing a flow:
|
||||
@@ -104,7 +111,10 @@ Use the included workflow asset when you want a ready-made local router for:
|
||||
- `get_logs` → read the most recent retained records from `actionLog`
|
||||
- `notify` → send through the current Telegram + Discord notification paths
|
||||
- `send_notification_draft` → queue approval-gated notifications that execute on approve through Telegram + Discord
|
||||
- `send_email_draft` → queue approval-gated email drafts in workflow static data
|
||||
- `send_email_draft` → queue approval-gated email draft creation proposals in workflow static data
|
||||
- `list_email_drafts` → queue approval-gated Gmail draft list requests (read-only, low mutation level)
|
||||
- `delete_email_draft` → queue approval-gated Gmail draft deletion requests
|
||||
- `send_gmail_draft` (alias: `send_approved_email`) → queue approval-gated Gmail draft send requests
|
||||
- `create_calendar_event` → queue approval-gated calendar proposals in workflow static data
|
||||
- `approval_queue_add` / `approval_queue_list` / `approval_queue_resolve` → manage pending approvals and recent history
|
||||
- `approval_history_attach_execution` → let a host-side executor attach real execution metadata back onto approval history entries
|
||||
@@ -128,6 +138,13 @@ When email/calendar provider creds live on the host via `gog` rather than inside
|
||||
python3 scripts/resolve-approval-with-gog.py --id <approval-id> --decision approve
|
||||
```
|
||||
|
||||
Supported host-executed approval kinds:
|
||||
- `email_draft` → `gog gmail drafts create`
|
||||
- `email_list_drafts` → `gog gmail drafts list`
|
||||
- `email_draft_delete` → `gog gmail drafts delete`
|
||||
- `email_draft_send` → `gog gmail drafts send`
|
||||
- `calendar_event` → `gog calendar create`
|
||||
|
||||
Practical note:
|
||||
- unattended execution needs `GOG_KEYRING_PASSWORD` available to the executor because `gog`'s file keyring cannot prompt in non-TTY automation
|
||||
- the included bridge auto-loads `/home/openclaw/.openclaw/credentials/gog.env` when present, so you can keep `GOG_ACCOUNT` and `GOG_KEYRING_PASSWORD` there with mode `600`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"action": "delete_email_draft",
|
||||
"request_id": "test-delete-email-draft-001",
|
||||
"args": {
|
||||
"draft_id": "r-example-draft-id"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"action": "list_email_drafts",
|
||||
"request_id": "test-list-email-drafts-001",
|
||||
"args": {
|
||||
"max": 10,
|
||||
"all": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"action": "send_approved_email",
|
||||
"request_id": "test-send-approved-email-001",
|
||||
"args": {
|
||||
"draft_id": "r-example-draft-id"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"action": "send_gmail_draft",
|
||||
"request_id": "test-send-gmail-draft-001",
|
||||
"args": {
|
||||
"draft_id": "r-example-draft-id"
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,9 @@ It implements a real local OpenClaw → n8n router.
|
||||
- `notify`
|
||||
- `send_notification_draft`
|
||||
- `send_email_draft`
|
||||
- `list_email_drafts`
|
||||
- `delete_email_draft`
|
||||
- `send_gmail_draft` (alias: `send_approved_email`)
|
||||
- `create_calendar_event`
|
||||
- `approval_queue_add`
|
||||
- `approval_queue_list`
|
||||
@@ -49,13 +52,30 @@ Example stored record:
|
||||
- when resolved with `decision=approve`, it executes the existing `notify` path and sends through Telegram + Discord
|
||||
- uses only the already-configured notification credentials in the live n8n instance
|
||||
|
||||
### `send_email_draft` and `create_calendar_event`
|
||||
### Gmail + Calendar approval-gated actions
|
||||
|
||||
- queue approval-gated proposals into workflow static data under key:
|
||||
Actions:
|
||||
- `send_email_draft`
|
||||
- `list_email_drafts`
|
||||
- `delete_email_draft`
|
||||
- `send_gmail_draft` (alias: `send_approved_email`)
|
||||
- `create_calendar_event`
|
||||
|
||||
Behavior:
|
||||
- queue proposals into workflow static data under key:
|
||||
- `approvalQueue`
|
||||
- keep the most recent `200` pending entries
|
||||
- do **not** send email or create provider-side calendar events in the shipped starter workflow
|
||||
- are designed to become safe provider-backed executors later once instance-local creds are bound in n8n
|
||||
- return explicit approval policy metadata per action (`approval.policy`, `approval.required`, `approval.mutation_level`)
|
||||
- do **not** execute Gmail/Calendar side effects directly in the shipped starter workflow
|
||||
- are intended for host-side execution via the included `gog` bridge after explicit approval resolution
|
||||
|
||||
Approval policy defaults:
|
||||
- `send_email_draft`, `delete_email_draft`, `send_gmail_draft` / `send_approved_email`, `create_calendar_event`
|
||||
- `approval.required = true`
|
||||
- `approval.mutation_level = "high"`
|
||||
- `list_email_drafts`
|
||||
- `approval.required = true`
|
||||
- `approval.mutation_level = "low"` (read-only action, still routed through approval queue for explicit operator acknowledgement + audit trail)
|
||||
|
||||
### `approval_queue_resolve`
|
||||
|
||||
@@ -147,6 +167,10 @@ After import, set this manually in n8n:
|
||||
- `assets/test-notify.json`
|
||||
- `assets/test-send-notification-draft.json`
|
||||
- `assets/test-send-email-draft.json`
|
||||
- `assets/test-list-email-drafts.json`
|
||||
- `assets/test-delete-email-draft.json`
|
||||
- `assets/test-send-gmail-draft.json`
|
||||
- `assets/test-send-approved-email.json`
|
||||
- `assets/test-create-calendar-event.json`
|
||||
- `assets/test-fetch-and-normalize-url.json`
|
||||
- `assets/test-approval-queue-list.json`
|
||||
@@ -161,6 +185,10 @@ scripts/call-action.sh get_logs --args '{"limit":5}' --pretty
|
||||
scripts/call-action.sh notify --args '{"title":"Workflow finished","message":"n8n router test"}' --pretty
|
||||
scripts/call-action.sh send_notification_draft --args-file assets/test-send-notification-draft.json --pretty
|
||||
scripts/call-action.sh send_email_draft --args-file assets/test-send-email-draft.json --pretty
|
||||
scripts/call-action.sh list_email_drafts --args-file assets/test-list-email-drafts.json --pretty
|
||||
scripts/call-action.sh delete_email_draft --args-file assets/test-delete-email-draft.json --pretty
|
||||
scripts/call-action.sh send_gmail_draft --args-file assets/test-send-gmail-draft.json --pretty
|
||||
scripts/call-action.sh send_approved_email --args-file assets/test-send-approved-email.json --pretty
|
||||
scripts/call-action.sh create_calendar_event --args-file assets/test-create-calendar-event.json --pretty
|
||||
scripts/call-action.sh fetch_and_normalize_url --args '{"url":"http://192.168.153.113:18808/healthz"}' --pretty
|
||||
scripts/call-action.sh fetch_and_normalize_url --args '{"url":"https://example.com","skip_ssl_certificate_validation":true}' --pretty
|
||||
@@ -201,6 +229,57 @@ python3 scripts/resolve-approval-with-gog.py --id <approval-id> --decision appro
|
||||
}
|
||||
```
|
||||
|
||||
### list_email_drafts
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_id": "test-list-email-drafts-001",
|
||||
"result": {
|
||||
"action": "list_email_drafts",
|
||||
"status": "queued_for_approval",
|
||||
"pending_id": "approval-ghi789",
|
||||
"approval_status": "pending",
|
||||
"approval": {
|
||||
"policy": "approval_queue_resolve",
|
||||
"required": true,
|
||||
"mutation_level": "low"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### delete_email_draft
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_id": "test-delete-email-draft-001",
|
||||
"result": {
|
||||
"action": "delete_email_draft",
|
||||
"status": "queued_for_approval",
|
||||
"pending_id": "approval-jkl012",
|
||||
"approval_status": "pending"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### send_gmail_draft / send_approved_email
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_id": "test-send-gmail-draft-001",
|
||||
"result": {
|
||||
"action": "send_gmail_draft",
|
||||
"requested_action": "send_gmail_draft",
|
||||
"status": "queued_for_approval",
|
||||
"pending_id": "approval-mno345",
|
||||
"approval_status": "pending"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### create_calendar_event
|
||||
|
||||
```json
|
||||
@@ -256,6 +335,9 @@ Behavior:
|
||||
- resolves an approval item through `openclaw-action`
|
||||
- executes supported kinds on the host:
|
||||
- `email_draft` → `gog gmail drafts create`
|
||||
- `email_list_drafts` → `gog gmail drafts list`
|
||||
- `email_draft_delete` → `gog gmail drafts delete`
|
||||
- `email_draft_send` → `gog gmail drafts send`
|
||||
- `calendar_event` → `gog calendar create`
|
||||
- writes execution metadata back via `approval_history_attach_execution`
|
||||
|
||||
|
||||
@@ -150,11 +150,76 @@ Purpose:
|
||||
- queue an email draft proposal for approval
|
||||
- does **not** send mail directly in the shipped starter workflow
|
||||
|
||||
Approval policy:
|
||||
- required: `true`
|
||||
- mutation level: `high`
|
||||
|
||||
Sink:
|
||||
- type: `workflow-static-data`
|
||||
- key: `approvalQueue`
|
||||
- retained entries: `200`
|
||||
|
||||
### `list_email_drafts`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "list_email_drafts",
|
||||
"args": {
|
||||
"max": 20,
|
||||
"all": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Purpose:
|
||||
- queue a host-side Gmail draft listing request for approval/audit
|
||||
|
||||
Approval policy:
|
||||
- required: `true`
|
||||
- mutation level: `low` (read-only)
|
||||
|
||||
### `delete_email_draft`
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "delete_email_draft",
|
||||
"args": {
|
||||
"draft_id": "r-example-draft-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Purpose:
|
||||
- queue deletion of a Gmail draft behind explicit approval
|
||||
|
||||
Approval policy:
|
||||
- required: `true`
|
||||
- mutation level: `high`
|
||||
|
||||
### `send_gmail_draft` (alias: `send_approved_email`)
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "send_gmail_draft",
|
||||
"args": {
|
||||
"draft_id": "r-example-draft-id"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Purpose:
|
||||
- queue sending of an existing Gmail draft behind explicit approval
|
||||
|
||||
Approval policy:
|
||||
- required: `true`
|
||||
- mutation level: `high`
|
||||
|
||||
### `create_calendar_event`
|
||||
|
||||
Request:
|
||||
@@ -176,6 +241,10 @@ Purpose:
|
||||
- queue a calendar event proposal for approval
|
||||
- does **not** write to a calendar provider directly in the shipped starter workflow
|
||||
|
||||
Approval policy:
|
||||
- required: `true`
|
||||
- mutation level: `high`
|
||||
|
||||
Sink:
|
||||
- type: `workflow-static-data`
|
||||
- key: `approvalQueue`
|
||||
|
||||
@@ -103,7 +103,7 @@ def attach_execution(item_id: str, execution: dict, *, base_url: str, path: str,
|
||||
)
|
||||
|
||||
|
||||
def build_email_command(item: dict, account: str, dry_run: bool):
|
||||
def build_email_draft_create_command(item: dict, account: str, dry_run: bool):
|
||||
payload = item.get('payload') or {}
|
||||
body_text = payload.get('body_text') or ''
|
||||
body_html = payload.get('body_html') or ''
|
||||
@@ -137,6 +137,68 @@ def build_email_command(item: dict, account: str, dry_run: bool):
|
||||
return cmd, tmp.name if tmp else None
|
||||
|
||||
|
||||
def build_email_draft_delete_command(item: dict, account: str, dry_run: bool):
|
||||
payload = item.get('payload') or {}
|
||||
draft_id = (payload.get('draft_id') or payload.get('id') or '').strip()
|
||||
if not draft_id:
|
||||
fail('email_draft_delete payload missing draft_id')
|
||||
cmd = [
|
||||
'gog', 'gmail', 'drafts', 'delete', draft_id,
|
||||
'--account', account,
|
||||
'--json',
|
||||
'--no-input',
|
||||
'--force',
|
||||
]
|
||||
if dry_run:
|
||||
cmd.append('--dry-run')
|
||||
return cmd
|
||||
|
||||
|
||||
def build_email_draft_send_command(item: dict, account: str, dry_run: bool):
|
||||
payload = item.get('payload') or {}
|
||||
draft_id = (payload.get('draft_id') or payload.get('id') or '').strip()
|
||||
if not draft_id:
|
||||
fail('email_draft_send payload missing draft_id')
|
||||
cmd = [
|
||||
'gog', 'gmail', 'drafts', 'send', draft_id,
|
||||
'--account', account,
|
||||
'--json',
|
||||
'--no-input',
|
||||
]
|
||||
if dry_run:
|
||||
cmd.append('--dry-run')
|
||||
return cmd
|
||||
|
||||
|
||||
def build_email_drafts_list_command(item: dict, account: str, dry_run: bool):
|
||||
payload = item.get('payload') or {}
|
||||
max_results = payload.get('max')
|
||||
if max_results is None:
|
||||
max_results = 20
|
||||
try:
|
||||
max_results = max(1, min(100, int(max_results)))
|
||||
except Exception:
|
||||
max_results = 20
|
||||
|
||||
cmd = [
|
||||
'gog', 'gmail', 'drafts', 'list',
|
||||
'--account', account,
|
||||
'--json',
|
||||
'--no-input',
|
||||
'--max', str(max_results),
|
||||
]
|
||||
page = (payload.get('page') or '').strip()
|
||||
if page:
|
||||
cmd.extend(['--page', page])
|
||||
if payload.get('all') is True:
|
||||
cmd.append('--all')
|
||||
if payload.get('fail_empty') is True:
|
||||
cmd.append('--fail-empty')
|
||||
if dry_run:
|
||||
cmd.append('--dry-run')
|
||||
return cmd
|
||||
|
||||
|
||||
def build_calendar_command(item: dict, account: str, dry_run: bool):
|
||||
payload = item.get('payload') or {}
|
||||
calendar = payload.get('calendar') or 'primary'
|
||||
@@ -211,7 +273,41 @@ def main():
|
||||
print(json.dumps({'resolved': resolved, 'executed': True, 'driver': 'n8n'}, indent=2))
|
||||
return
|
||||
|
||||
if kind not in {'email_draft', 'calendar_event'}:
|
||||
executors = {
|
||||
'email_draft': {
|
||||
'builder': build_email_draft_create_command,
|
||||
'op': 'gmail.drafts.create',
|
||||
'success_status': 'draft_created',
|
||||
'uses_tmpfile': True,
|
||||
},
|
||||
'email_list_drafts': {
|
||||
'builder': build_email_drafts_list_command,
|
||||
'op': 'gmail.drafts.list',
|
||||
'success_status': 'drafts_listed',
|
||||
'uses_tmpfile': False,
|
||||
},
|
||||
'email_draft_delete': {
|
||||
'builder': build_email_draft_delete_command,
|
||||
'op': 'gmail.drafts.delete',
|
||||
'success_status': 'draft_deleted',
|
||||
'uses_tmpfile': False,
|
||||
},
|
||||
'email_draft_send': {
|
||||
'builder': build_email_draft_send_command,
|
||||
'op': 'gmail.drafts.send',
|
||||
'success_status': 'draft_sent',
|
||||
'uses_tmpfile': False,
|
||||
},
|
||||
'calendar_event': {
|
||||
'builder': build_calendar_command,
|
||||
'op': 'calendar.create',
|
||||
'success_status': 'event_created',
|
||||
'uses_tmpfile': False,
|
||||
},
|
||||
}
|
||||
|
||||
spec = executors.get(kind)
|
||||
if not spec:
|
||||
print(json.dumps({'resolved': resolved, 'executed': False, 'reason': f'no host executor for kind {kind}'}, indent=2))
|
||||
return
|
||||
|
||||
@@ -219,15 +315,14 @@ def main():
|
||||
env = os.environ.copy()
|
||||
env['GOG_ACCOUNT'] = account
|
||||
|
||||
if kind == 'email_draft':
|
||||
cmd, tmpfile = build_email_command(item, account, args.dry_run)
|
||||
op = 'gmail.drafts.create'
|
||||
success_status = 'draft_created' if not args.dry_run else 'dry_run'
|
||||
tmpfile = None
|
||||
if spec['uses_tmpfile']:
|
||||
cmd, tmpfile = spec['builder'](item, account, args.dry_run)
|
||||
else:
|
||||
cmd = build_calendar_command(item, account, args.dry_run)
|
||||
tmpfile = None
|
||||
op = 'calendar.create'
|
||||
success_status = 'event_created' if not args.dry_run else 'dry_run'
|
||||
cmd = spec['builder'](item, account, args.dry_run)
|
||||
|
||||
op = spec['op']
|
||||
success_status = spec['success_status'] if not args.dry_run else 'dry_run'
|
||||
|
||||
try:
|
||||
code, stdout, stderr = run(cmd, env=env)
|
||||
|
||||
@@ -26,6 +26,10 @@ SAMPLE_FILES = [
|
||||
'test-notify.json',
|
||||
'test-send-notification-draft.json',
|
||||
'test-send-email-draft.json',
|
||||
'test-list-email-drafts.json',
|
||||
'test-delete-email-draft.json',
|
||||
'test-send-gmail-draft.json',
|
||||
'test-send-approved-email.json',
|
||||
'test-create-calendar-event.json',
|
||||
'test-fetch-and-normalize-url.json',
|
||||
'test-approval-queue-list.json',
|
||||
@@ -38,6 +42,10 @@ ROUTER_SNIPPETS = [
|
||||
'notify',
|
||||
'send_notification_draft',
|
||||
'send_email_draft',
|
||||
'list_email_drafts',
|
||||
'delete_email_draft',
|
||||
'send_gmail_draft',
|
||||
'send_approved_email',
|
||||
'create_calendar_event',
|
||||
'approval_queue_add',
|
||||
'approval_queue_list',
|
||||
@@ -50,6 +58,10 @@ ROUTER_SNIPPETS = [
|
||||
'$getWorkflowStaticData',
|
||||
'approvalQueue',
|
||||
'approvalHistory',
|
||||
'email_draft_send',
|
||||
'email_draft_delete',
|
||||
'email_list_drafts',
|
||||
'makeApprovalPolicy',
|
||||
'inboundEvents',
|
||||
'eventDedup',
|
||||
'notify_text',
|
||||
|
||||
Reference in New Issue
Block a user