Initial commit with translated description
This commit is contained in:
237
SKILL.md
Normal file
237
SKILL.md
Normal file
@@ -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 <email> [count] # List emails from sender
|
||||||
|
./scripts/outlook-mail.sh read <id> # Read email content
|
||||||
|
./scripts/outlook-mail.sh attachments <id> # List email attachments
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing Emails
|
||||||
|
```bash
|
||||||
|
./scripts/outlook-mail.sh mark-read <id> # Mark as read
|
||||||
|
./scripts/outlook-mail.sh mark-unread <id> # Mark as unread
|
||||||
|
./scripts/outlook-mail.sh flag <id> # Flag as important
|
||||||
|
./scripts/outlook-mail.sh unflag <id> # Remove flag
|
||||||
|
./scripts/outlook-mail.sh delete <id> # Move to trash
|
||||||
|
./scripts/outlook-mail.sh archive <id> # Move to archive
|
||||||
|
./scripts/outlook-mail.sh move <id> <folder> # Move to folder
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending Emails
|
||||||
|
```bash
|
||||||
|
./scripts/outlook-mail.sh send <to> <subj> <body> # Send new email
|
||||||
|
./scripts/outlook-mail.sh reply <id> "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 <id> # Event details
|
||||||
|
./scripts/outlook-calendar.sh calendars # List all calendars
|
||||||
|
./scripts/outlook-calendar.sh free <start> <end> # Check availability
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Events
|
||||||
|
```bash
|
||||||
|
./scripts/outlook-calendar.sh create <subj> <start> <end> [location] # Create event
|
||||||
|
./scripts/outlook-calendar.sh quick <subject> [time] # Quick 1-hour event
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing Events
|
||||||
|
```bash
|
||||||
|
./scripts/outlook-calendar.sh update <id> <field> <value> # Update (subject/location/start/end)
|
||||||
|
./scripts/outlook-calendar.sh delete <id> # 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
|
||||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn7d9cz2hq6kr5vs1gxqghbne17zxc5q",
|
||||||
|
"slug": "outlook",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"publishedAt": 1769367682148
|
||||||
|
}
|
||||||
127
references/setup.md
Normal file
127
references/setup.md
Normal file
@@ -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
|
||||||
238
scripts/outlook-calendar.sh
Normal file
238
scripts/outlook-calendar.sh
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Outlook Calendar Operations
|
||||||
|
# Usage: outlook-calendar.sh <command> [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 <subject> <start> <end> [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 <subject> [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 <id> <field> <value>
|
||||||
|
EVENT_ID="$2"
|
||||||
|
FIELD="$3"
|
||||||
|
VALUE="$4"
|
||||||
|
|
||||||
|
if [ -z "$FIELD" ] || [ -z "$VALUE" ]; then
|
||||||
|
echo "Usage: outlook-calendar.sh update <id> <field> <value>"
|
||||||
|
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 <start> <end>"
|
||||||
|
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 <command> [args]"
|
||||||
|
echo ""
|
||||||
|
echo "VIEW:"
|
||||||
|
echo " events [count] - List upcoming events"
|
||||||
|
echo " today - Today's events"
|
||||||
|
echo " week - This week's events"
|
||||||
|
echo " read <id> - Event details"
|
||||||
|
echo " calendars - List all calendars"
|
||||||
|
echo " free <start> <end> - Check availability"
|
||||||
|
echo ""
|
||||||
|
echo "CREATE:"
|
||||||
|
echo " create <subj> <start> <end> [loc] - Create event"
|
||||||
|
echo " quick <subject> [time] - Quick 1-hour event"
|
||||||
|
echo ""
|
||||||
|
echo "MANAGE:"
|
||||||
|
echo " update <id> <field> <val> - Update event"
|
||||||
|
echo " delete <id> - Delete event"
|
||||||
|
echo ""
|
||||||
|
echo "Date format: YYYY-MM-DDTHH:MM (e.g., 2026-01-26T10:00)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
707
scripts/outlook-mail.sh
Normal file
707
scripts/outlook-mail.sh
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Outlook Mail Operations
|
||||||
|
# Usage: outlook-mail.sh <command> [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 <to> <subject> <body>"
|
||||||
|
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 <id> "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 <id> <folder>
|
||||||
|
MSG_ID="$2"
|
||||||
|
FOLDER="$3"
|
||||||
|
|
||||||
|
if [ -z "$FOLDER" ]; then
|
||||||
|
echo "Usage: outlook-mail.sh move <id> <folder>"
|
||||||
|
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 <to> <subject> <body>"
|
||||||
|
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 <id> <to> [comment]
|
||||||
|
MSG_ID="$2"
|
||||||
|
TO="$3"
|
||||||
|
COMMENT="${4:-}"
|
||||||
|
|
||||||
|
if [ -z "$TO" ]; then
|
||||||
|
echo "Usage: outlook-mail.sh forward <id> <to> [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 <msg-id> <attachment-name> [output-path]
|
||||||
|
MSG_ID="$2"
|
||||||
|
ATT_NAME="$3"
|
||||||
|
OUTPUT="${4:-.}"
|
||||||
|
|
||||||
|
if [ -z "$ATT_NAME" ]; then
|
||||||
|
echo "Usage: outlook-mail.sh download <msg-id> <attachment-name> [output-path]"
|
||||||
|
echo "Use 'attachments <id>' 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 <name> [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 <name>"
|
||||||
|
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 <id1> <id2> ...
|
||||||
|
shift
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: outlook-mail.sh bulk-read <id1> <id2> <id3> ..."
|
||||||
|
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 <id1> <id2> ...
|
||||||
|
shift
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: outlook-mail.sh bulk-delete <id1> <id2> <id3> ..."
|
||||||
|
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 <id> <category-name>
|
||||||
|
MSG_ID="$2"
|
||||||
|
CATEGORY="$3"
|
||||||
|
|
||||||
|
if [ -z "$CATEGORY" ]; then
|
||||||
|
echo "Usage: outlook-mail.sh categorize <id> <category-name>"
|
||||||
|
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 <command> [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 <email> [count] - List emails from sender"
|
||||||
|
echo " read <id> - Read email content"
|
||||||
|
echo " thread <id> - View conversation thread"
|
||||||
|
echo " attachments <id> - List email attachments"
|
||||||
|
echo ""
|
||||||
|
echo "MANAGING:"
|
||||||
|
echo " mark-read <id> - Mark as read"
|
||||||
|
echo " mark-unread <id> - Mark as unread"
|
||||||
|
echo " flag <id> - Flag as important"
|
||||||
|
echo " unflag <id> - Remove flag"
|
||||||
|
echo " delete <id> - Move to trash"
|
||||||
|
echo " archive <id> - Move to archive"
|
||||||
|
echo " move <id> <folder> - Move to folder"
|
||||||
|
echo ""
|
||||||
|
echo "CATEGORIES (like Gmail labels):"
|
||||||
|
echo " categories - List available categories"
|
||||||
|
echo " categorize <id> <name> - Add category to email"
|
||||||
|
echo " uncategorize <id> - Remove categories"
|
||||||
|
echo ""
|
||||||
|
echo "SENDING:"
|
||||||
|
echo " send <to> <subj> <body> - Send new email"
|
||||||
|
echo " reply <id> \"body\" - Reply to email"
|
||||||
|
echo " forward <id> <to> [msg] - Forward email"
|
||||||
|
echo ""
|
||||||
|
echo "DRAFTS:"
|
||||||
|
echo " draft <to> <subj> <body> - Create draft (not sent)"
|
||||||
|
echo " drafts [count] - List drafts"
|
||||||
|
echo " send-draft <id> - Send a draft"
|
||||||
|
echo ""
|
||||||
|
echo "ATTACHMENTS:"
|
||||||
|
echo " attachments <id> - List attachments"
|
||||||
|
echo " download <id> <name> [path] - Download attachment"
|
||||||
|
echo ""
|
||||||
|
echo "FOLDERS:"
|
||||||
|
echo " folders - List mail folders"
|
||||||
|
echo " create-folder <name> [parent] - Create folder"
|
||||||
|
echo " delete-folder <name> - Delete folder"
|
||||||
|
echo ""
|
||||||
|
echo "BULK OPERATIONS:"
|
||||||
|
echo " bulk-read <id1> <id2>... - Mark multiple as read"
|
||||||
|
echo " bulk-delete <id1> <id2>... - Delete multiple"
|
||||||
|
echo ""
|
||||||
|
echo "INFO:"
|
||||||
|
echo " stats - Inbox statistics"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
251
scripts/outlook-setup.sh
Normal file
251
scripts/outlook-setup.sh
Normal file
@@ -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 "$@"
|
||||||
65
scripts/outlook-token.sh
Normal file
65
scripts/outlook-token.sh
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user