mcpeasy 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75c32597b80568d81e9dd7b100c73b2ea60a8cdb8e3718683bfa28875e4ef098
4
- data.tar.gz: 5c345c73d578163d9b04b7262b39e1489258b14522dcd24189042a51fc9524f4
3
+ metadata.gz: 96510ec1696160a8727b73d66eb7fbe9e9ac267bcdbe7c2e3db1f3a7840657f2
4
+ data.tar.gz: 64d8e8b25d58845501acba165271cc31010efbff4d3b65799ed932ab244aee09
5
5
  SHA512:
6
- metadata.gz: 2109cd9167c500802fd0b9ea0145e92fe03de3cb46adca578d924200f4e258c96c3bca39fdf22b094b33920d803cb8792cffad53ec4b262be9363f285fcf7752
7
- data.tar.gz: 480c54c57d54194472dc4db20629dc4337cbf93697514ca40d4eb73e53b01028af9137dc83f6a7db1041dc4ca2047a613d40f0756a77bc8627c4fa6fe1e7969e
6
+ metadata.gz: 6558ab4692baf95e5144db4f888bf594e28f8031a564d5d1127d90197c509b422225ae00438b61e640b1adc1ac355a444f1361db60485586441004d26b379b54
7
+ data.tar.gz: 8a12fc568bd98fd4ca14ea21f8e8ae6a13ae39bf34eb877a0e1b5e4e69c581f491578710147040a84550c7c4e5e4d791695208b25951ae492d877e860fc71a4f
data/.mcp.json CHANGED
@@ -27,6 +27,15 @@
27
27
  ],
28
28
  "env": {}
29
29
  },
30
+ "gmail": {
31
+ "type": "stdio",
32
+ "command": "./bin/mcpz",
33
+ "args": [
34
+ "gmail",
35
+ "mcp"
36
+ ],
37
+ "env": {}
38
+ },
30
39
  "gmeet": {
31
40
  "type": "stdio",
32
41
  "command": "./bin/mcpz",
data/CHANGELOG.md CHANGED
@@ -5,7 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.3.0] - 2025-06-07
9
+
10
+ ### Added
11
+ - Gmail MCP server integration with comprehensive email support
12
+ - List and search emails with various filters
13
+ - Read email content including headers and body
14
+ - Send new emails and reply to existing ones
15
+ - Manage email labels, mark as read/unread
16
+ - Archive and trash emails
17
+ - Full OAuth2 authentication flow integration
9
18
 
10
19
  ## [0.2.0] - 2025-06-03
11
20
 
@@ -47,4 +56,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
47
56
  - Google Calendar integration with event listing and search
48
57
  - Google Drive integration with file search and content retrieval
49
58
  - Basic MCP server implementations for all services
50
- - Development tooling with standardrb for code quality
59
+ - Development tooling with standardrb for code quality
data/lib/mcpeasy/cli.rb CHANGED
@@ -103,6 +103,32 @@ module Mcpeasy
103
103
  end
104
104
  end
105
105
 
106
+ # Load the existing GmailCLI and extend it with MCP functionality
107
+ require_relative "../utilities/gmail/cli"
108
+
109
+ class GmailCommands < Gmail::CLI
110
+ namespace "gmail"
111
+
112
+ desc "mcp", "Run Gmail MCP server"
113
+ def mcp
114
+ require_relative "../utilities/gmail/mcp"
115
+ Gmail::MCPServer.new.run
116
+ end
117
+
118
+ desc "auth", "Authenticate with Gmail API"
119
+ def auth
120
+ require_relative "../utilities/gmail/service"
121
+ tool = Gmail::Service.new(skip_auth: true)
122
+ result = tool.authenticate
123
+ if result[:success]
124
+ puts "✅ Successfully authenticated with Gmail"
125
+ else
126
+ puts "❌ Authentication failed: #{result[:error]}"
127
+ exit 1
128
+ end
129
+ end
130
+ end
131
+
106
132
  class CLI < Thor
107
133
  desc "version", "Show mcpeasy version"
108
134
  def version
@@ -166,6 +192,9 @@ module Mcpeasy
166
192
  desc "notion COMMAND", "Notion commands"
167
193
  subcommand "notion", NotionCommands
