🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Email Resend

by @ivelin

Send and receive emails using the Resend API. Use for: (1) sending emails directly via Resend API, (2) receiving email notifications via cron, (3) drafting r...

Versionv1.0.14
Downloads2,031
TERMINAL
clawhub install email-resend

πŸ“– About This Skill


license: Apache-2.0 name: email-resend version: "1.0.14" metadata: clawdbot: requires: env: - RESEND_API_KEY bins: - python3 - openclaw python: - requests - pyyaml triggers: - "send email" - "check emails" - "email me" - "email notification" - "download attachment" description: > Send and receive emails using the Resend API. Use for: (1) sending emails directly via Resend API, (2) receiving email notifications via cron, (3) drafting replies with proper threading, (4) downloading attachments from inbound emails.

Required env vars: RESEND_API_KEY (API key). Optional env vars: DEFAULT_FROM_EMAIL, DEFAULT_FROM_NAME (fall back to preferences file if not set). License: Apache-2.0 β€” See LICENSE file for details.


License

Apache License 2.0 β€” See LICENSE file for full text.

Email via Resend

Send and receive emails using the Resend API.

Configuration

No config file needed. The skill auto-discovers settings from:

1. Environment variables β€” RESEND_API_KEY (required), DEFAULT_FROM_EMAIL/NAME (optional) 2. Preferences file β€” memory/email-preferences.md (from_email, from_name, telegram target) 3. OpenClaw context β€” channel, chat_id, thread_id (for cron delivery)

Required Environment Variables

export RESEND_API_KEY="re_123456789"        # Resend API key (required)

DEFAULT_FROM_EMAIL and DEFAULT_FROM_NAME are optional - loaded from preferences file if not set

Preferences File

The skill reads sender info from memory/email-preferences.md:

---
from_email: you@company.com
from_name: Your Name
telegram:
  target: "CHAT_ID"
  threadId: "THREAD_ID"

Scripts check env vars first, then fall back to preferences file.

First-Time Setup

When the skill is first invoked, the sub-agent should:

1. Check context β€” OpenClaw context already has: - context.user.email (from USER.md) - context.channel (from current session) - context.chat_id - context.thread_id (for topics)

2. Check memory β€” Use memory_get tool: - Try: memory_get path="memory/email-preferences.md" - If not found, ask user to create memory/email-preferences.md (NO fallback scanning)

3. If missing, ask user β€” Via chat message (IMPORTANT for cron jobs): - "Which email should I send from?" (from_email) - "What's your display name for sent emails?" (from_name) - "Which channel/topic should I notify you on?" (telegram target + threadId) Then create memory/email-preferences.md with their answers using the format above.

4. Commit to memory β€” Write preferences to persist across sessions:

   write path="memory/email-preferences.md" content="---
   from_email: $EMAIL
   from_name: $NAME
   telegram:
     target: \"$CHAT_ID\"
     threadId: \"$THREAD_ID\"
   ---

# Email Notification Preferences Saved auto-configured "

This ensures memory_get finds it in future sessions. Use MD format with YAML frontmatter.

Format (MD with YAML frontmatter):

---
from_email: you@company.com
from_name: Your Name
telegram:
  target: \"123456789\"
  threadId: \"334\"

