Skill: mintannn/invoice-collector (version 1.2.0)

--- name: invoice-collector version: 1.2.0 description: Collect invoices/receipts from Gmail and send a summary email with attachments. Automatically downloads PDF attachments or takes screenshots of emails without PDFs. Use when user wants to gather invoices, receipts, or billing emails and forward them to another address. Requires gogcli (gog) to be configured with Gmail access. metadata: openclaw: emoji: "🧾" requires: bins: ["gog", "node", "jq"] npm: ["puppeteer"] env: GOG_ACCOUNT: "Gmail account email (e.g. [REDACTED_SECRET])" GOG_KEYRING_PASSWORD: "Password for gogcli keyring storage" --- # Invoice Collector Collect invoices from Gmail and send them as a summary email with all attachments. ## Security Considerations ### Puppeteer `--no-sandbox` Flag This skill uses `--no-sandbox` when launching Puppeteer/Chromium. This is **required** in many environments: - **WSL (Windows Subsystem for Linux)**: Chrome sandbox requires kernel features not available in WSL1/WSL2 - **Docker containers**: Unless running with `--privileged` or specific seccomp profiles - **CI/CD environments**: Most runners don't support Chrome's sandbox **Risk**: Disabling the sandbox means if a malicious HTML email were rendered, it could potentially execute code outside the browser context. **Mitigation**: This skill only renders emails from your own Gmail inbox. The risk is limited to emails you've already received. If you're concerned, review emails before processing or run in an isolated environment. ### Installation via `curl | tar` The gogcli installation example uses `curl -sL ... | tar xz`, which is a common pattern but carries supply chain risks if the source were compromised. **Safer alternative** (verify checksum): ```bash # Download and verify curl -sLO https://github.com/steipete/gogcli/releases/latest/download/gogcli_linux_amd64.tar.gz curl -sLO https://github.com/steipete/gogcli/releases/latest/download/checksums.txt sha256sum -c checksums.txt --ignore-missing tar xzf gogcli_linux_amd64.tar.gz mv gog ~/.local/bin/ ``` **macOS users**: Use `brew install steipete/tap/gogcli` which handles verification automatically. --- ## Prerequisites & Setup ### 1. Install gogcli ```bash # Linux (download binary) curl -sL https://github.com/steipete/gogcli/releases/latest/download/gogcli_linux_amd64.tar.gz | tar xz mv gog ~/.local/bin/ # macOS brew install steipete/tap/gogcli ``` ### 2. Setup Google OAuth 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create project → Enable Gmail API 3. Create OAuth credentials (Desktop app) 4. Download JSON ```bash gog auth credentials ~/path/to/client_secret.json gog auth add [REDACTED_SECRET] ``` ### 3. Install Puppeteer (for email screenshots) ```bash cd /tmp && npm install puppeteer ``` ### 4. Install Japanese fonts (optional, for JP emails) ```bash sudo apt install fonts-noto-cjk ``` --- ## Usage ### Generic Invoice Search Search for any invoice/receipt without specifying specific senders: ```bash export GOG_ACCOUNT="[REDACTED_SECRET]" export GOG_KEYRING_PASSWORD="your-password" # Search all invoices in date range gog gmail search '(invoice OR receipt OR 請求書 OR 領収書 OR billing OR payment) after:2026/01/01 before:2026/02/01' # Search with specific criteria gog gmail search 'subject:(invoice OR receipt) has:attachment after:2026/01/01' ``` ### Workflow 1. **Search** - Find invoice emails 2. **Download** - Get PDFs or screenshot emails 3. **Summarize** - Create summary with amounts 4. **Send** - Email to destination with attachments ### Step 1: Search Invoices ```bash # All invoices from last month LAST_MONTH=$(date -d "1 month ago" +%Y/%m/01) THIS_MONTH=$(date +%Y/%m/01) gog gmail search "(invoice OR receipt OR 請求書 OR 領収書) after:$LAST_MONTH before:$THIS_MONTH" --json ``` ### Step 2: Process Each Email ```bash mkdir -p /tmp/invoices ``` **For emails WITH PDF attachments:** ```bash # Get message details MSG_ID="<message_id_here>" EMAIL_JSON=$(gog gmail read $MSG_ID --json) # Find PDF attachment ATTACH_INFO=$(echo "$EMAIL_JSON" | jq -r '.thread.messages[0].payload.parts[]? | select(.filename | test("\\.pdf$"; "i")) | "\(.body.attachmentId)|\(.filename)"' | head -1) ATTACH_ID=$(echo "$ATTACH_INFO" | cut -d'|' -f1) FILENAME=$(echo "$ATTACH_INFO" | cut -d'|' -f2) # Download gog gmail attachment $MSG_ID "$ATTACH_ID" --out "/tmp/invoices/$FILENAME" ``` **For emails WITHOUT PDF (take screenshot):** ```bash MSG_ID="<message_id_here>" # Extract HTML gog gmail read $MSG_ID --json | node -e " const fs = require('fs'); let data = ''; process.stdin.on('data', chunk => data += chunk); process.stdin.on('end', () => { const json = JSON.parse(data); const msg = json.thread.messages[0]; let html = ''; const findHtml = (p) => { if (p.mimeType === 'text/html' && p.body?.data) { html = Buffer.from(p.body.data, 'base64').toString('utf-8'); } if (p.parts) p.parts.forEach(findHtml); }; (msg.payload.parts || []).forEach(findHtml); if (!html && msg.payload.body?.data) { html = Buffer.from(msg.payload.body.data, 'base64').toString('utf-8'); } fs.writeFileSync('/tmp/invoices/email.html', html || '<html><body>No content</body></html>'); }); " # Screenshot node -e " const puppeteer = require('puppeteer'); const fs = require('fs'); (async () => { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] }); const page = await browser.newPage(); await page.setViewport({ width: 800, height: 1200 }); await page.setContent(fs.readFileSync('/tmp/invoices/email.html', 'utf-8'), { waitUntil: 'networkidle0' }); await page.screenshot({ path: '/tmp/invoices/receipt.png', fullPage: true }); await browser.close(); })(); " ``` ### Step 3: Extract Invoice Info Parse email for sender, date, amount: ```bash # Get basic info from email gog gmail read $MSG_ID --json | jq '{ from: .thread.messages[0].payload.headers[] | select(.name=="From") | .value, subject: .thread.messages[0].payload.headers[] | select(.name=="Subject") | .value, date: .thread.messages[0].payload.headers[] | select(.name=="Date") | .value }' ``` ### Step 4: Send Summary Email ```bash gog gmail send \ --to "[REDACTED_SECRET]" \ --subject "【$(date +%Y年%m月)】請求書まとめ" \ --body "請求書・領収書を添付します。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 📊 請求書まとめ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 【添付ファイル】 1. Invoice-001.pdf - Service A 2. Receipt.png - Service B (メールスクショ) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ このメールは自動生成されました。 " \ --attach /tmp/invoices/Invoice-001.pdf \ --attach /tmp/invoices/Receipt.png ``` --- ## Example Prompts **Generic:** - "先月の請求書を全部集めてまとめて送って" - "invoiceで検索して今月届いた請求書を[REDACTED_SECRET]に転送して" - "has:attachment receipt で検索して請求書集めて" **Specific:** - "AnthropicとVercelとAWSの請求書を集めて" - "from:stripe の請求書を過去3ヶ月分まとめて" --- ## Tips - **Date format**: `YYYY/MM/DD` for gog search - **PDF priority**: Always prefer PDF attachments over screenshots - **Japanese fonts**: Required for correct rendering of JP emails - **Cleanup**: `rm -rf /tmp/invoices` after sending - **Cron**: Set up monthly cron job for recurring collection

