commit 54471bd63ee6107ba3eb5051c6ca0a2d68d16d60 Author: zlei9 Date: Sun Mar 29 10:19:32 2026 +0800 Initial commit with translated description diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..a14772d --- /dev/null +++ b/SKILL.md @@ -0,0 +1,237 @@ +--- +name: outlook +description: "通过Microsoft Graph API阅读、搜索和管理Outlook邮件和日历。" +version: 1.3.0 +author: jotamed +--- + +# Outlook Skill + +Access Outlook/Hotmail email and calendar via Microsoft Graph API using OAuth2. + +## Quick Setup (Automated) + +```bash +# Requires: Azure CLI, jq +./scripts/outlook-setup.sh +``` + +The setup script will: +1. Log you into Azure (device code flow) +2. Create an App Registration automatically +3. Configure API permissions (Mail.ReadWrite, Mail.Send, Calendars.ReadWrite) +4. Guide you through authorization +5. Save credentials to `~/.outlook-mcp/` + +## Manual Setup + +See `references/setup.md` for step-by-step manual configuration via Azure Portal. + +## Usage + +### Token Management +```bash +./scripts/outlook-token.sh refresh # Refresh expired token +./scripts/outlook-token.sh test # Test connection +./scripts/outlook-token.sh get # Print access token +``` + +### Reading Emails +```bash +./scripts/outlook-mail.sh inbox [count] # List latest emails (default: 10) +./scripts/outlook-mail.sh unread [count] # List unread emails +./scripts/outlook-mail.sh search "query" [count] # Search emails +./scripts/outlook-mail.sh from [count] # List emails from sender +./scripts/outlook-mail.sh read # Read email content +./scripts/outlook-mail.sh attachments # List email attachments +``` + +### Managing Emails +```bash +./scripts/outlook-mail.sh mark-read # Mark as read +./scripts/outlook-mail.sh mark-unread # Mark as unread +./scripts/outlook-mail.sh flag # Flag as important +./scripts/outlook-mail.sh unflag # Remove flag +./scripts/outlook-mail.sh delete # Move to trash +./scripts/outlook-mail.sh archive # Move to archive +./scripts/outlook-mail.sh move # Move to folder +``` + +### Sending Emails +```bash +./scripts/outlook-mail.sh send # Send new email +./scripts/outlook-mail.sh reply "body" # Reply to email +``` + +### Folders & Stats +```bash +./scripts/outlook-mail.sh folders # List mail folders +./scripts/outlook-mail.sh stats # Inbox statistics +``` + +## Calendar + +### Viewing Events +```bash +./scripts/outlook-calendar.sh events [count] # List upcoming events +./scripts/outlook-calendar.sh today # Today's events +./scripts/outlook-calendar.sh week # This week's events +./scripts/outlook-calendar.sh read # Event details +./scripts/outlook-calendar.sh calendars # List all calendars +./scripts/outlook-calendar.sh free # Check availability +``` + +### Creating Events +```bash +./scripts/outlook-calendar.sh create [location] # Create event +./scripts/outlook-calendar.sh quick [time] # Quick 1-hour event +``` + +### Managing Events +```bash +./scripts/outlook-calendar.sh update # Update (subject/location/start/end) +./scripts/outlook-calendar.sh delete # Delete event +``` + +Date format: `YYYY-MM-DDTHH:MM` (e.g., `2026-01-26T10:00`) + +### Example Output + +```bash +$ ./scripts/outlook-mail.sh inbox 3 + +{ + "n": 1, + "subject": "Your weekly digest", + "from": "digest@example.com", + "date": "2026-01-25T15:44", + "read": false, + "id": "icYY6QAIUE26PgAAAA==" +} +{ + "n": 2, + "subject": "Meeting reminder", + "from": "calendar@outlook.com", + "date": "2026-01-25T14:06", + "read": true, + "id": "icYY6QAIUE26PQAAAA==" +} + +$ ./scripts/outlook-mail.sh read "icYY6QAIUE26PgAAAA==" + +{ + "subject": "Your weekly digest", + "from": { "name": "Digest", "address": "digest@example.com" }, + "to": ["you@hotmail.com"], + "date": "2026-01-25T15:44:00Z", + "body": "Here's what happened this week..." +} + +$ ./scripts/outlook-mail.sh stats + +{ + "folder": "Inbox", + "total": 14098, + "unread": 2955 +} + +$ ./scripts/outlook-calendar.sh today + +{ + "n": 1, + "subject": "Team standup", + "start": "2026-01-25T10:00", + "end": "2026-01-25T10:30", + "location": "Teams", + "id": "AAMkAGQ5NzE4YjQ3..." +} + +$ ./scripts/outlook-calendar.sh create "Lunch with client" "2026-01-26T13:00" "2026-01-26T14:00" "Restaurant" + +{ + "status": "event created", + "subject": "Lunch with client", + "start": "2026-01-26T13:00", + "end": "2026-01-26T14:00", + "id": "AAMkAGQ5NzE4YjQ3..." +} +``` + +## Token Refresh + +Access tokens expire after ~1 hour. Refresh with: + +```bash +./scripts/outlook-token.sh refresh +``` + +## Files + +- `~/.outlook-mcp/config.json` - Client ID and secret +- `~/.outlook-mcp/credentials.json` - OAuth tokens (access + refresh) + +## Permissions + +- `Mail.ReadWrite` - Read and modify emails +- `Mail.Send` - Send emails +- `Calendars.ReadWrite` - Read and modify calendar events +- `offline_access` - Refresh tokens (stay logged in) +- `User.Read` - Basic profile info + +## Notes + +- **Email IDs**: The `id` field shows the last 20 characters of the full message ID. Use this ID with commands like `read`, `mark-read`, `delete`, etc. +- **Numbered results**: Emails are numbered (n: 1, 2, 3...) for easy reference in conversation. +- **Text extraction**: HTML email bodies are automatically converted to plain text. +- **Token expiry**: Access tokens expire after ~1 hour. Run `outlook-token.sh refresh` when you see auth errors. +- **Recent emails**: Commands like `read`, `mark-read`, etc. search the 100 most recent emails for the ID. + +## Troubleshooting + +**"Token expired"** → Run `outlook-token.sh refresh` + +**"Invalid grant"** → Token invalid, re-run setup: `outlook-setup.sh` + +**"Insufficient privileges"** → Check app permissions in Azure Portal → API Permissions + +**"Message not found"** → The email may be older than 100 messages. Use search to find it first. + +**"Folder not found"** → Use exact folder name. Run `folders` to see available folders. + +## Supported Accounts + +- Personal Microsoft accounts (outlook.com, hotmail.com, live.com) +- Work/School accounts (Microsoft 365) - may require admin consent + +## Changelog + +### v1.3.0 +- Added: **Calendar support** (`outlook-calendar.sh`) + - View events (today, week, upcoming) + - Create/quick-create events + - Update event details (subject, location, time) + - Delete events + - Check availability (free/busy) + - List calendars +- Added: `Calendars.ReadWrite` permission + +### v1.2.0 +- Added: `mark-unread` - Mark emails as unread +- Added: `flag/unflag` - Flag/unflag emails as important +- Added: `delete` - Move emails to trash +- Added: `archive` - Archive emails +- Added: `move` - Move emails to any folder +- Added: `from` - Filter emails by sender +- Added: `attachments` - List email attachments +- Added: `reply` - Reply to emails +- Improved: `send` - Better error handling and status output +- Improved: `move` - Case-insensitive folder names, shows available folders on error + +### v1.1.0 +- Fixed: Email IDs now use unique suffixes (last 20 chars) +- Added: Numbered results (n: 1, 2, 3...) +- Improved: HTML bodies converted to plain text +- Added: `to` field in read output + +### v1.0.0 +- Initial release diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..f30d54c --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7d9cz2hq6kr5vs1gxqghbne17zxc5q", + "slug": "outlook", + "version": "1.3.0", + "publishedAt": 1769367682148 +} \ No newline at end of file diff --git a/references/setup.md b/references/setup.md new file mode 100644 index 0000000..dbd1887 --- /dev/null +++ b/references/setup.md @@ -0,0 +1,127 @@ +# Outlook Manual Setup Guide + +Use this guide if you prefer manual setup via Azure Portal, or if the automated setup fails. + +## Prerequisites + +- Microsoft account (Outlook.com, Hotmail, Live, or Microsoft 365) +- Access to [Azure Portal](https://portal.azure.com) +- `jq` installed (`sudo apt install jq`) + +## Step 1: Create Azure App Registration + +1. Go to https://portal.azure.com +2. Search for **"App registrations"** → Click it +3. Click **"+ New registration"** +4. Configure: + - **Name:** `Clawdbot-Outlook` (or any name) + - **Supported account types:** "Accounts in any organizational directory and personal Microsoft accounts" + - **Redirect URI:** Platform = Web, URI = `http://localhost` +5. Click **Register** + +## Step 2: Get Client Credentials + +After registration: +1. On the app overview page, copy the **Application (client) ID** → This is your `CLIENT_ID` +2. Go to **Certificates & secrets** in the left menu +3. Click **+ New client secret** +4. Add a description (e.g., "clawdbot") and choose expiration +5. Click **Add** +6. **Immediately copy the Value** (not the ID) → This is your `CLIENT_SECRET` + - ⚠️ You can only see this once! + +## Step 3: Configure API Permissions + +1. Go to **API permissions** in the left menu +2. Click **+ Add a permission** +3. Select **Microsoft Graph** → **Delegated permissions** +4. Add these permissions: + - `Mail.ReadWrite` - Read and write mail + - `Mail.Send` - Send mail + - `Calendars.ReadWrite` - Read and write calendar + - `User.Read` - Read user profile +5. Click **Add permissions** + +Note: `offline_access` is requested during auth, not configured here. + +## Step 4: Save Configuration + +Create the config directory and files: + +```bash +mkdir -p ~/.outlook-mcp +``` + +Create `~/.outlook-mcp/config.json`: +```json +{ + "client_id": "YOUR_CLIENT_ID", + "client_secret": "YOUR_CLIENT_SECRET" +} +``` + +Secure the file: +```bash +chmod 600 ~/.outlook-mcp/config.json +``` + +## Step 5: Authorize the App + +Build the authorization URL (replace YOUR_CLIENT_ID): + +``` +https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost&scope=https://graph.microsoft.com/Mail.ReadWrite%20https://graph.microsoft.com/Mail.Send%20https://graph.microsoft.com/Calendars.ReadWrite%20offline_access&response_mode=query +``` + +1. Open the URL in a browser +2. Sign in with your Microsoft account +3. Grant the requested permissions +4. You'll be redirected to `http://localhost?code=XXXXX...` +5. Copy the `code` value from the URL (everything after `code=` until `&` or end) + +## Step 6: Exchange Code for Tokens + +```bash +CLIENT_ID="your-client-id" +CLIENT_SECRET="your-client-secret" +AUTH_CODE="the-code-from-step-5" + +curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&code=$AUTH_CODE&redirect_uri=http://localhost&grant_type=authorization_code&scope=https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/Calendars.ReadWrite offline_access" \ + > ~/.outlook-mcp/credentials.json + +chmod 600 ~/.outlook-mcp/credentials.json +``` + +## Step 7: Verify Setup + +```bash +ACCESS_TOKEN=$(jq -r '.access_token' ~/.outlook-mcp/credentials.json) + +curl -s "https://graph.microsoft.com/v1.0/me/mailFolders/inbox" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '{total: .totalItemCount, unread: .unreadItemCount}' +``` + +You should see your inbox statistics. + +## Troubleshooting + +### "AADSTS700016: Application not found" +- Double-check the client_id is correct +- Ensure you selected "Accounts in any organizational directory and personal Microsoft accounts" + +### "AADSTS7000218: Invalid client secret" +- Client secrets can only be viewed once - create a new one if lost + +### "AADSTS65001: User hasn't consented" +- Re-run the authorization step (Step 5) +- Make sure you click "Accept" on the consent screen + +### "Token expired" +- Access tokens last ~1 hour +- Run `./scripts/outlook-token.sh refresh` to get a new one + +### Work/School Account Issues +- Your organization may require admin consent +- Contact your IT admin or use a personal Microsoft account diff --git a/scripts/outlook-calendar.sh b/scripts/outlook-calendar.sh new file mode 100644 index 0000000..7271751 --- /dev/null +++ b/scripts/outlook-calendar.sh @@ -0,0 +1,238 @@ +#!/bin/bash +# Outlook Calendar Operations +# Usage: outlook-calendar.sh [args] + +CONFIG_DIR="$HOME/.outlook-mcp" +CREDS_FILE="$CONFIG_DIR/credentials.json" + +# Load token +ACCESS_TOKEN=$(jq -r '.access_token' "$CREDS_FILE" 2>/dev/null) + +if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + echo "Error: No access token. Run setup first." + exit 1 +fi + +API="https://graph.microsoft.com/v1.0/me" + +case "$1" in + events) + # List upcoming events + COUNT=${2:-10} + curl -s "$API/calendar/events?\$top=$COUNT&\$orderby=start/dateTime%20desc&\$select=id,subject,start,end,location,isAllDay" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Prefer: outlook.timezone=\"Europe/Madrid\"" | jq '.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, start: .value.start.dateTime[0:16], end: .value.end.dateTime[0:16], location: (.value.location.displayName // ""), id: .value.id[-20:]}' + ;; + + today) + # List today's events + TODAY_START=$(date -u +"%Y-%m-%dT00:00:00Z") + TODAY_END=$(date -u +"%Y-%m-%dT23:59:59Z") + curl -s "$API/calendarView?startDateTime=$TODAY_START&endDateTime=$TODAY_END&\$orderby=start/dateTime&\$select=id,subject,start,end,location" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Prefer: outlook.timezone=\"Europe/Madrid\"" | jq 'if .value then (.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, start: .value.start.dateTime[0:16], end: .value.end.dateTime[0:16], location: (.value.location.displayName // ""), id: .value.id[-20:]}) else {error: .error.message} end' + ;; + + week) + # List this week's events + WEEK_START=$(date -u +"%Y-%m-%dT00:00:00Z") + WEEK_END=$(date -u -d "+7 days" +"%Y-%m-%dT23:59:59Z" 2>/dev/null || date -u -v+7d +"%Y-%m-%dT23:59:59Z") + curl -s "$API/calendarView?startDateTime=$WEEK_START&endDateTime=$WEEK_END&\$orderby=start/dateTime&\$select=id,subject,start,end,location,isAllDay" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Prefer: outlook.timezone=\"Europe/Madrid\"" | jq 'if .value then (.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, start: .value.start.dateTime[0:16], end: .value.end.dateTime[0:16], location: (.value.location.displayName // ""), id: .value.id[-20:]}) else {error: .error.message} end' + ;; + + read) + # Read event details + EVENT_ID="$2" + FULL_ID=$(curl -s "$API/calendar/events?\$top=50&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$EVENT_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Event not found" + exit 1 + fi + + curl -s "$API/calendar/events/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Prefer: outlook.timezone=\"Europe/Madrid\"" | jq '{ + subject, + start: .start.dateTime, + end: .end.dateTime, + location: .location.displayName, + body: (if .body.contentType == "html" then (.body.content | gsub("<[^>]*>"; "") | gsub("\\s+"; " ")[0:500]) else .body.content[0:500] end), + attendees: [.attendees[]?.emailAddress.address], + isOnline: .isOnlineMeeting, + link: .onlineMeeting.joinUrl + }' + ;; + + create) + # Create event: outlook-calendar.sh create "Subject" "2026-01-26T10:00" "2026-01-26T11:00" [location] + SUBJECT="$2" + START="$3" + END="$4" + LOCATION="${5:-}" + + if [ -z "$SUBJECT" ] || [ -z "$START" ] || [ -z "$END" ]; then + echo "Usage: outlook-calendar.sh create [location]" + echo "Date format: YYYY-MM-DDTHH:MM (e.g., 2026-01-26T10:00)" + exit 1 + fi + + LOCATION_JSON="" + if [ -n "$LOCATION" ]; then + LOCATION_JSON=",\"location\": {\"displayName\": \"$LOCATION\"}" + fi + + curl -s -X POST "$API/calendar/events" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"subject\": \"$SUBJECT\", + \"start\": {\"dateTime\": \"$START\", \"timeZone\": \"Europe/Madrid\"}, + \"end\": {\"dateTime\": \"$END\", \"timeZone\": \"Europe/Madrid\"} + $LOCATION_JSON + }" | jq '{status: "event created", subject: .subject, start: .start.dateTime[0:16], end: .end.dateTime[0:16], id: .id[-20:]}' + ;; + + quick) + # Quick event (1 hour from now or specified time) + SUBJECT="$2" + START_TIME="${3:-}" + + if [ -z "$SUBJECT" ]; then + echo "Usage: outlook-calendar.sh quick [start-time]" + echo "If no time given, creates 1-hour event starting now" + exit 1 + fi + + if [ -z "$START_TIME" ]; then + START=$(date +"%Y-%m-%dT%H:%M") + END=$(date -d "+1 hour" +"%Y-%m-%dT%H:%M" 2>/dev/null || date -v+1H +"%Y-%m-%dT%H:%M") + else + START="$START_TIME" + # Parse and add 1 hour + END=$(date -d "$START_TIME + 1 hour" +"%Y-%m-%dT%H:%M" 2>/dev/null || echo "$START_TIME") + fi + + curl -s -X POST "$API/calendar/events" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"subject\": \"$SUBJECT\", + \"start\": {\"dateTime\": \"$START\", \"timeZone\": \"Europe/Madrid\"}, + \"end\": {\"dateTime\": \"$END\", \"timeZone\": \"Europe/Madrid\"} + }" | jq '{status: "quick event created", subject: .subject, start: .start.dateTime[0:16], end: .end.dateTime[0:16], id: .id[-20:]}' + ;; + + delete) + # Delete event + EVENT_ID="$2" + FULL_ID=$(curl -s "$API/calendar/events?\$top=50&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$EVENT_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Event not found" + exit 1 + fi + + RESULT=$(curl -s -w "\n%{http_code}" -X DELETE "$API/calendar/events/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + + HTTP_CODE=$(echo "$RESULT" | tail -1) + if [ "$HTTP_CODE" = "204" ]; then + echo "{\"status\": \"event deleted\", \"id\": \"$EVENT_ID\"}" + else + echo "$RESULT" | head -n -1 | jq '.error // .' + fi + ;; + + update) + # Update event: outlook-calendar.sh update + EVENT_ID="$2" + FIELD="$3" + VALUE="$4" + + if [ -z "$FIELD" ] || [ -z "$VALUE" ]; then + echo "Usage: outlook-calendar.sh update " + echo "Fields: subject, location, start, end" + exit 1 + fi + + FULL_ID=$(curl -s "$API/calendar/events?\$top=50&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$EVENT_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Event not found" + exit 1 + fi + + case "$FIELD" in + subject) + BODY="{\"subject\": \"$VALUE\"}" + ;; + location) + BODY="{\"location\": {\"displayName\": \"$VALUE\"}}" + ;; + start) + BODY="{\"start\": {\"dateTime\": \"$VALUE\", \"timeZone\": \"Europe/Madrid\"}}" + ;; + end) + BODY="{\"end\": {\"dateTime\": \"$VALUE\", \"timeZone\": \"Europe/Madrid\"}}" + ;; + *) + echo "Unknown field: $FIELD" + exit 1 + ;; + esac + + curl -s -X PATCH "$API/calendar/events/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$BODY" | jq '{status: "event updated", subject: .subject, start: .start.dateTime[0:16], end: .end.dateTime[0:16], id: .id[-20:]}' + ;; + + calendars) + # List all calendars + curl -s "$API/calendars" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {name: .name, color: .color, canEdit: .canEdit, id: .id[-20:]}' + ;; + + free) + # Check free/busy for a time range + START="$2" + END="$3" + + if [ -z "$START" ] || [ -z "$END" ]; then + echo "Usage: outlook-calendar.sh free " + echo "Date format: YYYY-MM-DDTHH:MM" + exit 1 + fi + + curl -s "$API/calendarView?startDateTime=${START}:00Z&endDateTime=${END}:00Z&\$select=subject,start,end" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq 'if (.value | length) == 0 then {status: "free", start: "'"$START"'", end: "'"$END"'"} else {status: "busy", events: [.value[].subject]} end' + ;; + + *) + echo "Usage: outlook-calendar.sh [args]" + echo "" + echo "VIEW:" + echo " events [count] - List upcoming events" + echo " today - Today's events" + echo " week - This week's events" + echo " read - Event details" + echo " calendars - List all calendars" + echo " free - Check availability" + echo "" + echo "CREATE:" + echo " create [loc] - Create event" + echo " quick [time] - Quick 1-hour event" + echo "" + echo "MANAGE:" + echo " update - Update event" + echo " delete - Delete event" + echo "" + echo "Date format: YYYY-MM-DDTHH:MM (e.g., 2026-01-26T10:00)" + ;; +esac diff --git a/scripts/outlook-mail.sh b/scripts/outlook-mail.sh new file mode 100644 index 0000000..882bd3b --- /dev/null +++ b/scripts/outlook-mail.sh @@ -0,0 +1,707 @@ +#!/bin/bash +# Outlook Mail Operations +# Usage: outlook-mail.sh [args] + +CONFIG_DIR="$HOME/.outlook-mcp" +CREDS_FILE="$CONFIG_DIR/credentials.json" + +# Load token +ACCESS_TOKEN=$(jq -r '.access_token' "$CREDS_FILE" 2>/dev/null) + +if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then + echo "Error: No access token. Run setup first." + exit 1 +fi + +API="https://graph.microsoft.com/v1.0/me" + +case "$1" in + inbox) + # List inbox messages + COUNT=${2:-10} + curl -s "$API/messages?\$top=$COUNT&\$orderby=receivedDateTime%20desc&\$select=id,subject,from,receivedDateTime,isRead" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], read: .value.isRead, id: .value.id[-20:]}' + ;; + + unread) + # List unread messages + COUNT=${2:-20} + curl -s "$API/messages?\$filter=isRead%20eq%20false&\$top=$COUNT&\$orderby=receivedDateTime%20desc&\$select=id,subject,from,receivedDateTime" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], id: .value.id[-20:]}' + ;; + + search) + # Search emails + QUERY="$2" + COUNT=${3:-20} + curl -s "$API/messages?\$search=\"$QUERY\"&\$top=$COUNT&\$select=id,subject,from,receivedDateTime" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], id: .value.id[-20:]}' + ;; + + read) + # Read specific email by ID (partial ID match - uses last 20 chars) + MSG_ID="$2" + # First find full ID (search by suffix) + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found. Use the ID shown in inbox/unread/search results." + exit 1 + fi + + # Get message and extract text from HTML body + curl -s "$API/messages/$FULL_ID?\$select=subject,from,receivedDateTime,body,toRecipients" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '{ + subject, + from: .from.emailAddress, + to: [.toRecipients[].emailAddress.address], + date: .receivedDateTime, + body: (if .body.contentType == "html" then (.body.content | gsub("<[^>]*>"; "") | gsub("\\s+"; " ") | gsub(" "; " ") | .[0:2000]) else .body.content[0:2000] end) + }' + ;; + + mark-read) + # Mark message as read + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"isRead": true}' | jq '{status: "marked as read", subject: .subject, id: .id[-20:]}' + ;; + + folders) + # List mail folders + curl -s "$API/mailFolders" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {name: .displayName, total: .totalItemCount, unread: .unreadItemCount}' + ;; + + stats) + # Get inbox stats + curl -s "$API/mailFolders/inbox" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '{folder: .displayName, total: .totalItemCount, unread: .unreadItemCount}' + ;; + + send) + # Send email: outlook-mail.sh send "to@email.com" "Subject" "Body" + TO="$2" + SUBJECT="$3" + BODY="$4" + + if [ -z "$TO" ] || [ -z "$SUBJECT" ]; then + echo "Usage: outlook-mail.sh send " + exit 1 + fi + + RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/sendMail" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"message\": { + \"subject\": \"$SUBJECT\", + \"body\": {\"contentType\": \"Text\", \"content\": \"$BODY\"}, + \"toRecipients\": [{\"emailAddress\": {\"address\": \"$TO\"}}] + } + }") + + HTTP_CODE=$(echo "$RESULT" | tail -1) + if [ "$HTTP_CODE" = "202" ]; then + echo "{\"status\": \"sent\", \"to\": \"$TO\", \"subject\": \"$SUBJECT\"}" + else + echo "$RESULT" | head -n -1 | jq '.error // .' + fi + ;; + + mark-unread) + # Mark message as unread + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"isRead": false}' | jq '{status: "marked as unread", subject: .subject, id: .id[-20:]}' + ;; + + delete) + # Move message to trash + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X POST "$API/messages/$FULL_ID/move" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"destinationId": "deleteditems"}' | jq '{status: "moved to trash", subject: .subject, id: .id[-20:]}' + ;; + + archive) + # Move message to archive + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X POST "$API/messages/$FULL_ID/move" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"destinationId": "archive"}' | jq '{status: "archived", subject: .subject, id: .id[-20:]}' + ;; + + flag) + # Flag message as important + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"flag": {"flagStatus": "flagged"}}' | jq '{status: "flagged", subject: .subject, id: .id[-20:]}' + ;; + + unflag) + # Remove flag from message + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"flag": {"flagStatus": "notFlagged"}}' | jq '{status: "unflagged", subject: .subject, id: .id[-20:]}' + ;; + + from) + # List emails from specific sender (uses search - more reliable than filter) + SENDER="$2" + COUNT=${3:-20} + curl -s "$API/messages?\$search=\"from:$SENDER\"&\$top=$COUNT&\$select=id,subject,from,receivedDateTime,isRead" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq 'if .value then (.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], read: .value.isRead, id: .value.id[-20:]}) else {error: .error.message} end' + ;; + + attachments) + # List attachments for a message + MSG_ID="$2" + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s "$API/messages/$FULL_ID/attachments" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {name: .name, size: .size, contentType: .contentType, id: .id}' + ;; + + reply) + # Reply to a message: outlook-mail.sh reply "Reply body" + MSG_ID="$2" + BODY="$3" + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/messages/$FULL_ID/reply" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"comment\": \"$BODY\"}") + + HTTP_CODE=$(echo "$RESULT" | tail -1) + if [ "$HTTP_CODE" = "202" ]; then + echo "{\"status\": \"reply sent\", \"id\": \"$MSG_ID\"}" + else + echo "$RESULT" | head -n -1 | jq '.error // .' + fi + ;; + + move) + # Move message to folder: outlook-mail.sh move + MSG_ID="$2" + FOLDER="$3" + + if [ -z "$FOLDER" ]; then + echo "Usage: outlook-mail.sh move " + echo "Use 'folders' command to see available folders" + exit 1 + fi + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + # Get folder ID by name (case-insensitive) + FOLDER_LOWER=$(echo "$FOLDER" | tr '[:upper:]' '[:lower:]') + FOLDER_ID=$(curl -s "$API/mailFolders" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select((.displayName | ascii_downcase) == \"$FOLDER_LOWER\") | .id" | head -1) + + if [ -z "$FOLDER_ID" ]; then + echo "Folder not found: $FOLDER" + echo "Available folders:" + curl -s "$API/mailFolders" -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[].displayName' + exit 1 + fi + + curl -s -X POST "$API/messages/$FULL_ID/move" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"destinationId\": \"$FOLDER_ID\"}" | jq '{status: "moved", folder: "'"$FOLDER"'", subject: .subject, id: .id[-20:]}' + ;; + + draft) + # Create a draft email (not sent) + TO="$2" + SUBJECT="$3" + BODY="$4" + + if [ -z "$TO" ] || [ -z "$SUBJECT" ]; then + echo "Usage: outlook-mail.sh draft " + exit 1 + fi + + curl -s -X POST "$API/messages" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"subject\": \"$SUBJECT\", + \"body\": {\"contentType\": \"Text\", \"content\": \"$BODY\"}, + \"toRecipients\": [{\"emailAddress\": {\"address\": \"$TO\"}}] + }" | jq '{status: "draft created", subject: .subject, to: .toRecipients[0].emailAddress.address, id: .id[-20:]}' + ;; + + drafts) + # List draft emails + COUNT=${2:-10} + curl -s "$API/mailFolders/drafts/messages?\$top=$COUNT&\$select=id,subject,toRecipients,createdDateTime" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, to: (.value.toRecipients[0].emailAddress.address // "no recipient"), date: .value.createdDateTime[0:16], id: .value.id[-20:]}' + ;; + + send-draft) + # Send an existing draft + MSG_ID="$2" + + # Search in drafts folder + FULL_ID=$(curl -s "$API/mailFolders/drafts/messages?\$top=50&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Draft not found" + exit 1 + fi + + RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/messages/$FULL_ID/send" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Length: 0") + + HTTP_CODE=$(echo "$RESULT" | tail -1) + if [ "$HTTP_CODE" = "202" ]; then + echo "{\"status\": \"draft sent\", \"id\": \"$MSG_ID\"}" + else + echo "$RESULT" | head -n -1 | jq '.error // .' + fi + ;; + + forward) + # Forward an email: outlook-mail.sh forward [comment] + MSG_ID="$2" + TO="$3" + COMMENT="${4:-}" + + if [ -z "$TO" ]; then + echo "Usage: outlook-mail.sh forward [comment]" + exit 1 + fi + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + RESULT=$(curl -s -w "\n%{http_code}" -X POST "$API/messages/$FULL_ID/forward" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"comment\": \"$COMMENT\", + \"toRecipients\": [{\"emailAddress\": {\"address\": \"$TO\"}}] + }") + + HTTP_CODE=$(echo "$RESULT" | tail -1) + if [ "$HTTP_CODE" = "202" ]; then + echo "{\"status\": \"forwarded\", \"to\": \"$TO\", \"id\": \"$MSG_ID\"}" + else + echo "$RESULT" | head -n -1 | jq '.error // .' + fi + ;; + + download) + # Download an attachment: outlook-mail.sh download [output-path] + MSG_ID="$2" + ATT_NAME="$3" + OUTPUT="${4:-.}" + + if [ -z "$ATT_NAME" ]; then + echo "Usage: outlook-mail.sh download [output-path]" + echo "Use 'attachments ' to see available attachments" + exit 1 + fi + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + # Get attachment by name + ATT_DATA=$(curl -s "$API/messages/$FULL_ID/attachments" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.name == \"$ATT_NAME\")") + + if [ -z "$ATT_DATA" ]; then + echo "Attachment not found: $ATT_NAME" + echo "Available attachments:" + curl -s "$API/messages/$FULL_ID/attachments" -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.value[].name' + exit 1 + fi + + # Get content and decode + ATT_ID=$(echo "$ATT_DATA" | jq -r '.id') + CONTENT=$(curl -s "$API/messages/$FULL_ID/attachments/$ATT_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.contentBytes') + + OUTPUT_FILE="$OUTPUT/$ATT_NAME" + echo "$CONTENT" | base64 -d > "$OUTPUT_FILE" + + if [ -f "$OUTPUT_FILE" ]; then + SIZE=$(stat -c%s "$OUTPUT_FILE" 2>/dev/null || stat -f%z "$OUTPUT_FILE") + echo "{\"status\": \"downloaded\", \"file\": \"$OUTPUT_FILE\", \"size\": $SIZE}" + else + echo "{\"error\": \"Failed to save file\"}" + exit 1 + fi + ;; + + create-folder) + # Create a new mail folder + FOLDER_NAME="$2" + PARENT="${3:-}" + + if [ -z "$FOLDER_NAME" ]; then + echo "Usage: outlook-mail.sh create-folder [parent-folder]" + exit 1 + fi + + if [ -n "$PARENT" ]; then + # Get parent folder ID + PARENT_LOWER=$(echo "$PARENT" | tr '[:upper:]' '[:lower:]') + PARENT_ID=$(curl -s "$API/mailFolders" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select((.displayName | ascii_downcase) == \"$PARENT_LOWER\") | .id" | head -1) + + if [ -z "$PARENT_ID" ]; then + echo "Parent folder not found: $PARENT" + exit 1 + fi + + curl -s -X POST "$API/mailFolders/$PARENT_ID/childFolders" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"displayName\": \"$FOLDER_NAME\"}" | jq '{status: "folder created", name: .displayName, parent: "'"$PARENT"'", id: .id[-20:]}' + else + curl -s -X POST "$API/mailFolders" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"displayName\": \"$FOLDER_NAME\"}" | jq '{status: "folder created", name: .displayName, id: .id[-20:]}' + fi + ;; + + delete-folder) + # Delete a mail folder + FOLDER_NAME="$2" + + if [ -z "$FOLDER_NAME" ]; then + echo "Usage: outlook-mail.sh delete-folder " + exit 1 + fi + + FOLDER_LOWER=$(echo "$FOLDER_NAME" | tr '[:upper:]' '[:lower:]') + FOLDER_ID=$(curl -s "$API/mailFolders" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select((.displayName | ascii_downcase) == \"$FOLDER_LOWER\") | .id" | head -1) + + if [ -z "$FOLDER_ID" ]; then + echo "Folder not found: $FOLDER_NAME" + exit 1 + fi + + RESULT=$(curl -s -w "\n%{http_code}" -X DELETE "$API/mailFolders/$FOLDER_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + + HTTP_CODE=$(echo "$RESULT" | tail -1) + if [ "$HTTP_CODE" = "204" ]; then + echo "{\"status\": \"folder deleted\", \"name\": \"$FOLDER_NAME\"}" + else + echo "$RESULT" | head -n -1 | jq '.error // .' + fi + ;; + + bulk-read) + # Mark multiple messages as read: outlook-mail.sh bulk-read ... + shift + if [ $# -eq 0 ]; then + echo "Usage: outlook-mail.sh bulk-read ..." + exit 1 + fi + + SUCCESS=0 + FAILED=0 + + for MSG_ID in "$@"; do + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -n "$FULL_ID" ]; then + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"isRead": true}' > /dev/null + SUCCESS=$((SUCCESS + 1)) + else + FAILED=$((FAILED + 1)) + fi + done + + echo "{\"status\": \"bulk operation complete\", \"marked_read\": $SUCCESS, \"not_found\": $FAILED}" + ;; + + bulk-delete) + # Delete multiple messages: outlook-mail.sh bulk-delete ... + shift + if [ $# -eq 0 ]; then + echo "Usage: outlook-mail.sh bulk-delete ..." + exit 1 + fi + + SUCCESS=0 + FAILED=0 + + for MSG_ID in "$@"; do + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -n "$FULL_ID" ]; then + curl -s -X POST "$API/messages/$FULL_ID/move" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"destinationId": "deleteditems"}' > /dev/null + SUCCESS=$((SUCCESS + 1)) + else + FAILED=$((FAILED + 1)) + fi + done + + echo "{\"status\": \"bulk delete complete\", \"deleted\": $SUCCESS, \"not_found\": $FAILED}" + ;; + + categories) + # List available categories (like Gmail labels) + curl -s "https://graph.microsoft.com/v1.0/me/outlook/masterCategories" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value[] | {name: .displayName, color: .color, id: .id[0:8]}' + ;; + + categorize) + # Add category to message: outlook-mail.sh categorize + MSG_ID="$2" + CATEGORY="$3" + + if [ -z "$CATEGORY" ]; then + echo "Usage: outlook-mail.sh categorize " + echo "Use 'categories' to see available categories" + exit 1 + fi + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id,categories" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + # Get current categories and add new one + CURRENT=$(curl -s "$API/messages/$FULL_ID?\$select=categories" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.categories | join(",")') + + if [ -n "$CURRENT" ]; then + NEW_CATS="[\"$CURRENT\",\"$CATEGORY\"]" + else + NEW_CATS="[\"$CATEGORY\"]" + fi + + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"categories\": $NEW_CATS}" | jq '{status: "categorized", subject: .subject, categories: .categories, id: .id[-20:]}' + ;; + + uncategorize) + # Remove all categories from message + MSG_ID="$2" + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + curl -s -X PATCH "$API/messages/$FULL_ID" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"categories": []}' | jq '{status: "categories removed", subject: .subject, id: .id[-20:]}' + ;; + + focused) + # List focused inbox (important emails) + COUNT=${2:-10} + curl -s "$API/messages?\$filter=inferenceClassification%20eq%20'focused'&\$top=$COUNT&\$orderby=receivedDateTime%20desc&\$select=id,subject,from,receivedDateTime" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq 'if .value then (.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], id: .value.id[-20:]}) else {info: "Focused inbox not available or empty"} end' + ;; + + other) + # List "other" inbox (non-focused emails) + COUNT=${2:-10} + curl -s "$API/messages?\$filter=inferenceClassification%20eq%20'other'&\$top=$COUNT&\$orderby=receivedDateTime%20desc&\$select=id,subject,from,receivedDateTime" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq 'if .value then (.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], id: .value.id[-20:]}) else {info: "Other inbox not available or empty"} end' + ;; + + thread) + # List emails in same conversation/thread (by subject keyword) + MSG_ID="$2" + + FULL_ID=$(curl -s "$API/messages?\$top=100&\$select=id" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r ".value[] | select(.id | endswith(\"$MSG_ID\")) | .id" | head -1) + + if [ -z "$FULL_ID" ]; then + echo "Message not found" + exit 1 + fi + + # Get subject, clean prefixes + SUBJECT=$(curl -s "$API/messages/$FULL_ID?\$select=subject" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r '.subject' | sed 's/^RE: //i' | sed 's/^FW: //i' | sed 's/^Fwd: //i') + + # Get longest word as keyword (more specific) + KEYWORD=$(echo "$SUBJECT" | tr ' ' '\n' | awk '{print length, $0}' | sort -rn | head -1 | cut -d' ' -f2) + + if [ -z "$KEYWORD" ] || [ ${#KEYWORD} -lt 4 ]; then + KEYWORD=$(echo "$SUBJECT" | cut -d' ' -f1) + fi + + echo "Searching thread by keyword: $KEYWORD" + + # Search by single keyword + curl -s "$API/messages?\$search=\"$KEYWORD\"&\$top=20&\$select=id,subject,from,receivedDateTime" \ + -H "Authorization: Bearer $ACCESS_TOKEN" | jq '.value | to_entries | .[] | {n: (.key + 1), subject: .value.subject, from: .value.from.emailAddress.address, date: .value.receivedDateTime[0:16], id: .value.id[-20:]}' + ;; + + *) + echo "Usage: outlook-mail.sh [args]" + echo "" + echo "READING:" + echo " inbox [count] - List latest emails (default: 10)" + echo " unread [count] - List unread emails" + echo " focused [count] - Focused/important inbox" + echo " other [count] - Other/low-priority inbox" + echo " search \"query\" [count] - Search emails" + echo " from [count] - List emails from sender" + echo " read - Read email content" + echo " thread - View conversation thread" + echo " attachments - List email attachments" + echo "" + echo "MANAGING:" + echo " mark-read - Mark as read" + echo " mark-unread - Mark as unread" + echo " flag - Flag as important" + echo " unflag - Remove flag" + echo " delete - Move to trash" + echo " archive - Move to archive" + echo " move - Move to folder" + echo "" + echo "CATEGORIES (like Gmail labels):" + echo " categories - List available categories" + echo " categorize - Add category to email" + echo " uncategorize - Remove categories" + echo "" + echo "SENDING:" + echo " send - Send new email" + echo " reply \"body\" - Reply to email" + echo " forward [msg] - Forward email" + echo "" + echo "DRAFTS:" + echo " draft - Create draft (not sent)" + echo " drafts [count] - List drafts" + echo " send-draft - Send a draft" + echo "" + echo "ATTACHMENTS:" + echo " attachments - List attachments" + echo " download [path] - Download attachment" + echo "" + echo "FOLDERS:" + echo " folders - List mail folders" + echo " create-folder [parent] - Create folder" + echo " delete-folder - Delete folder" + echo "" + echo "BULK OPERATIONS:" + echo " bulk-read ... - Mark multiple as read" + echo " bulk-delete ... - Delete multiple" + echo "" + echo "INFO:" + echo " stats - Inbox statistics" + ;; +esac diff --git a/scripts/outlook-setup.sh b/scripts/outlook-setup.sh new file mode 100644 index 0000000..64c4075 --- /dev/null +++ b/scripts/outlook-setup.sh @@ -0,0 +1,251 @@ +#!/bin/bash +# Outlook OAuth Setup via Azure CLI +# Automatically creates App Registration and configures OAuth + +set -e + +CONFIG_DIR="$HOME/.outlook-mcp" +CONFIG_FILE="$CONFIG_DIR/config.json" +CREDS_FILE="$CONFIG_DIR/credentials.json" + +APP_NAME="Clawdbot-Outlook" +REDIRECT_URI="http://localhost" +SCOPES="https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/Calendars.ReadWrite offline_access" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Outlook OAuth Setup ===${NC}" +echo "" + +# Check prerequisites +check_prereqs() { + if ! command -v az &> /dev/null; then + echo -e "${RED}Error: Azure CLI not installed${NC}" + echo "Install with: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash" + exit 1 + fi + + if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: jq not installed${NC}" + echo "Install with: sudo apt install jq" + exit 1 + fi +} + +# Step 1: Azure Login +azure_login() { + echo -e "${YELLOW}Step 1: Azure Login${NC}" + + # Check if already logged in + if az account show &> /dev/null; then + CURRENT_USER=$(az account show --query user.name -o tsv) + echo -e "Currently logged in as: ${GREEN}$CURRENT_USER${NC}" + read -p "Continue with this account? [Y/n] " -n 1 -r + echo + if [[ $REPLY =~ ^[Nn]$ ]]; then + az logout 2>/dev/null || true + else + return 0 + fi + fi + + echo "Opening browser for Azure login..." + echo "(If no browser available, use: az login --use-device-code)" + + if ! az login --use-device-code; then + echo -e "${RED}Login failed${NC}" + exit 1 + fi + + echo -e "${GREEN}✓ Logged in successfully${NC}" +} + +# Step 2: Create App Registration +create_app() { + echo "" + echo -e "${YELLOW}Step 2: Creating App Registration${NC}" + + # Check if app already exists + EXISTING_APP=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv 2>/dev/null) + + if [ -n "$EXISTING_APP" ] && [ "$EXISTING_APP" != "null" ]; then + echo -e "App '$APP_NAME' already exists: ${BLUE}$EXISTING_APP${NC}" + read -p "Use existing app? [Y/n] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + APP_ID="$EXISTING_APP" + echo -e "${GREEN}✓ Using existing app${NC}" + return 0 + fi + APP_NAME="$APP_NAME-$(date +%s)" + echo "Creating new app: $APP_NAME" + fi + + # Create new app + APP_RESULT=$(az ad app create \ + --display-name "$APP_NAME" \ + --sign-in-audience "AzureADandPersonalMicrosoftAccount" \ + --web-redirect-uris "$REDIRECT_URI" \ + --query "{appId: appId, objectId: id}" -o json) + + APP_ID=$(echo "$APP_RESULT" | jq -r '.appId') + echo -e "${GREEN}✓ App created: $APP_ID${NC}" +} + +# Step 3: Create Client Secret +create_secret() { + echo "" + echo -e "${YELLOW}Step 3: Creating Client Secret${NC}" + + SECRET_RESULT=$(az ad app credential reset \ + --id "$APP_ID" \ + --display-name "clawdbot-secret" \ + --years 2 \ + --query "{clientId: appId, clientSecret: password}" -o json 2>&1) + + CLIENT_ID=$(echo "$SECRET_RESULT" | jq -r '.clientId') + CLIENT_SECRET=$(echo "$SECRET_RESULT" | jq -r '.clientSecret') + + if [ -z "$CLIENT_SECRET" ] || [ "$CLIENT_SECRET" = "null" ]; then + echo -e "${RED}Failed to create secret${NC}" + exit 1 + fi + + echo -e "${GREEN}✓ Secret created${NC}" +} + +# Step 4: Add API Permissions +add_permissions() { + echo "" + echo -e "${YELLOW}Step 4: Adding API Permissions${NC}" + + # Microsoft Graph API ID + GRAPH_API="00000003-0000-0000-c000-000000000000" + + # Get permission IDs + MAIL_RW_ID=$(az ad sp show --id "$GRAPH_API" --query "oauth2PermissionScopes[?value=='Mail.ReadWrite'].id" -o tsv 2>/dev/null) + MAIL_SEND_ID=$(az ad sp show --id "$GRAPH_API" --query "oauth2PermissionScopes[?value=='Mail.Send'].id" -o tsv 2>/dev/null) + CAL_RW_ID=$(az ad sp show --id "$GRAPH_API" --query "oauth2PermissionScopes[?value=='Calendars.ReadWrite'].id" -o tsv 2>/dev/null) + USER_READ_ID=$(az ad sp show --id "$GRAPH_API" --query "oauth2PermissionScopes[?value=='User.Read'].id" -o tsv 2>/dev/null) + + # Add permissions + az ad app permission add --id "$APP_ID" \ + --api "$GRAPH_API" \ + --api-permissions "$MAIL_RW_ID=Scope" "$MAIL_SEND_ID=Scope" "$CAL_RW_ID=Scope" "$USER_READ_ID=Scope" 2>/dev/null || true + + echo -e "${GREEN}✓ Permissions added (Mail.ReadWrite, Mail.Send, Calendars.ReadWrite, User.Read)${NC}" +} + +# Step 5: Save Config +save_config() { + echo "" + echo -e "${YELLOW}Step 5: Saving Configuration${NC}" + + mkdir -p "$CONFIG_DIR" + + cat > "$CONFIG_FILE" << EOF +{ + "client_id": "$CLIENT_ID", + "client_secret": "$CLIENT_SECRET" +} +EOF + + chmod 600 "$CONFIG_FILE" + echo -e "${GREEN}✓ Config saved to $CONFIG_FILE${NC}" +} + +# Step 6: User Authorization +authorize() { + echo "" + echo -e "${YELLOW}Step 6: User Authorization${NC}" + echo "" + + AUTH_URL="https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$CLIENT_ID&response_type=code&redirect_uri=$REDIRECT_URI&scope=$(echo $SCOPES | sed 's/ /%20/g')&response_mode=query" + + echo "Open this URL in your browser:" + echo "" + echo -e "${BLUE}$AUTH_URL${NC}" + echo "" + echo "After authorizing, you'll be redirected to a page that won't load." + echo "Copy the FULL URL from the address bar and paste it here:" + echo "" + read -p "URL: " REDIRECT_URL + + # Extract code from URL + AUTH_CODE=$(echo "$REDIRECT_URL" | grep -oP 'code=\K[^&]+' || echo "") + + if [ -z "$AUTH_CODE" ]; then + echo -e "${RED}Could not extract authorization code from URL${NC}" + exit 1 + fi + + echo "" + echo "Exchanging code for tokens..." + + TOKEN_RESPONSE=$(curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&code=$AUTH_CODE&redirect_uri=$REDIRECT_URI&grant_type=authorization_code&scope=$SCOPES") + + if echo "$TOKEN_RESPONSE" | jq -e '.access_token' > /dev/null 2>&1; then + echo "$TOKEN_RESPONSE" > "$CREDS_FILE" + chmod 600 "$CREDS_FILE" + echo -e "${GREEN}✓ Tokens saved to $CREDS_FILE${NC}" + else + echo -e "${RED}Failed to get tokens:${NC}" + echo "$TOKEN_RESPONSE" | jq '.error_description // .' + exit 1 + fi +} + +# Step 7: Test Connection +test_connection() { + echo "" + echo -e "${YELLOW}Step 7: Testing Connection${NC}" + + ACCESS_TOKEN=$(jq -r '.access_token' "$CREDS_FILE") + + INBOX=$(curl -s "https://graph.microsoft.com/v1.0/me/mailFolders/inbox" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + + if echo "$INBOX" | jq -e '.totalItemCount' > /dev/null 2>&1; then + TOTAL=$(echo "$INBOX" | jq '.totalItemCount') + UNREAD=$(echo "$INBOX" | jq '.unreadItemCount') + echo -e "${GREEN}✓ Connection successful!${NC}" + echo "" + echo -e "Inbox: ${BLUE}$TOTAL${NC} emails (${YELLOW}$UNREAD${NC} unread)" + else + echo -e "${RED}Connection test failed${NC}" + echo "$INBOX" | jq '.error.message // .' + exit 1 + fi +} + +# Main +main() { + check_prereqs + azure_login + create_app + create_secret + add_permissions + save_config + authorize + test_connection + + echo "" + echo -e "${GREEN}=== Setup Complete! ===${NC}" + echo "" + echo "You can now use:" + echo " outlook-mail.sh inbox - List emails" + echo " outlook-mail.sh unread - List unread" + echo " outlook-mail.sh search X - Search emails" + echo " outlook-calendar.sh today - Today's events" + echo " outlook-calendar.sh week - This week's events" + echo " outlook-token.sh refresh - Refresh token" +} + +main "$@" diff --git a/scripts/outlook-token.sh b/scripts/outlook-token.sh new file mode 100644 index 0000000..9066f81 --- /dev/null +++ b/scripts/outlook-token.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Outlook Token Manager +# Usage: outlook-token.sh [refresh|get|test] + +CONFIG_DIR="$HOME/.outlook-mcp" +CONFIG_FILE="$CONFIG_DIR/config.json" +CREDS_FILE="$CONFIG_DIR/credentials.json" + +# Check if config exists +if [ ! -f "$CONFIG_FILE" ] || [ ! -f "$CREDS_FILE" ]; then + echo "Error: Outlook not configured. Run setup first." + echo "Missing: $CONFIG_FILE or $CREDS_FILE" + exit 1 +fi + +# Load credentials +CLIENT_ID=$(jq -r '.client_id' "$CONFIG_FILE") +CLIENT_SECRET=$(jq -r '.client_secret' "$CONFIG_FILE") +ACCESS_TOKEN=$(jq -r '.access_token' "$CREDS_FILE") +REFRESH_TOKEN=$(jq -r '.refresh_token' "$CREDS_FILE") + +case "$1" in + refresh) + echo "Refreshing token..." + RESPONSE=$(curl -s -X POST "https://login.microsoftonline.com/common/oauth2/v2.0/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&refresh_token=$REFRESH_TOKEN&grant_type=refresh_token&scope=https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/Calendars.ReadWrite offline_access") + + if echo "$RESPONSE" | jq -e '.access_token' > /dev/null 2>&1; then + echo "$RESPONSE" > "$CREDS_FILE" + echo "Token refreshed successfully" + else + echo "Error refreshing token:" + echo "$RESPONSE" | jq '.error_description // .' + exit 1 + fi + ;; + + get) + echo "$ACCESS_TOKEN" + ;; + + test) + echo "Testing connection..." + RESULT=$(curl -s "https://graph.microsoft.com/v1.0/me/mailFolders/inbox" \ + -H "Authorization: Bearer $ACCESS_TOKEN") + + if echo "$RESULT" | jq -e '.totalItemCount' > /dev/null 2>&1; then + TOTAL=$(echo "$RESULT" | jq '.totalItemCount') + UNREAD=$(echo "$RESULT" | jq '.unreadItemCount') + echo "✓ Connected! Inbox: $TOTAL emails ($UNREAD unread)" + else + echo "✗ Connection failed. Try: outlook-token.sh refresh" + echo "$RESULT" | jq '.error.message // .' + exit 1 + fi + ;; + + *) + echo "Usage: outlook-token.sh [refresh|get|test]" + echo " refresh - Refresh the access token" + echo " get - Print current access token" + echo " test - Test the connection" + ;; +esac