Skill: kai-jar/gmail-oauth (version 1.0.0)

--- name: gmail-oauth description: Set up Gmail API access via gog CLI with manual OAuth flow. Use when setting up Gmail integration, renewing expired OAuth tokens, or troubleshooting Gmail authentication on headless servers. --- # Gmail OAuth Setup Headless-friendly OAuth flow for Gmail API access using `gog` CLI. ## Prerequisites - `gog` CLI installed (`brew install steipete/tap/gogcli`) - Google Cloud project with OAuth credentials (Desktop app type) - Gmail API enabled in the project ## Quick Setup ### 1. Create Google Cloud Project & Credentials 1. Go to https://console.cloud.google.com 2. Create a new project (or select existing) 3. **Enable Gmail API**: APIs & Services → Library → search "Gmail API" → Enable 4. **Configure OAuth consent screen**: APIs & Services → OAuth consent screen - Choose "External" user type - Fill in app name, user support email - Add scopes: `gmail.modify` (or others as needed) - **Important**: Click "PUBLISH APP" for permanent tokens (see Troubleshooting) 5. **Create credentials**: APIs & Services → Credentials → Create Credentials → OAuth client ID - Application type: **Desktop app** - Download the JSON file ### 2. Configure gog ```bash gog auth credentials /path/to/client_secret.json gog auth keyring file # Use file-based keyring for headless export GOG_KEYRING_PASSWORD="your-password" # Add to .bashrc ``` ### 3. Run Auth Flow Run `scripts/gmail-auth.sh` interactively, or: ```bash # Generate URL scripts/gmail-auth.sh --url # User opens URL, approves, copies code from localhost redirect # Exchange code (do this quickly - codes expire in minutes!) scripts/gmail-auth.sh --exchange CODE EMAIL ``` ### 4. Verify ```bash gog gmail search 'is:unread' --max 5 --account [REDACTED_SECRET] ``` ## Troubleshooting ### "Access blocked: [app] has not completed the Google verification process" **Cause**: App is in "Testing" mode and the Gmail account isn't a test user. **Solutions** (choose one): 1. **Publish the app** (recommended): - Google Cloud Console → APIs & Services → OAuth consent screen - Click **"PUBLISH APP"** → Confirm - No Google review needed for personal use - Tokens become permanent 2. **Add test user**: - OAuth consent screen → Test users → + ADD USERS - Add the Gmail address you're authorizing - Tokens still expire in 7 days ### "Google hasn't verified this app" warning screen **This is normal for personal apps.** Click: 1. **Advanced** (bottom left) 2. **Go to [app name] (unsafe)** Safe to proceed since you own the app. ### Token expires in 7 days **Cause**: App is in "Testing" mode. **Fix**: Publish the app (see above). Published apps get permanent refresh tokens. ### "invalid_request" or "invalid_grant" errors **Causes**: - Authorization code expired (they only last a few minutes) - Code was already used - Redirect URI mismatch **Fix**: Generate a fresh auth URL and complete the flow quickly. Paste the code immediately after getting it. ### "redirect_uri_mismatch" error **Cause**: The redirect URI in the token exchange doesn't match what was used in the auth URL. **Fix**: This script uses `http://localhost`. Make sure both the auth URL and exchange use the same redirect URI. ### Page hangs after approving permissions (mobile) **Cause**: Browser trying to connect to localhost which doesn't exist on phone. **Fix**: - Use a desktop browser instead - Or tap the address bar while it's "hanging" - the URL contains the code - The URL will look like: `http://localhost/?code=4/0ABC...` ### Multiple permission checkboxes causing hangs **Cause**: Too many OAuth scopes requested. **Fix**: Use minimal scopes. `gmail.modify` alone is usually sufficient and shows just one permission. ### Can't find project in Google Cloud Console **Cause**: Signed into wrong Google account. **Fix**: Check which account owns the project: - Click profile icon (top right) - Switch accounts - Check project dropdown for each account ### "invalid_request" with oob redirect (new projects) **Cause**: Google deprecated `urn:ietf:wg:oauth:2.0:oob` for OAuth clients created after 2022. **Fix**: Use `http://localhost` redirect instead (this script's default). After approval, browser redirects to localhost with code in URL. ## Scopes Reference | Scope | Access | |-------|--------| | `gmail.modify` | Read, send, delete, manage labels (recommended) | | `gmail.readonly` | Read only | | `gmail.send` | Send only | | `gmail.compose` | Create drafts, send | ## Files - `scripts/gmail-auth.sh` — Interactive auth helper ## Tips - **Publish your app** — Avoids test user limits and 7-day token expiry - **Exchange codes quickly** — They expire in minutes - **Use desktop browser** — Mobile browsers can be finicky with localhost redirects - **One scope is enough** — `gmail.modify` covers most use cases