path: scripts/collect_invoices.sh
content: #!/bin/bash
# Invoice Collector Script
# Usage: ./collect_invoices.sh <config_file>
# Config file format (JSON):
# {
#   "account": "[REDACTED_SECRET]",
#   "sources": [
#     {"name": "Claude", "query": "from:anthropic (invoice OR receipt)"},
#     {"name": "Vercel", "query": "from:[REDACTED_SECRET]"}
#   ],
#   "date_range": {"after": "2026/01/01", "before": "2026/02/01"},
#   "destination": "[REDACTED_SECRET]",
#   "subject": "【2026年1月】請求書まとめ"
# }

set -e

CONFIG_FILE="${1:-config.json}"
if [ ! -f "$CONFIG_FILE" ]; then
  echo "Error: Config file not found: $CONFIG_FILE"
  exit 1
fi

# Parse config
ACCOUNT=$(jq -r '.account' "$CONFIG_FILE")
DESTINATION=$(jq -r '.destination' "$CONFIG_FILE")
SUBJECT=$(jq -r '.subject' "$CONFIG_FILE")
AFTER=$(jq -r '.date_range.after' "$CONFIG_FILE")
BEFORE=$(jq -r '.date_range.before' "$CONFIG_FILE")

export GOG_ACCOUNT="$ACCOUNT"
export PATH="$HOME/.local/bin:$PATH"