Email Notification Preferences

  • Updated: 2026-01-01
  • Purpose: Default notification channel for email alerts
  • Important: Store in memory/email-preferences.md (NOT MEMORY.md) - isolated cron jobs can read this file via memory_get but NOT MEMORY.md.

    Context Fields (Available in Sub-Agent)

    | Field | Source | Example | |-------|--------|---------| | user.email | USER.md | you@company.com | | user.name | USER.md | Your Name | | channel | OpenClaw | from context | | chat_id | OpenClaw | 123456789 | | thread_id | OpenClaw | 334 |

    The skill uses these directly from OpenClaw context β€” no parsing needed.

    Usage

    Inbound (Receive)

    Cron Setup

    There are two ways to configure the cron:

    #### Option 1: Static (Hardcoded Target)

    Use this if you always want the same delivery target:

    openclaw cron add \
      --name "email-resend-inbound" \
      --cron "*/15 * * * *" \
      --message "Follow instructions in skills/email-resend/cron-prompts/email-inbound.md exactly. If new emails found, include them in your reply." \
      --session isolated \
      --announce \
      --channel telegram \
      --to "-1003748898773:topic:334"
    

    #### Option 2: Dynamic (From Preferences) β€” Recommended

    This reads your notification preferences from memory/email-preferences.md and configures the cron automatically.

    Run:

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/configure-cron.py
    

    What it does: 1. Reads memory/email-preferences.md for your telegram target/threadId 2. Deletes any existing email-resend-inbound cron 3. Creates a new cron with your preferred delivery target

    First-time setup: If preferences don't exist, it will tell you what to configure.

    Parameters:

  • --schedule "cron */15 * * * *" β€” Run every 15 minutes
  • --session isolated β€” Required for agentTurn payloads
  • --announce β€” Enable delivery of results to chat
  • --channel telegram β€” Delivery channel
  • --to β€” Telegram target (format: chat_id:topic:thread_id)
  • Note: The cron prompt reads notification preferences from memory/email-preferences.md. On first run, if preferences are missing, it will ask you for:

  • Which channel for notifications (telegram, discord, etc.)
  • Chat ID and Thread ID (for topics)
  • Manual Check

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/inbound.py
    

    Notification Format

    Each new email triggers a notification with:

  • From, Subject, Date
  • Body preview (~2000 chars)
  • Attachment list (if any)
  • Importance: πŸ”₯ HIGH / πŸ“… MEETING / πŸ“¬ NORMAL
  • Acknowledge Flow (CRITICAL)

    NEVER auto-acknowledge emails. Only the user can acknowledge by:

  • Replying to the notification message, OR
  • Typing: done / ack
  • Emails must remain in pending state until user explicitly acknowledges.

    Use draft-reply.py to compose replies with proper quoting.

    Important: Always use inline replies ([[reply_to_current]]) to keep messages linked in the thread. This enables:

  • Proper custody chain tracking
  • Reply-to-email tracing
  • Better conversation flow
  • CRITICAL: When responding via OpenClaw message tool, use replyTo parameter (not [[reply_to_current]] tag):

    message(action="send", channel="", replyTo="", ...)
    

    Scripts

    | Script | Purpose | |--------|---------| | inbound.py | Check emails, send notifications | | draft-reply.py | Draft reply workflow with quoting & threading | | outbound.py | Send emails directly | | download_attachment.py | Download attachments from inbound emails |

    Downloading Attachments

    To download attachments from an inbound email:

    # List attachments (shows IDs)
    python3 scripts/download_attachment.py  --list

    Download all to directory

    python3 scripts/download_attachment.py --output-dir ./attachments

    Download specific attachment

    python3 scripts/download_attachment.py --attachment-id

    Note: The API path is /emails/receiving/{email_id}/attachments (not the standard /emails/ path).

    State Files

  • memory/email-resend-inbound-notified.json β€” pending/acknowledged emails
  • memory/email-message-map.json β€” notification message_id β†’ email_id (legacy)
  • memory/email-custody-chain.json β€” Full DAG of email β†’ notification β†’ actions
  • memory/email-msg-to-chain.json β€” notification message_id β†’ chain lookup
  • memory/email-draft-state.json β€” Active draft state (email_id, status, reply_content)
  • See docs/custody-chain.md for DAG design.

    Outbound (Send)

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/outbound.py \
      --to "recipient@example.com" \
      --subject "Hello" \
      --body "Message text"

    With attachments

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/outbound.py \ --to "recipient@example.com" \ --subject "Here's the file" \ --body "See attachment" \ --attachment ./file.pdf \ --attachment ./image.png

    ⚠️ CRITICAL: Email Threading Rule (2026-02-22)

    MANDATORY: Always use draft-reply.py for replying to emails.

    This is non-negotiable. Failure to follow this rule will result in broken Gmail threading.

    Why This Matters

  • Gmail threads emails based on In-Reply-To AND References headers
  • Using wrong headers = reply appears as NEW thread = context lost
  • There's no way to fix this after sending
  • βœ… Correct Workflow (ALWAYS USE THIS)

    # Step 1: Start draft (fetches Message-ID automatically)
    python3 ~/.openclaw/workspace/skills/email-resend/scripts/draft-reply.py start 

    Step 2: Set reply content

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/draft-reply.py content "Your reply"

    Step 3: Send

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/draft-reply.py send

    ⚠️ CRITICAL: Approval Execution Rule (2026-02-22)

    When user approves a draft, you MUST execute the send command immediately.

    The mistake to avoid:

  • ❌ Show draft for approval β†’ User says "send" β†’ Only acknowledge, don't execute
  • βœ… Show draft for approval β†’ User says "send" β†’ RUN draft-reply.py send β†’ Then confirm
  • Correct workflow:

    1. Show draft for approval
    2. User replies "approve", "send", "yes", or "ok"
    3. IMMEDIATELY run: draft-reply.py send
    4. Only THEN confirm to user
    

    Never:

  • Only acknowledge the approval without executing
  • Ask for confirmation after user already approved
  • Wait to send - do it immediately
  • ❌ NEVER Do These Things

    NEVER use outbound.py for replies:

    # WRONG - will break threading
    python3 ~/.openclaw/workspace/skills/email-resend/scripts/outbound.py \
      --to "x@y.com" --subject "Re: Original" --body "Reply"
    

    NEVER manually construct --reply-to flags:

    # WRONG - guessing Message-ID format never works
    python3 ~/.openclaw/workspace/skills/email-resend/scripts/outbound.py \
      --to "x@y.com" --subject "Re: Original" --body "Reply" \
      --reply-to "@resend"
    

    NEVER skip the workflow when subject starts with "Re:":

    # WRONG - replying without threading headers breaks thread
    python3 ~/.openclaw/workspace/skills/email-resend/scripts/outbound.py \
      --to "x@y.com" --subject "Re: Previous Thread" --body "Quick reply"
    

    outbound.py Only For New Emails

    outbound.py is for new emails only (not replies):

  • First contact
  • Announcements
  • Emails where you intentionally want a NEW thread
  • For anything that could be a reply, use draft-reply.py.

    Requirements

  • RESEND_API_KEY environment variable set
  • Python requests library
  • Draft Reply Best Practices

    When composing a reply via draft-reply.py:

    1. Always quote the original β€” Include the original message with > prefix so recipient knows what you're responding to

    2. Use proper threading β€” Set In-Reply-To and References headers using the original email's Message-ID

    3. Keep subject line β€” Start with Re: prefix to maintain thread (but avoid "Re: Re:")

    4. Structure:

       Your reply here

    ---

    On [date] [original sender] wrote: > quoted original message > continues here

    5. Multiple replies supported β€” After sending, draft is marked as "sent" so you can reply again to the same thread. Use resume command to continue.

    6. No double Re: β€” If original subject already starts with "Re:", don't add another

    7. Custody Chain β€” Track full lineage: - Email β†’ notification β†’ All replies/actions - DAG structure with parent links - Any message traces back to original email

    Draft Reply Commands

    | Command | Purpose | |---------|---------| | start | Start a draft reply to an email | | resume | Continue a sent thread to reply again | | content "text" | Set reply content | | send | Send the reply | | cancel | Cancel the draft | | status | Show current draft status |

    After sending, use resume to reply again to the same thread β€” threading headers are preserved.

    Run tests:

    python3 skills/email-resend/tests/test_inbound.py
    

    Expected: 43+ tests total (test_inbound.py: 37, test_threading.py: 6, test_attachments.py: varies).

    If tests fail: 1. Check which test failed and why 2. Fix the feature/code to match expected behavior 3. Or update tests if feature intentionally changed


    ⚠️ Privacy & Security Considerations

    Required Credentials

  • RESEND_API_KEY β€” Required. Get from https://resend.com API settings. Create with minimal permissions.
  • DEFAULT_FROM_EMAIL / DEFAULT_FROM_NAME β€” Optional. If not set, loaded from memory/email-preferences.md.
  • Memory File Access

    The skill reads ONLY from explicit preferences file:
  • memory/email-preferences.md β€” Required for telegram target/threadId
  • No fallback scanning of MEMORY.md, USER.md, TOOLS.md, or memory/*.md
  • This restricted approach prevents information leakage from sensitive files.

    Cron Job

    The configure-cron.py script will create/delete a cron job named email-resend-inbound via OpenClaw CLI.

    Recommendations

  • Run tests with a dummy RESEND_API_KEY before enabling in production
  • If you only need outbound email, don't enable inbound/cron
  • Audit memory/email-preferences.md to ensure it contains only necessary fields
  • Keep preferences file minimal - only include required fields (target, threadId)
  • πŸ’‘ Examples

    Inbound (Receive)

    Cron Setup

    There are two ways to configure the cron:

    #### Option 1: Static (Hardcoded Target)

    Use this if you always want the same delivery target:

    openclaw cron add \
      --name "email-resend-inbound" \
      --cron "*/15 * * * *" \
      --message "Follow instructions in skills/email-resend/cron-prompts/email-inbound.md exactly. If new emails found, include them in your reply." \
      --session isolated \
      --announce \
      --channel telegram \
      --to "-1003748898773:topic:334"
    

    #### Option 2: Dynamic (From Preferences) β€” Recommended

    This reads your notification preferences from memory/email-preferences.md and configures the cron automatically.

    Run:

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/configure-cron.py
    

    What it does: 1. Reads memory/email-preferences.md for your telegram target/threadId 2. Deletes any existing email-resend-inbound cron 3. Creates a new cron with your preferred delivery target

    First-time setup: If preferences don't exist, it will tell you what to configure.

    Parameters:

  • --schedule "cron */15 * * * *" β€” Run every 15 minutes
  • --session isolated β€” Required for agentTurn payloads
  • --announce β€” Enable delivery of results to chat
  • --channel telegram β€” Delivery channel
  • --to β€” Telegram target (format: chat_id:topic:thread_id)
  • Note: The cron prompt reads notification preferences from memory/email-preferences.md. On first run, if preferences are missing, it will ask you for:

  • Which channel for notifications (telegram, discord, etc.)
  • Chat ID and Thread ID (for topics)
  • Manual Check

    python3 ~/.openclaw/workspace/skills/email-resend/scripts/inbound.py
    

    Notification Format

    Each new email triggers a notification with:

  • From, Subject, Date
  • Body preview (~2000 chars)
  • Attachment list (if any)
  • Importance: πŸ”₯ HIGH / πŸ“… MEETING / πŸ“¬ NORMAL
  • Acknowledge Flow (CRITICAL)

    NEVER auto-acknowledge emails. Only the user can acknowledge by:

  • Replying to the notification message, OR
  • Typing: done / ack
  • Emails must remain in pending state until user explicitly acknowledges.

    Use draft-reply.py to compose replies with proper quoting.

    Important: Always use inline replies ([[reply_to_current]]) to keep messages linked in the thread. This enables:

  • Proper custody chain tracking
  • Reply-to-email tracing
  • Better conversation flow
  • CRITICAL: When responding via OpenClaw message tool, use replyTo parameter (not [[reply_to_current]] tag):

    message(action="send", channel="", replyTo="", ...)
    

    βš™οΈ Configuration

    No config file needed. The skill auto-discovers settings from:

    1. Environment variables β€” RESEND_API_KEY (required), DEFAULT_FROM_EMAIL/NAME (optional) 2. Preferences file β€” memory/email-preferences.md (from_email, from_name, telegram target) 3. OpenClaw context β€” channel, chat_id, thread_id (for cron delivery)

    Required Environment Variables

    export RESEND_API_KEY="re_123456789"        # Resend API key (required)
    

    DEFAULT_FROM_EMAIL and DEFAULT_FROM_NAME are optional - loaded from preferences file if not set

    Preferences File

    The skill reads sender info from memory/email-preferences.md:

    ---
    from_email: you@company.com
    from_name: Your Name
    telegram:
      target: "CHAT_ID"
      threadId: "THREAD_ID"
    

    Scripts check env vars first, then fall back to preferences file.

    First-Time Setup

    When the skill is first invoked, the sub-agent should:

    1. Check context β€” OpenClaw context already has: - context.user.email (from USER.md) - context.channel (from current session) - context.chat_id - context.thread_id (for topics)

    2. Check memory β€” Use memory_get tool: - Try: memory_get path="memory/email-preferences.md" - If not found, ask user to create memory/email-preferences.md (NO fallback scanning)

    3. If missing, ask user β€” Via chat message (IMPORTANT for cron jobs): - "Which email should I send from?" (from_email) - "What's your display name for sent emails?" (from_name) - "Which channel/topic should I notify you on?" (telegram target + threadId) Then create memory/email-preferences.md with their answers using the format above.

    4. Commit to memory β€” Write preferences to persist across sessions:

       write path="memory/email-preferences.md" content="---
       from_email: $EMAIL
       from_name: $NAME
       telegram:
         target: \"$CHAT_ID\"
         threadId: \"$THREAD_ID\"
       ---

    # Email Notification Preferences Saved auto-configured "

    This ensures memory_get finds it in future sessions. Use MD format with YAML frontmatter.

    Format (MD with YAML frontmatter):

    ---
    from_email: you@company.com
    from_name: Your Name
    telegram:
      target: \"123456789\"
      threadId: \"334\"
    

    Email Notification Preferences

  • Updated: 2026-01-01
  • Purpose: Default notification channel for email alerts
  • Important: Store in memory/email-preferences.md (NOT MEMORY.md) - isolated cron jobs can read this file via memory_get but NOT MEMORY.md.

    Context Fields (Available in Sub-Agent)

    | Field | Source | Example | |-------|--------|---------| | user.email | USER.md | you@company.com | | user.name | USER.md | Your Name | | channel | OpenClaw | from context | | chat_id | OpenClaw | 123456789 | | thread_id | OpenClaw | 334 |

    The skill uses these directly from OpenClaw context β€” no parsing needed.