path: scripts/gmail-auth.sh
content: #!/bin/bash
# Gmail OAuth helper for headless servers
# Usage:
#   ./gmail-auth.sh                    # Interactive setup
#   ./gmail-auth.sh --url              # Just print auth URL
#   ./gmail-auth.sh --exchange CODE    # Exchange code for tokens

set -e

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# Load credentials from gog config
GOG_CREDS="${HOME}/.config/gogcli/credentials.json"

if [[ ! -f "$GOG_CREDS" ]]; then
    echo -e "${RED}Error: No gog credentials found at $GOG_CREDS${NC}"
    echo "Run: gog auth credentials /path/to/client_secret.json"
    exit 1
fi

CLIENT_ID=$(python3 -c "import json; print(json.load(open('$GOG_CREDS'))['client_id'])")
CLIENT_SECRET=$(python3 -c "import json; print(json.load(open('$GOG_CREDS'))['client_secret'])")

# Default scope - gmail.modify covers most use cases
SCOPE="https://www.googleapis.com/auth/gmail.modify"
REDIRECT_URI="http://localhost"

generate_url() {
    local encoded_scope=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$SCOPE'))")
    echo "https://accounts.google.com/o/oauth2/auth?access_type=offline&client_id=${CLIENT_ID}&prompt=consent&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${encoded_scope}"
}

exchange_code() {
    local code="$1"
    local email="$2"
    
    echo -e "${YELLOW}Exchanging code for tokens...${NC}"
    
    response=$(curl -s -X POST https://oauth2.googleapis.com/token \
        -d "code=${code}" \
        -d "client_id=${CLIENT_ID}" \
        -d "client_secret=${CLIENT_SECRET}" \
        -d "redirect_uri=${REDIRECT_URI}" \
        -d "grant_type=authorization_code")
    
    # Check for error
    if echo "$response" | python3 -c "import json,sys; d=json.load(sys.stdin); sys.exit(0 if 'access_token' in d else 1)" 2>/dev/null; then
        refresh_token=$(echo "$response" | python3 -c "import json,sys; print(json.load(sys.stdin)['refresh_token'])")
        
        # Create token file for gog import
        token_file=$(mktemp)
        cat > "$token_file" << EOF
{
  "email": "${email}",
  "client": "default",
  "refresh_token": "${refresh_token}",
  "scopes": ["${SCOPE}"]
}
EOF
        
        echo -e "${GREEN}Token exchange successful!${NC}"
        echo ""
        echo "Importing to gog..."
        
        if [[ -z "$GOG_KEYRING_PASSWORD" ]]; then
            echo -e "${YELLOW}Note: Set GOG_KEYRING_PASSWORD environment variable for non-interactive import${NC}"
        fi
        
        gog auth tokens import "$token_file"
        rm "$token_file"
        
        echo ""
        echo -e "${GREEN}Done! Test with:${NC}"
        echo "  gog gmail search 'is:unread' --max 5 --account ${email}"
    else
        echo -e "${RED}Token exchange failed:${NC}"
        echo "$response" | python3 -m json.tool 2>/dev/null || echo "$response"
        exit 1
    fi
}

# Parse arguments
case "${1:-}" in
    --url)
        generate_url
        ;;
    --exchange)
        if [[ -z "${2:-}" ]]; then
            echo "Usage: $0 --exchange CODE [EMAIL]"
            exit 1
        fi
        email="${3:-}"
        if [[ -z "$email" ]]; then
            read -p "Gmail address to authorize: " email
        fi
        exchange_code "$2" "$email"
        ;;
    --help|-h)
        echo "Gmail OAuth Helper"
        echo ""
        echo "Usage:"
        echo "  $0              Interactive setup"
        echo "  $0 --url        Print auth URL only"
        echo "  $0 --exchange CODE [EMAIL]  Exchange code for tokens"
        echo ""
        echo "Environment:"
        echo "  GOG_KEYRING_PASSWORD  Required for non-interactive token import"
        ;;
    *)
        # Interactive mode
        echo -e "${GREEN}Gmail OAuth Setup${NC}"
        echo ""
        
        read -p "Gmail address to authorize: " email
        
        echo ""
        echo -e "${YELLOW}Step 1: Open this URL in a browser${NC}"
        echo ""
        generate_url
        echo ""
        echo -e "${YELLOW}Step 2: Sign in with ${email} and approve access${NC}"
        echo ""
        echo -e "${YELLOW}Step 3: Copy the authorization code shown on the page${NC}"
        echo ""
        read -p "Paste the code here: " code
        
        exchange_code "$code" "$email"
        ;;
esac


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