# Create output directory
INVOICE_DIR="/tmp/invoices_$(date +%Y%m%d%H%M%S)"
mkdir -p "$INVOICE_DIR"

ATTACHMENTS=""
SUMMARY=""

# Process each source
jq -c '.sources[]' "$CONFIG_FILE" | while read -r source; do
  NAME=$(echo "$source" | jq -r '.name')
  QUERY=$(echo "$source" | jq -r '.query')
  
  echo "Processing: $NAME"
  
  # Search for emails
  FULL_QUERY="$QUERY after:$AFTER before:$BEFORE"
  RESULT=$(gog gmail search "$FULL_QUERY" --json 2>/dev/null || echo '{"threads":[]}')
  
  MSG_ID=$(echo "$RESULT" | jq -r '.threads[0].id // empty')
  
  if [ -z "$MSG_ID" ]; then
    echo "  No emails found for $NAME"
    continue
  fi
  
  echo "  Found email: $MSG_ID"
  
  # Get email details
  EMAIL_JSON=$(gog gmail read "$MSG_ID" --json 2>/dev/null)
  
  # Check for PDF attachment
  ATTACH_ID=$(echo "$EMAIL_JSON" | jq -r '.thread.messages[0].payload.parts[]? | select(.filename | test("\\.pdf$"; "i")) | .body.attachmentId // empty' | head -1)
  
  if [ -n "$ATTACH_ID" ]; then
    # Download PDF
    FILENAME="${NAME}_$(date +%Y%m).pdf"
    echo "  Downloading PDF: $FILENAME"
    gog gmail attachment "$MSG_ID" "$ATTACH_ID" --out "$INVOICE_DIR/$FILENAME"
    echo "$INVOICE_DIR/$FILENAME" >> "$INVOICE_DIR/attachments.txt"
  else
    # Take screenshot
    FILENAME="${NAME}_$(date +%Y%m).png"
    echo "  Taking screenshot: $FILENAME"
    
    # Extract HTML
    echo "$EMAIL_JSON" | node -e "
      const fs = require('fs');
      let data = '';
      process.stdin.on('data', chunk => data += chunk);
      process.stdin.on('end', () => {
        const json = JSON.parse(data);
        const msg = json.thread.messages[0];
        const parts = msg.payload.parts || [];
        let html = '';
        const findHtml = (p) => {
          if (p.mimeType === 'text/html' && p.body && p.body.data) {
            html = Buffer.from(p.body.data, 'base64').toString('utf-8');
          }
          if (p.parts) p.parts.forEach(findHtml);
        };
        parts.forEach(findHtml);
        if (!html && msg.payload.body && msg.payload.body.data) {
          html = Buffer.from(msg.payload.body.data, 'base64').toString('utf-8');
        }
        fs.writeFileSync('$INVOICE_DIR/temp.html', html || '<html><body>No content</body></html>');
      });
    "
    
    # Screenshot with Puppeteer
    cd /tmp && node -e "
      const puppeteer = require('puppeteer');
      const fs = require('fs');
      (async () => {
        const browser = await puppeteer.launch({
          headless: 'new',
          args: ['--no-sandbox', '--disable-setuid-sandbox']
        });
        const page = await browser.newPage();
        await page.setViewport({ width: 800, height: 1200 });
        const html = fs.readFileSync('$INVOICE_DIR/temp.html', 'utf-8');
        await page.setContent(html, { waitUntil: 'networkidle0' });
        await page.screenshot({ path: '$INVOICE_DIR/$FILENAME', fullPage: true });
        await browser.close();
      })();
    "
    
    echo "$INVOICE_DIR/$FILENAME" >> "$INVOICE_DIR/attachments.txt"
  fi
done

# Build attachment flags
if [ -f "$INVOICE_DIR/attachments.txt" ]; then
  ATTACH_FLAGS=""
  while read -r file; do
    ATTACH_FLAGS="$ATTACH_FLAGS --attach $file"
  done < "$INVOICE_DIR/attachments.txt"
  
  # Send email
  echo "Sending summary email to $DESTINATION"
  gog gmail send \
    --to "$DESTINATION" \
    --subject "$SUBJECT" \
    --body "請求書を添付します。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 請求書まとめ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

添付ファイル一覧は本メールをご確認ください。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

このメールは自動生成されました。
" $ATTACH_FLAGS
  
  echo "Done! Sent to $DESTINATION"
else
  echo "No invoices found"
fi

# Cleanup
rm -rf "$INVOICE_DIR"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)