168
194
 
195
+ desc "gmail COMMAND", "Gmail commands"
196
+ subcommand "gmail", GmailCommands
197
+
169
198
  class << self
170
199
  private
171
200
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mcpeasy
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -0,0 +1,278 @@
1
+ # Gmail MCP Server
2
+
3
+ A Model Context Protocol (MCP) server for Gmail integration with AI assistants.
4
+
5
+ ## Features
6
+
7
+ - **List emails** with filtering by date range, sender, subject, labels, and read/unread status
8
+ - **Search emails** using Gmail's powerful search syntax
9
+ - **Get email content** including full body, headers, and attachment information
10
+ - **Send emails** with support for CC, BCC, and reply-to fields
11
+ - **Reply to emails** with automatic threading and optional quoted text
12
+ - **Email management** - mark as read/unread, add/remove labels, archive, and trash
13
+ - **Test connection** to verify Gmail API connectivity
14
+
15
+ ## Prerequisites
16
+
17
+ 1. **Google Cloud Project** with Gmail API enabled
18
+ 2. **OAuth 2.0 credentials** (client ID and client secret)
19
+ 3. **Ruby environment** with required gems
20
+
21
+ ## Setup
22
+
23
+ ### 1. Enable Gmail API
24
+
25
+ 1. Go to the [Google Cloud Console](https://console.cloud.google.com)
26
+ 2. Create a new project or select an existing one
27
+ 3. Enable the Gmail API:
28
+ - Navigate to "APIs & Services" > "Library"
29
+ - Search for "Gmail API" and click "Enable"
30
+
31
+ ### 2. Create OAuth 2.0 Credentials
32
+
33
+ 1. Go to "APIs & Services" > "Credentials"
34
+ 2. Click "Create Credentials" > "OAuth 2.0 Client IDs"
35
+ 3. Set application type to "Desktop application"
36
+ 4. Download the JSON file containing your credentials
37
+
38
+ ### 3. Install and Configure MCPEasy
39
+
40
+ ```bash
41
+ # Install the gem
42
+ gem install mcpeasy
43
+
44
+ # Set up configuration directories
45
+ mcpz setup
46
+
47
+ # Save your Google credentials
48
+ mcpz set_google_credentials path/to/your/credentials.json
49
+
50
+ # Authenticate with Gmail (will open browser for OAuth)
51
+ mcpz gmail auth
52
+ ```
53
+
54
+ ### 4. Verify Setup
55
+
56
+ ```bash
57
+ # Test the connection
58
+ mcpz gmail test
59
+ ```
60
+
61
+ ## CLI Usage
62
+
63
+ ### Authentication
64
+ ```bash
65
+ # Authenticate with Gmail API
66
+ mcpz gmail auth
67
+ ```
68
+
69
+ ### Listing Emails
70
+ ```bash
71
+ # List recent emails
72
+ mcpz gmail list
73
+
74
+ # Filter by date range
75
+ mcpz gmail list --start_date 2024-01-01 --end_date 2024-01-31
76
+
77
+ # Filter by sender
78
+ mcpz gmail list --sender "someone@example.com"
79
+
80
+ # Filter by subject
81
+ mcpz gmail list --subject "Important"
82
+
83
+ # Filter by labels
84
+ mcpz gmail list --labels "inbox,important"
85
+
86
+ # Filter by read status
87
+ mcpz gmail list --read_status unread
88
+
89
+ # Limit results
90
+ mcpz gmail list --max_results 10
91
+ ```
92
+
93
+ ### Searching Emails
94
+ ```bash
95
+ # Basic search
96
+ mcpz gmail search "quarterly report"
97
+
98
+ # Advanced Gmail search syntax
99
+ mcpz gmail search "from:boss@company.com subject:urgent"
100
+ mcpz gmail search "has:attachment filename:pdf"
101
+ mcpz gmail search "is:unread after:2024/01/01"
102
+ ```
103
+
104
+ ### Reading Emails
105
+ ```bash
106
+ # Read a specific email by ID
107
+ mcpz gmail read 18c8b5d4e8f9a2b6
108
+ ```
109
+
110
+ ### Sending Emails
111
+ ```bash
112
+ # Send a basic email
113
+ mcpz gmail send \
114
+ --to "recipient@example.com" \
115
+ --subject "Hello from MCPEasy" \
116
+ --body "This is a test email sent via Gmail API."
117
+
118
+ # Send with CC and BCC
119
+ mcpz gmail send \
120
+ --to "recipient@example.com" \
121
+ --cc "cc@example.com" \
122
+ --bcc "bcc@example.com" \
123
+ --subject "Team Update" \
124
+ --body "Weekly team update..." \
125
+ --reply_to "noreply@example.com"
126
+ ```
127
+
128
+ ### Replying to Emails
129
+ ```bash
130
+ # Reply to an email
131
+ mcpz gmail reply 18c8b5d4e8f9a2b6 \
132
+ --body "Thank you for your message."
133
+
134
+ # Reply without including quoted original message
135
+ mcpz gmail reply 18c8b5d4e8f9a2b6 \
136
+ --body "Thank you for your message." \
137
+ --include_quoted false
138
+ ```
139
+
140
+ ### Email Management
141
+ ```bash
142
+ # Mark as read/unread
143
+ mcpz gmail mark_read 18c8b5d4e8f9a2b6
144
+ mcpz gmail mark_unread 18c8b5d4e8f9a2b6
145
+
146
+ # Add/remove labels
147
+ mcpz gmail add_label 18c8b5d4e8f9a2b6 "important"
148
+ mcpz gmail remove_label 18c8b5d4e8f9a2b6 "important"
149
+
150
+ # Archive email (remove from inbox)
151
+ mcpz gmail archive 18c8b5d4e8f9a2b6
152
+
153
+ # Move to trash
154
+ mcpz gmail trash 18c8b5d4e8f9a2b6
155
+ ```
156
+
157
+ ## MCP Server Usage
158
+
159
+ ### Running the Server
160
+
161
+ ```bash
162
+ # Start the Gmail MCP server
163
+ mcpz gmail mcp
164
+ ```
165
+
166
+ ### MCP Configuration
167
+
168
+ Add to your `.mcp.json` configuration:
169
+
170
+ ```json
171
+ {
172
+ "mcpServers": {
173
+ "gmail": {
174
+ "command": "mcpz",
175
+ "args": ["gmail", "mcp"]
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### Available MCP Tools
182
+
183
+ - `test_connection` - Test Gmail API connectivity
184
+ - `list_emails` - List emails with filtering options
185
+ - `search_emails` - Search emails using Gmail syntax
186
+ - `get_email_content` - Get full email content including attachments
187
+ - `send_email` - Send new emails
188
+ - `reply_to_email` - Reply to existing emails
189
+ - `mark_as_read` / `mark_as_unread` - Change read status
190
+ - `add_label` / `remove_label` - Manage email labels
191
+ - `archive_email` - Archive emails
192
+ - `trash_email` - Move emails to trash
193
+
194
+ ### Available MCP Prompts
195
+
196
+ - `check_email` - Check inbox for new messages
197
+ - `compose_email` - Compose and send emails
198
+ - `email_search` - Search through emails
199
+ - `email_management` - Manage emails (read/unread, archive, etc.)
200
+
201
+ ## Gmail Search Syntax
202
+
203
+ The Gmail MCP server supports Gmail's advanced search operators:
204
+
205
+ - `from:sender@example.com` - From specific sender
206
+ - `to:recipient@example.com` - To specific recipient
207
+ - `subject:keyword` - Subject contains keyword
208
+ - `has:attachment` - Has attachments
209
+ - `filename:pdf` - Attachment filename contains "pdf"
210
+ - `is:unread` / `is:read` - Read status
211
+ - `is:important` / `is:starred` - Importance/starred
212
+ - `label:labelname` - Has specific label
213
+ - `after:2024/01/01` / `before:2024/12/31` - Date ranges
214
+ - `newer_than:7d` / `older_than:1m` - Relative dates
215
+
216
+ ## API Scopes
217
+
218
+ This MCP server requires the following Gmail API scopes:
219
+
220
+ - `https://www.googleapis.com/auth/gmail.readonly` - Read access to Gmail
221
+ - `https://www.googleapis.com/auth/gmail.send` - Send emails
222
+ - `https://www.googleapis.com/auth/gmail.modify` - Modify email labels and status
223
+
224
+ ## Security Notes
225
+
226
+ - **OAuth tokens are stored locally** in `~/.config/mcpeasy/`
227
+ - **Tokens are automatically refreshed** when they expire
228
+ - **Only your authenticated user** can access emails through this server
229
+ - **No emails are stored** by the MCP server - all data comes directly from Gmail
230
+
231
+ ## Troubleshooting
232
+
233
+ ### Authentication Issues
234
+
235
+ ```bash
236
+ # Re-authenticate if you see authentication errors
237
+ mcpz gmail auth
238
+
239
+ # Check configuration status
240
+ mcpz config
241
+ ```
242
+
243
+ ### API Quota Errors
244
+
245
+ Gmail API has usage quotas. If you hit rate limits:
246
+ - Reduce the number of requests
247
+ - Add delays between operations
248
+ - Check your Google Cloud Console quota usage
249
+
250
+ ### Common Error Messages
251
+
252
+ - **"Gmail authentication required"** - Run `mcpz gmail auth`
253
+ - **"Google API credentials not configured"** - Run `mcpz set_google_credentials <path>`
254
+ - **"Gmail API Error: Insufficient Permission"** - Re-run authentication to grant necessary scopes
255
+
256
+ ## Development
257
+
258
+ The Gmail MCP server follows the same patterns as other MCPEasy services:
259
+
260
+ - `service.rb` - Core Gmail API functionality
261
+ - `cli.rb` - Thor-based CLI commands
262
+ - `mcp.rb` - MCP server implementation
263
+ - `README.md` - Documentation
264
+
265
+ For development setup:
266
+
267
+ ```bash
268
+ # Clone the repository
269
+ git clone https://github.com/your-repo/mcpeasy.git
270
+ cd mcpeasy
271
+
272
+ # Install dependencies
273
+ bundle install
274
+
275
+ # Build and install locally
276
+ gem build mcpeasy.gemspec
277
+ gem install mcpeasy-*.gem
278
+ ```
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "thor"
5
+ require_relative "service"
6
+
7
+ module Gmail
8
+ class CLI < Thor
9
+ desc "test", "Test the Gmail API connection"
10
+ def test
11
+ response = tool.test_connection
12
+
13
+ if response[:ok]
14
+ puts "✅ Successfully connected to Gmail"
15
+ puts " Email: #{response[:email]}"
16
+ puts " Messages: #{response[:messages_total]}"
17
+ puts " Threads: #{response[:threads_total]}"
18
+ else
19
+ warn "❌ Connection test failed"
20
+ end
21
+ rescue RuntimeError => e
22
+ puts "❌ Failed to connect to Gmail: #{e.message}\n\n#{e.backtrace.join("\n")}"
23
+ exit 1
24
+ end
25
+
26
+ desc "list", "List recent emails"
27
+ method_option :start_date, type: :string, aliases: "-s", desc: "Start date (YYYY-MM-DD)"
28
+ method_option :end_date, type: :string, aliases: "-e", desc: "End date (YYYY-MM-DD)"
29
+ method_option :max_results, type: :numeric, default: 20, aliases: "-n", desc: "Max number of emails"
30
+ method_option :sender, type: :string, aliases: "-f", desc: "Filter by sender email"
31
+ method_option :subject, type: :string, aliases: "-j", desc: "Filter by subject"
32
+ method_option :labels, type: :string, aliases: "-l", desc: "Filter by labels (comma-separated)"
33
+ method_option :read_status, type: :string, aliases: "-r", desc: "Filter by read status (read/unread)"
34
+ def list
35
+ labels = options[:labels]&.split(",")&.map(&:strip)
36
+
37
+ result = tool.list_emails(
38
+ start_date: options[:start_date],
39
+ end_date: options[:end_date],
40
+ max_results: options[:max_results],
41
+ sender: options[:sender],
42
+ subject: options[:subject],
43
+ labels: labels,
44
+ read_status: options[:read_status]
45
+ )
46
+ emails = result[:emails]
47
+
48
+ if emails.empty?
49
+ puts "📧 No emails found for the specified criteria"
50
+ else
51
+ puts "📧 Found #{result[:count]} email(s):"
52
+ emails.each_with_index do |email, index|
53
+ puts " #{index + 1}. #{email[:subject] || "No subject"}"
54
+ puts " From: #{email[:from]}"
55
+ puts " Date: #{email[:date]}"
56
+ puts " Snippet: #{email[:snippet]}" if email[:snippet]
57
+ puts " Labels: #{email[:labels].join(", ")}" if email[:labels]&.any?
58
+ puts " ID: #{email[:id]}"
59
+ puts
60
+ end
61
+ end
62
+ rescue RuntimeError => e
63
+ warn "❌ Failed to list emails: #{e.message}"
64
+ exit 1
65
+ end
66
+
67
+ desc "search QUERY", "Search emails by text content"
68
+ method_option :max_results, type: :numeric, default: 10, aliases: "-n", desc: "Max number of emails"
69
+ def search(query)
70
+ result = tool.search_emails(
71
+ query,
72
+ max_results: options[:max_results]
73
+ )
74
+ emails = result[:emails]
75
+
76
+ if emails.empty?
77
+ puts "🔍 No emails found matching '#{query}'"
78
+ else
79
+ puts "🔍 Found #{result[:count]} email(s) matching '#{query}':"
80
+ emails.each_with_index do |email, index|
81
+ puts " #{index + 1}. #{email[:subject] || "No subject"}"
82
+ puts " From: #{email[:from]}"
83
+ puts " Date: #{email[:date]}"
84
+ puts " Snippet: #{email[:snippet]}" if email[:snippet]
85
+ puts " ID: #{email[:id]}"
86
+ puts
87
+ end
88
+ end
89
+ rescue RuntimeError => e
90
+ warn "❌ Failed to search emails: #{e.message}"
91
+ exit 1
92
+ end
93
+
94
+ desc "read EMAIL_ID", "Read a specific email"
95
+ def read(email_id)
96
+ email = tool.get_email_content(email_id)
97
+
98
+ puts "📧 Email Details:"
99
+ puts " ID: #{email[:id]}"
100
+ puts " Thread ID: #{email[:thread_id]}"
101
+ puts " Subject: #{email[:subject] || "No subject"}"
102
+ puts " From: #{email[:from]}"
103
+ puts " To: #{email[:to]}"
104
+ puts " CC: #{email[:cc]}" if email[:cc]
105
+ puts " BCC: #{email[:bcc]}" if email[:bcc]
106
+ puts " Date: #{email[:date]}"
107
+ puts " Labels: #{email[:labels].join(", ")}" if email[:labels]&.any?
108
+
109
+ if email[:attachments]&.any?
110
+ puts " Attachments:"
111
+ email[:attachments].each do |attachment|
112
+ puts " - #{attachment[:filename]} (#{attachment[:mime_type]}, #{attachment[:size]} bytes)"
113
+ end
114
+ end
115
+
116
+ puts "\n📄 Body:"
117
+ puts email[:body]
118
+ rescue RuntimeError => e
119
+ warn "❌ Failed to read email: #{e.message}"
120
+ exit 1
121
+ end
122
+
123
+ desc "send", "Send a new email"
124
+ method_option :to, type: :string, required: true, aliases: "-t", desc: "Recipient email address"
125
+ method_option :subject, type: :string, required: true, aliases: "-s", desc: "Email subject"
126
+ method_option :body, type: :string, required: true, aliases: "-b", desc: "Email body"
127
+ method_option :cc, type: :string, aliases: "-c", desc: "CC email address"
128
+ method_option :bcc, type: :string, aliases: "-B", desc: "BCC email address"
129
+ method_option :reply_to, type: :string, aliases: "-r", desc: "Reply-to email address"
130
+ def send
131
+ result = tool.send_email(
132
+ to: options[:to],
133
+ subject: options[:subject],
134
+ body: options[:body],
135
+ cc: options[:cc],
136
+ bcc: options[:bcc],
137
+ reply_to: options[:reply_to]
138
+ )
139
+
140
+ if result[:success]
141
+ puts "✅ Email sent successfully"
142
+ puts " Message ID: #{result[:message_id]}"
143
+ puts " Thread ID: #{result[:thread_id]}"
144
+ else
145
+ puts "❌ Failed to send email"
146
+ end
147
+ rescue RuntimeError => e
148
+ warn "❌ Failed to send email: #{e.message}"
149
+ exit 1
150
+ end
151
+
152
+ desc "reply EMAIL_ID", "Reply to an email"
153
+ method_option :body, type: :string, required: true, aliases: "-b", desc: "Reply body"
154
+ method_option :include_quoted, type: :boolean, default: true, aliases: "-q", desc: "Include quoted original message"
155
+ def reply(email_id)
156
+ result = tool.reply_to_email(
157
+ email_id: email_id,
158
+ body: options[:body],
159
+ include_quoted: options[:include_quoted]
160
+ )
161
+
162
+ if result[:success]
163
+ puts "✅ Reply sent successfully"
164
+ puts " Message ID: #{result[:message_id]}"
165
+ puts " Thread ID: #{result[:thread_id]}"
166
+ else
167
+ puts "❌ Failed to send reply"
168
+ end
169
+ rescue RuntimeError => e
170
+ warn "❌ Failed to send reply: #{e.message}"
171
+ exit 1
172
+ end
173
+
174
+ desc "mark_read EMAIL_ID", "Mark an email as read"
175
+ def mark_read(email_id)
176
+ result = tool.mark_as_read(email_id)
177
+
178
+ if result[:success]
179
+ puts "✅ Email marked as read"
180
+ else
181
+ puts "❌ Failed to mark email as read"
182
+ end
183
+ rescue RuntimeError => e
184
+ warn "❌ Failed to mark email as read: #{e.message}"
185
+ exit 1
186
+ end
187
+
188
+ desc "mark_unread EMAIL_ID", "Mark an email as unread"
189
+ def mark_unread(email_id)
190
+ result = tool.mark_as_unread(email_id)
191
+
192
+ if result[:success]
193
+ puts "✅ Email marked as unread"
194
+ else
195
+ puts "❌ Failed to mark email as unread"
196
+ end
197
+ rescue RuntimeError => e
198
+ warn "❌ Failed to mark email as unread: #{e.message}"
199
+ exit 1
200
+ end
201
+
202
+ desc "add_label EMAIL_ID LABEL", "Add a label to an email"
203
+ def add_label(email_id, label)
204
+ result = tool.add_label(email_id, label)
205
+
206
+ if result[:success]
207
+ puts "✅ Label '#{label}' added to email"
208
+ else
209
+ puts "❌ Failed to add label to email"
210
+ end
211
+ rescue RuntimeError => e
212
+ warn "❌ Failed to add label to email: #{e.message}"
213
+ exit 1
214
+ end
215
+
216
+ desc "remove_label EMAIL_ID LABEL", "Remove a label from an email"
217
+ def remove_label(email_id, label)
218
+ result = tool.remove_label(email_id, label)
219
+
220
+ if result[:success]
221
+ puts "✅ Label '#{label}' removed from email"
222
+ else
223
+ puts "❌ Failed to remove label from email"
224
+ end
225
+ rescue RuntimeError => e
226
+ warn "❌ Failed to remove label from email: #{e.message}"
227
+ exit 1
228
+ end
229
+
230
+ desc "archive EMAIL_ID", "Archive an email"
231
+ def archive(email_id)
232
+ result = tool.archive_email(email_id)
233
+
234
+ if result[:success]
235
+ puts "✅ Email archived"
236
+ else
237
+ puts "❌ Failed to archive email"
238
+ end
239
+ rescue RuntimeError => e
240
+ warn "❌ Failed to archive email: #{e.message}"
241
+ exit 1
242
+ end
243
+
244
+ desc "trash EMAIL_ID", "Move an email to trash"
245
+ def trash(email_id)
246
+ result = tool.trash_email(email_id)
247
+
248
+ if result[:success]
249
+ puts "✅ Email moved to trash"
250
+ else
251
+ puts "❌ Failed to move email to trash"
252
+ end
253
+ rescue RuntimeError => e
254
+ warn "❌ Failed to move email to trash: #{e.message}"
255
+ exit 1
256
+ end
257
+
258
+ private
259
+
260
+ def tool
261
+ @tool ||= Service.new
262
+ end
263
+ end
264
+ end