Da-123 commited on
Commit
c5c5634
Β·
verified Β·
1 Parent(s): 2312d97

authChange (#10)

Browse files

- google auth done (627b659cd240e1470e45f0954a416c6e09ce5997)
- removed open ai involvement (6b7ccb572f2f962f5766c8e814961897080debc8)

agentic_implementation/README_OAuth.md ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gmail MCP Server with OAuth Authentication
2
+
3
+ This is an enhanced version of the Gmail MCP (Model Context Protocol) server that uses **OAuth 2.0 authentication** instead of requiring users to provide email credentials for each query.
4
+
5
+ ## πŸš€ Key Features
6
+
7
+ - **OAuth 2.0 Authentication**: Secure authentication flow using Google's OAuth system
8
+ - **One-time Setup**: Authenticate once, use anywhere
9
+ - **Automatic Token Refresh**: Handles token expiration automatically
10
+ - **Encrypted Storage**: Credentials are encrypted and stored securely
11
+ - **No More Password Sharing**: No need to provide email/password to Claude
12
+
13
+ ## πŸ“‹ Prerequisites
14
+
15
+ 1. **Google Account**: You need a Gmail account
16
+ 2. **Google Cloud Project**: Free to create
17
+ 3. **Python 3.8+**: Required for running the server
18
+
19
+ ## πŸ› οΈ Setup Instructions
20
+
21
+ ### Step 1: Install Dependencies
22
+
23
+ ```bash
24
+ pip install -r requirements_oauth.txt
25
+ ```
26
+
27
+ ### Step 2: Run the Interactive Setup
28
+
29
+ The setup script will guide you through the entire process:
30
+
31
+ ```bash
32
+ python setup_oauth.py
33
+ ```
34
+
35
+ This will walk you through:
36
+ 1. Creating a Google Cloud project
37
+ 2. Enabling the Gmail API
38
+ 3. Setting up OAuth consent screen
39
+ 4. Creating OAuth credentials
40
+ 5. Testing the authentication flow
41
+
42
+ ### Step 3: Start the MCP Server
43
+
44
+ ```bash
45
+ python email_mcp_server_oauth.py
46
+ ```
47
+
48
+ The server will start and show you:
49
+ - Authentication status
50
+ - MCP endpoint URL
51
+ - Web interface URL
52
+
53
+ ## πŸ”§ Claude Desktop Configuration
54
+
55
+ Add this configuration to your Claude Desktop MCP settings:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "gmail-oauth": {
61
+ "command": "npx",
62
+ "args": [
63
+ "mcp-remote",
64
+ "http://localhost:7860/gradio_api/mcp/sse"
65
+ ]
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## πŸ” Available Tools
72
+
73
+ ### 1. search_emails
74
+ Search your emails using natural language queries - **no credentials needed!**
75
+
76
+ **Parameters:**
77
+ - `query`: Natural language query (e.g., "show me emails from amazon last week")
78
+
79
+ **Example Usage in Claude:**
80
+ > "Can you search my emails for messages from Swiggy in the last week?"
81
+
82
+ ### 2. get_email_details
83
+ Get full details of a specific email by message ID.
84
+
85
+ **Parameters:**
86
+ - `message_id`: Message ID from search results
87
+
88
+ ### 3. analyze_email_patterns
89
+ Analyze email patterns from a specific sender over time.
90
+
91
+ **Parameters:**
92
+ - `sender_keyword`: Sender to analyze (e.g., "amazon", "google")
93
+ - `days_back`: Number of days to analyze (default: "30")
94
+
95
+ ### 4. authenticate_user
96
+ Trigger the OAuth authentication flow from Claude Desktop.
97
+
98
+ **Parameters:** None
99
+
100
+ ### 5. get_authentication_status
101
+ Check current authentication status.
102
+
103
+ **Parameters:** None
104
+
105
+ ## πŸ” Security Features
106
+
107
+ ### Encrypted Storage
108
+ - All credentials are encrypted using Fernet encryption
109
+ - Encryption keys are stored securely with proper permissions
110
+ - No plaintext credentials are ever stored
111
+
112
+ ### OAuth Benefits
113
+ - No need to share Gmail passwords
114
+ - Granular permission control
115
+ - Easy revocation from Google Account settings
116
+ - Automatic token refresh
117
+
118
+ ### Local Storage
119
+ - All data stored locally on your machine
120
+ - No cloud storage of credentials
121
+ - You maintain full control
122
+
123
+ ## πŸ”§ Advanced Usage
124
+
125
+ ### Command Line Tools
126
+
127
+ Check authentication status:
128
+ ```bash
129
+ python setup_oauth.py --status
130
+ ```
131
+
132
+ Re-authenticate:
133
+ ```bash
134
+ python setup_oauth.py --auth
135
+ ```
136
+
137
+ Clear stored credentials:
138
+ ```bash
139
+ python setup_oauth.py --clear
140
+ ```
141
+
142
+ Show help:
143
+ ```bash
144
+ python setup_oauth.py --help
145
+ ```
146
+
147
+ ### Web Interface
148
+
149
+ When the server is running, you can access the web interface at:
150
+ ```
151
+ http://localhost:7860
152
+ ```
153
+
154
+ Use this interface to:
155
+ - Check authentication status
156
+ - Trigger authentication flow
157
+ - Test email search functionality
158
+
159
+ ## πŸ†š Comparison: OAuth vs App Passwords
160
+
161
+ | Feature | App Password (Old) | OAuth (New) |
162
+ |---------|-------------------|-------------|
163
+ | **Setup Complexity** | Simple | One-time setup required |
164
+ | **Security** | Share app password | No password sharing |
165
+ | **User Experience** | Enter credentials each time | Authenticate once |
166
+ | **Revocation** | Change app password | Revoke from Google Account |
167
+ | **Token Management** | Manual | Automatic refresh |
168
+ | **Scope Control** | Full Gmail access | Granular permissions |
169
+
170
+ ## πŸ› Troubleshooting
171
+
172
+ ### Authentication Issues
173
+
174
+ **"OAuth not configured" error:**
175
+ ```bash
176
+ python setup_oauth.py
177
+ ```
178
+
179
+ **"Not authenticated" error:**
180
+ ```bash
181
+ python setup_oauth.py --auth
182
+ ```
183
+
184
+ **Authentication timeout:**
185
+ - Check if port 8080 is available
186
+ - Try disabling firewall temporarily
187
+ - Ensure browser can access localhost:8080
188
+
189
+ ### Common Issues
190
+
191
+ **"No module named 'google.auth'" error:**
192
+ ```bash
193
+ pip install -r requirements_oauth.txt
194
+ ```
195
+
196
+ **"Permission denied" on credential files:**
197
+ ```bash
198
+ # Check permissions
199
+ ls -la ~/.mailquery_oauth/
200
+ # Should show restricted permissions (600/700)
201
+ ```
202
+
203
+ **Browser doesn't open:**
204
+ - Copy the authorization URL manually
205
+ - Paste it in your browser
206
+ - Complete the flow manually
207
+
208
+ ### Getting Help
209
+
210
+ 1. Check authentication status: `python setup_oauth.py --status`
211
+ 2. Review server logs for detailed error messages
212
+ 3. Ensure Google Cloud project is properly configured
213
+ 4. Verify OAuth consent screen is set up correctly
214
+
215
+ ## πŸ“ File Structure
216
+
217
+ ```
218
+ ~/.mailquery_oauth/
219
+ β”œβ”€β”€ client_secret.json # OAuth client configuration
220
+ β”œβ”€β”€ token.pickle # Encrypted access/refresh tokens
221
+ └── key.key # Encryption key (secure permissions)
222
+ ```
223
+
224
+ ## πŸ”„ Migration from App Password Version
225
+
226
+ If you're migrating from the app password version:
227
+
228
+ 1. Run the new OAuth setup: `python setup_oauth.py`
229
+ 2. Update your Claude Desktop configuration to use the new server
230
+ 3. The old environment variables (EMAIL_ID, APP_PASSWORD) are no longer needed
231
+
232
+ ## πŸ“ž Support
233
+
234
+ For issues or questions:
235
+ 1. Check the troubleshooting section above
236
+ 2. Review the setup script output for specific guidance
237
+ 3. Ensure all prerequisites are met
238
+ 4. Verify Google Cloud project configuration
239
+
240
+ ## 🎯 Example Queries for Claude
241
+
242
+ Once set up, you can ask Claude:
243
+
244
+ - "Search my emails for messages from Amazon in the last month"
245
+ - "Show me emails from my bank from last week"
246
+ - "Analyze my LinkedIn email patterns over the last 60 days"
247
+ - "Find emails from Swiggy today"
248
+ - "Get details of the email with ID xyz123"
249
+
250
+ Claude will automatically use the OAuth-authenticated tools without asking for credentials!
251
+
252
+ ## πŸ”’ Privacy & Data
253
+
254
+ - **No data leaves your machine**: All processing happens locally
255
+ - **Google only provides**: Access to your Gmail via official APIs
256
+ - **We store**: Encrypted authentication tokens only
257
+ - **We never store**: Email content, passwords, or personal data
258
+ - **You control**: Access can be revoked anytime from Google Account settings
agentic_implementation/email_mcp_server_oauth.py ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gmail MCP Server with OAuth Authentication and Multi-Account Support
4
+ """
5
+
6
+ import gradio as gr
7
+ import json
8
+ import os
9
+ from typing import Dict, List
10
+ from datetime import datetime, timedelta
11
+ from dotenv import load_dotenv
12
+
13
+ # Import OAuth-enabled modules
14
+ from tools import extract_query_info, analyze_emails
15
+ from gmail_api_scraper import GmailAPIScraper
16
+ from oauth_manager import oauth_manager
17
+ from logger import logger
18
+
19
+ load_dotenv()
20
+
21
+ # Initialize Gmail API scraper
22
+ gmail_scraper = GmailAPIScraper()
23
+
24
+ def check_authentication() -> tuple[bool, str]:
25
+ """Check if user is authenticated and return status"""
26
+ current_account = oauth_manager.get_current_account()
27
+ if current_account and oauth_manager.is_authenticated():
28
+ return True, current_account
29
+ else:
30
+ return False, "Not authenticated"
31
+
32
+ def simple_analyze_emails(emails) -> dict:
33
+ """
34
+ Simple email analysis without OpenAI - just basic statistics and patterns
35
+ """
36
+ if not emails:
37
+ return {"summary": "No emails to analyze.", "insights": []}
38
+
39
+ # Basic statistics
40
+ total_count = len(emails)
41
+
42
+ # Group by sender
43
+ senders = {}
44
+ subjects = []
45
+ dates = []
46
+
47
+ for email in emails:
48
+ sender = email.get("from", "Unknown")
49
+ # Extract just the email domain for grouping
50
+ if "<" in sender and ">" in sender:
51
+ email_part = sender.split("<")[1].split(">")[0]
52
+ else:
53
+ email_part = sender
54
+
55
+ domain = email_part.split("@")[-1] if "@" in email_part else sender
56
+
57
+ senders[domain] = senders.get(domain, 0) + 1
58
+ subjects.append(email.get("subject", ""))
59
+ dates.append(email.get("date", ""))
60
+
61
+ # Create insights
62
+ insights = []
63
+ insights.append(f"Found {total_count} emails total")
64
+
65
+ if senders:
66
+ top_sender = max(senders.items(), key=lambda x: x[1])
67
+ insights.append(f"Most emails from: {top_sender[0]} ({top_sender[1]} emails)")
68
+
69
+ if len(senders) > 1:
70
+ insights.append(f"Emails from {len(senders)} different domains")
71
+
72
+ # Date range
73
+ if dates:
74
+ unique_dates = list(set(dates))
75
+ if len(unique_dates) > 1:
76
+ insights.append(f"Spanning {len(unique_dates)} different days")
77
+
78
+ # Subject analysis
79
+ if subjects:
80
+ # Count common words in subjects (simple approach)
81
+ all_words = []
82
+ for subject in subjects:
83
+ words = subject.lower().split()
84
+ all_words.extend([w for w in words if len(w) > 3]) # Only words longer than 3 chars
85
+
86
+ if all_words:
87
+ word_counts = {}
88
+ for word in all_words:
89
+ word_counts[word] = word_counts.get(word, 0) + 1
90
+
91
+ if word_counts:
92
+ common_word = max(word_counts.items(), key=lambda x: x[1])
93
+ if common_word[1] > 1:
94
+ insights.append(f"Common subject word: '{common_word[0]}' appears {common_word[1]} times")
95
+
96
+ summary = f"Analysis of {total_count} emails from {len(senders)} sender(s)"
97
+
98
+ return {
99
+ "summary": summary,
100
+ "insights": insights
101
+ }
102
+
103
+ def authenticate_user() -> str:
104
+ """
105
+ Start OAuth authentication flow for Gmail access.
106
+ Opens a browser window for user to authenticate with Google.
107
+
108
+ Returns:
109
+ str: JSON string containing authentication result
110
+ """
111
+ try:
112
+ logger.info("Starting OAuth authentication flow...")
113
+
114
+ # Check if OAuth is configured
115
+ if not oauth_manager.client_secrets_file.exists():
116
+ return json.dumps({
117
+ "error": "OAuth not configured",
118
+ "message": "Please run 'python setup_oauth.py' first to configure OAuth credentials.",
119
+ "success": False
120
+ }, indent=2)
121
+
122
+ # Start authentication
123
+ success = oauth_manager.authenticate_interactive()
124
+
125
+ if success:
126
+ user_email = oauth_manager.get_current_account()
127
+ result = {
128
+ "success": True,
129
+ "message": "Authentication successful! You can now use the email tools.",
130
+ "user_email": user_email,
131
+ "instructions": [
132
+ "Authentication completed successfully",
133
+ "You can now search emails, get email details, and analyze patterns",
134
+ f"Currently authenticated as: {user_email}"
135
+ ]
136
+ }
137
+ else:
138
+ result = {
139
+ "success": False,
140
+ "error": "Authentication failed",
141
+ "message": "Please try again or check your internet connection.",
142
+ "instructions": [
143
+ "Make sure you have internet connection",
144
+ "Ensure you complete the authentication in the browser",
145
+ "Try running 'python setup_oauth.py' if problems persist"
146
+ ]
147
+ }
148
+
149
+ return json.dumps(result, indent=2)
150
+
151
+ except Exception as e:
152
+ logger.error("Error in authenticate_user: %s", e)
153
+ error_result = {
154
+ "success": False,
155
+ "error": str(e),
156
+ "message": "Authentication failed due to an error."
157
+ }
158
+ return json.dumps(error_result, indent=2)
159
+
160
+ def switch_account(target_email: str) -> str:
161
+ """
162
+ Switch to a different authenticated Gmail account.
163
+
164
+ Args:
165
+ target_email (str): Email address to switch to
166
+
167
+ Returns:
168
+ str: JSON string containing switch result
169
+ """
170
+ try:
171
+ logger.info("Switching to account: %s", target_email)
172
+
173
+ # Check if target account is authenticated
174
+ if not oauth_manager.is_authenticated(target_email):
175
+ return json.dumps({
176
+ "error": "Account not authenticated",
177
+ "message": f"Account '{target_email}' is not authenticated. Please authenticate first.",
178
+ "target_email": target_email,
179
+ "authenticated_accounts": list(oauth_manager.list_accounts().keys())
180
+ }, indent=2)
181
+
182
+ # Switch account
183
+ success = oauth_manager.switch_account(target_email)
184
+
185
+ if success:
186
+ result = {
187
+ "success": True,
188
+ "message": f"Successfully switched to account: {target_email}",
189
+ "current_account": oauth_manager.get_current_account(),
190
+ "previous_account": None # Could track this if needed
191
+ }
192
+ else:
193
+ result = {
194
+ "success": False,
195
+ "error": "Failed to switch account",
196
+ "message": f"Could not switch to account: {target_email}",
197
+ "current_account": oauth_manager.get_current_account()
198
+ }
199
+
200
+ return json.dumps(result, indent=2)
201
+
202
+ except Exception as e:
203
+ logger.error("Error switching account: %s", e)
204
+ error_result = {
205
+ "success": False,
206
+ "error": str(e),
207
+ "message": f"Failed to switch to account: {target_email}"
208
+ }
209
+ return json.dumps(error_result, indent=2)
210
+
211
+ def list_accounts() -> str:
212
+ """
213
+ List all authenticated Gmail accounts and their status.
214
+
215
+ Returns:
216
+ str: JSON string containing all accounts and their authentication status
217
+ """
218
+ try:
219
+ logger.info("Listing all accounts")
220
+
221
+ accounts = oauth_manager.list_accounts()
222
+ current_account = oauth_manager.get_current_account()
223
+
224
+ result = {
225
+ "accounts": accounts,
226
+ "current_account": current_account,
227
+ "total_accounts": len(accounts),
228
+ "authenticated_accounts": [email for email, is_auth in accounts.items() if is_auth],
229
+ "message": f"Found {len(accounts)} stored accounts, currently using: {current_account or 'None'}"
230
+ }
231
+
232
+ return json.dumps(result, indent=2)
233
+
234
+ except Exception as e:
235
+ logger.error("Error listing accounts: %s", e)
236
+ error_result = {
237
+ "error": str(e),
238
+ "message": "Failed to list accounts"
239
+ }
240
+ return json.dumps(error_result, indent=2)
241
+
242
+ def remove_account(email_to_remove: str) -> str:
243
+ """
244
+ Remove an authenticated Gmail account and its stored credentials.
245
+
246
+ Args:
247
+ email_to_remove (str): Email address to remove
248
+
249
+ Returns:
250
+ str: JSON string containing removal result
251
+ """
252
+ try:
253
+ logger.info("Removing account: %s", email_to_remove)
254
+
255
+ # Check if account exists
256
+ accounts = oauth_manager.list_accounts()
257
+ if email_to_remove not in accounts:
258
+ return json.dumps({
259
+ "error": "Account not found",
260
+ "message": f"Account '{email_to_remove}' not found in stored accounts.",
261
+ "available_accounts": list(accounts.keys())
262
+ }, indent=2)
263
+
264
+ # Remove account
265
+ oauth_manager.remove_account(email_to_remove)
266
+
267
+ result = {
268
+ "success": True,
269
+ "message": f"Successfully removed account: {email_to_remove}",
270
+ "removed_account": email_to_remove,
271
+ "current_account": oauth_manager.get_current_account(),
272
+ "remaining_accounts": list(oauth_manager.list_accounts().keys())
273
+ }
274
+
275
+ return json.dumps(result, indent=2)
276
+
277
+ except Exception as e:
278
+ logger.error("Error removing account: %s", e)
279
+ error_result = {
280
+ "success": False,
281
+ "error": str(e),
282
+ "message": f"Failed to remove account: {email_to_remove}"
283
+ }
284
+ return json.dumps(error_result, indent=2)
285
+
286
+ def search_emails(sender_keyword: str, start_date: str = "", end_date: str = "") -> str:
287
+ """
288
+ Search for emails from a specific sender within a date range using OAuth authentication.
289
+
290
+ Args:
291
+ sender_keyword (str): The sender/company keyword to search for (e.g., "apple", "amazon")
292
+ start_date (str): Start date in DD-MMM-YYYY format (e.g., "01-Jan-2025"). If empty, defaults to 7 days ago.
293
+ end_date (str): End date in DD-MMM-YYYY format (e.g., "07-Jan-2025"). If empty, defaults to today.
294
+
295
+ Returns:
296
+ str: JSON string containing email search results and analysis
297
+ """
298
+ try:
299
+ logger.info("OAuth Email search tool called with sender: %s, dates: %s to %s", sender_keyword, start_date, end_date)
300
+
301
+ # Check authentication
302
+ is_auth, auth_info = check_authentication()
303
+ if not is_auth:
304
+ return json.dumps({
305
+ "error": "Not authenticated",
306
+ "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
307
+ "auth_status": auth_info
308
+ }, indent=2)
309
+
310
+ # Set default date range if not provided
311
+ if not start_date or not end_date:
312
+ today = datetime.today()
313
+ if not end_date:
314
+ end_date = today.strftime("%d-%b-%Y")
315
+ if not start_date:
316
+ start_date = (today - timedelta(days=7)).strftime("%d-%b-%Y")
317
+
318
+ logger.info(f"Searching for emails with keyword '{sender_keyword}' between {start_date} and {end_date}")
319
+
320
+ # Use Gmail API scraper with OAuth
321
+ full_emails = gmail_scraper.search_emails(sender_keyword, start_date, end_date)
322
+
323
+ if not full_emails:
324
+ result = {
325
+ "sender_keyword": sender_keyword,
326
+ "date_range": f"{start_date} to {end_date}",
327
+ "email_summary": [],
328
+ "analysis": {"summary": f"No emails found for '{sender_keyword}' in the specified date range.", "insights": []},
329
+ "email_count": 0,
330
+ "user_email": auth_info
331
+ }
332
+ return json.dumps(result, indent=2)
333
+
334
+ # Create summary version without full content
335
+ email_summary = []
336
+ for email in full_emails:
337
+ summary_email = {
338
+ "date": email.get("date"),
339
+ "time": email.get("time"),
340
+ "subject": email.get("subject"),
341
+ "from": email.get("from", "Unknown Sender"),
342
+ "message_id": email.get("message_id"),
343
+ "gmail_id": email.get("gmail_id")
344
+ }
345
+ email_summary.append(summary_email)
346
+
347
+ # Auto-analyze the emails for insights (no OpenAI)
348
+ analysis = simple_analyze_emails(full_emails)
349
+
350
+ # Return summary info with analysis
351
+ result = {
352
+ "sender_keyword": sender_keyword,
353
+ "date_range": f"{start_date} to {end_date}",
354
+ "email_summary": email_summary,
355
+ "analysis": analysis,
356
+ "email_count": len(full_emails),
357
+ "user_email": auth_info
358
+ }
359
+
360
+ return json.dumps(result, indent=2)
361
+
362
+ except Exception as e:
363
+ logger.error("Error in search_emails: %s", e)
364
+ error_result = {
365
+ "error": str(e),
366
+ "sender_keyword": sender_keyword,
367
+ "message": "Failed to search emails."
368
+ }
369
+ return json.dumps(error_result, indent=2)
370
+
371
+ def get_email_details(message_id: str) -> str:
372
+ """
373
+ Get full details of a specific email by its message ID using OAuth authentication.
374
+
375
+ Args:
376
+ message_id (str): The message ID of the email to retrieve
377
+
378
+ Returns:
379
+ str: JSON string containing the full email details
380
+ """
381
+ try:
382
+ logger.info("Getting email details for message_id: %s", message_id)
383
+
384
+ # Check authentication
385
+ is_auth, auth_info = check_authentication()
386
+ if not is_auth:
387
+ return json.dumps({
388
+ "error": "Not authenticated",
389
+ "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
390
+ "auth_status": auth_info
391
+ }, indent=2)
392
+
393
+ # Get email using Gmail API
394
+ email = gmail_scraper.get_email_by_id(message_id)
395
+
396
+ if email:
397
+ email["user_email"] = auth_info
398
+ return json.dumps(email, indent=2)
399
+ else:
400
+ error_result = {
401
+ "error": f"No email found with message_id '{message_id}'",
402
+ "message": "Email may not exist or you may not have access to it.",
403
+ "user_email": auth_info
404
+ }
405
+ return json.dumps(error_result, indent=2)
406
+
407
+ except Exception as e:
408
+ logger.error("Error in get_email_details: %s", e)
409
+ error_result = {
410
+ "error": str(e),
411
+ "message_id": message_id,
412
+ "message": "Failed to retrieve email details."
413
+ }
414
+ return json.dumps(error_result, indent=2)
415
+
416
+ def analyze_email_patterns(sender_keyword: str, days_back: str = "30") -> str:
417
+ """
418
+ Analyze email patterns from a specific sender over a given time period using OAuth authentication.
419
+
420
+ Args:
421
+ sender_keyword (str): The sender/company keyword to analyze (e.g., "amazon", "google")
422
+ days_back (str): Number of days to look back (default: "30")
423
+
424
+ Returns:
425
+ str: JSON string containing email pattern analysis
426
+ """
427
+ try:
428
+ logger.info("Analyzing email patterns for sender: %s, days_back: %s", sender_keyword, days_back)
429
+
430
+ # Check authentication
431
+ is_auth, auth_info = check_authentication()
432
+ if not is_auth:
433
+ return json.dumps({
434
+ "error": "Not authenticated",
435
+ "message": "Please authenticate first using the authenticate_user tool or run 'python setup_oauth.py'",
436
+ "auth_status": auth_info
437
+ }, indent=2)
438
+
439
+ # Calculate date range
440
+ days_int = int(days_back)
441
+ end_date = datetime.today()
442
+ start_date = end_date - timedelta(days=days_int)
443
+
444
+ start_date_str = start_date.strftime("%d-%b-%Y")
445
+ end_date_str = end_date.strftime("%d-%b-%Y")
446
+
447
+ # Search for emails using Gmail API
448
+ full_emails = gmail_scraper.search_emails(sender_keyword, start_date_str, end_date_str)
449
+
450
+ if not full_emails:
451
+ result = {
452
+ "sender_keyword": sender_keyword,
453
+ "date_range": f"{start_date_str} to {end_date_str}",
454
+ "analysis": {"summary": f"No emails found from '{sender_keyword}' in the last {days_back} days.", "insights": []},
455
+ "email_count": 0,
456
+ "user_email": auth_info
457
+ }
458
+ return json.dumps(result, indent=2)
459
+
460
+ # Analyze the emails (no OpenAI)
461
+ analysis = simple_analyze_emails(full_emails)
462
+
463
+ result = {
464
+ "sender_keyword": sender_keyword,
465
+ "date_range": f"{start_date_str} to {end_date_str}",
466
+ "analysis": analysis,
467
+ "email_count": len(full_emails),
468
+ "user_email": auth_info
469
+ }
470
+
471
+ return json.dumps(result, indent=2)
472
+
473
+ except Exception as e:
474
+ logger.error("Error in analyze_email_patterns: %s", e)
475
+ error_result = {
476
+ "error": str(e),
477
+ "sender_keyword": sender_keyword,
478
+ "message": "Failed to analyze email patterns."
479
+ }
480
+ return json.dumps(error_result, indent=2)
481
+
482
+ def get_authentication_status() -> str:
483
+ """
484
+ Get current authentication status and account information.
485
+
486
+ Returns:
487
+ str: JSON string containing authentication status
488
+ """
489
+ try:
490
+ current_account = oauth_manager.get_current_account()
491
+ is_auth = oauth_manager.is_authenticated() if current_account else False
492
+ all_accounts = oauth_manager.list_accounts()
493
+
494
+ result = {
495
+ "authenticated": is_auth,
496
+ "current_account": current_account,
497
+ "status": "authenticated" if is_auth else "not_authenticated",
498
+ "message": f"Current account: {current_account}" if is_auth else "No account selected or not authenticated",
499
+ "all_accounts": all_accounts,
500
+ "total_accounts": len(all_accounts),
501
+ "authenticated_accounts": [email for email, auth in all_accounts.items() if auth]
502
+ }
503
+
504
+ if not is_auth and not oauth_manager.client_secrets_file.exists():
505
+ result["setup_required"] = True
506
+ result["message"] = "OAuth not configured. Please run 'python setup_oauth.py' first."
507
+ elif not is_auth and current_account:
508
+ result["message"] = f"Account {current_account} needs re-authentication"
509
+ elif not current_account and all_accounts:
510
+ result["message"] = "Accounts available but none selected. Use switch_account to select one."
511
+
512
+ return json.dumps(result, indent=2)
513
+
514
+ except Exception as e:
515
+ logger.error("Error checking authentication status: %s", e)
516
+ return json.dumps({
517
+ "error": str(e),
518
+ "message": "Failed to check authentication status"
519
+ }, indent=2)
520
+
521
+ # Create Gradio interfaces
522
+ search_interface = gr.Interface(
523
+ fn=search_emails,
524
+ inputs=[
525
+ gr.Textbox(label="Sender Keyword", placeholder="apple, amazon, etc."),
526
+ gr.Textbox(label="Start Date (Optional)", placeholder="01-Jan-2025 (leave empty for last 7 days)"),
527
+ gr.Textbox(label="End Date (Optional)", placeholder="07-Jan-2025 (leave empty for today)")
528
+ ],
529
+ outputs=gr.Textbox(label="Search Results", lines=20),
530
+ title="Email Search (OAuth)",
531
+ description="Search your emails by sender keyword and date range with OAuth authentication"
532
+ )
533
+
534
+ details_interface = gr.Interface(
535
+ fn=get_email_details,
536
+ inputs=[
537
+ gr.Textbox(label="Message ID", placeholder="Email message ID from search results")
538
+ ],
539
+ outputs=gr.Textbox(label="Email Details", lines=20),
540
+ title="Email Details (OAuth)",
541
+ description="Get full details of a specific email by message ID with OAuth authentication"
542
+ )
543
+
544
+ analysis_interface = gr.Interface(
545
+ fn=analyze_email_patterns,
546
+ inputs=[
547
+ gr.Textbox(label="Sender Keyword", placeholder="amazon, google, linkedin, etc."),
548
+ gr.Textbox(label="Days Back", value="30", placeholder="Number of days to analyze")
549
+ ],
550
+ outputs=gr.Textbox(label="Analysis Results", lines=20),
551
+ title="Email Pattern Analysis (OAuth)",
552
+ description="Analyze email patterns from a specific sender over time with OAuth authentication"
553
+ )
554
+
555
+ auth_interface = gr.Interface(
556
+ fn=authenticate_user,
557
+ inputs=[],
558
+ outputs=gr.Textbox(label="Authentication Result", lines=10),
559
+ title="Authenticate with Gmail",
560
+ description="Click Submit to start OAuth authentication flow with Gmail"
561
+ )
562
+
563
+ status_interface = gr.Interface(
564
+ fn=get_authentication_status,
565
+ inputs=[],
566
+ outputs=gr.Textbox(label="Authentication Status", lines=15),
567
+ title="Authentication Status",
568
+ description="Check current authentication status and view all accounts"
569
+ )
570
+
571
+ switch_interface = gr.Interface(
572
+ fn=switch_account,
573
+ inputs=[
574
+ gr.Textbox(label="Target Email", placeholder="[email protected]")
575
+ ],
576
+ outputs=gr.Textbox(label="Switch Result", lines=10),
577
+ title="Switch Account",
578
+ description="Switch to a different authenticated Gmail account"
579
+ )
580
+
581
+ accounts_interface = gr.Interface(
582
+ fn=list_accounts,
583
+ inputs=[],
584
+ outputs=gr.Textbox(label="Accounts List", lines=15),
585
+ title="List All Accounts",
586
+ description="View all authenticated Gmail accounts and their status"
587
+ )
588
+
589
+ remove_interface = gr.Interface(
590
+ fn=remove_account,
591
+ inputs=[
592
+ gr.Textbox(label="Email to Remove", placeholder="[email protected]")
593
+ ],
594
+ outputs=gr.Textbox(label="Removal Result", lines=10),
595
+ title="Remove Account",
596
+ description="Remove an authenticated Gmail account and its credentials"
597
+ )
598
+
599
+ # Combine interfaces into a tabbed interface
600
+ demo = gr.TabbedInterface(
601
+ [auth_interface, status_interface, accounts_interface, switch_interface, remove_interface, search_interface, details_interface, analysis_interface],
602
+ ["πŸ” Authenticate", "πŸ“Š Status", "πŸ‘₯ All Accounts", "πŸ”„ Switch Account", "πŸ—‘οΈ Remove Account", "πŸ“§ Email Search", "πŸ“„ Email Details", "πŸ“ˆ Pattern Analysis"],
603
+ title="πŸ“§ Gmail Assistant MCP Server (Multi-Account OAuth)"
604
+ )
605
+
606
+ if __name__ == "__main__":
607
+ # Set environment variable to enable MCP server
608
+ import os
609
+ os.environ["GRADIO_MCP_SERVER"] = "True"
610
+
611
+ # Check authentication status on startup
612
+ current_account = oauth_manager.get_current_account()
613
+ all_accounts = oauth_manager.list_accounts()
614
+
615
+ if current_account and oauth_manager.is_authenticated():
616
+ print(f"βœ… Currently authenticated as: {current_account}")
617
+ if len(all_accounts) > 1:
618
+ print(f"πŸ“± {len(all_accounts)} total accounts available: {list(all_accounts.keys())}")
619
+ elif all_accounts:
620
+ print(f"πŸ“± {len(all_accounts)} stored accounts found: {list(all_accounts.keys())}")
621
+ print("⚠️ No current account selected. Use the web interface or Claude to switch accounts.")
622
+ else:
623
+ print("❌ No authenticated accounts. Users will need to authenticate through the web interface.")
624
+ print("πŸ’‘ Or run 'python setup_oauth.py' for initial setup.")
625
+
626
+ # Launch the server
627
+ demo.launch(share=False)
628
+
629
+ print("\nπŸš€ MCP Server is running!")
630
+ print("πŸ“ MCP Endpoint: http://localhost:7860/gradio_api/mcp/sse")
631
+ print("πŸ“– Copy this URL to your Claude Desktop MCP configuration")
632
+ print("\nπŸ”— Web Interface: http://localhost:7860")
633
+ print("πŸ“ Use the web interface to authenticate and test the tools")
agentic_implementation/gmail_api_scraper.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Gmail API-based Email Scraper with OAuth Authentication
4
+ """
5
+
6
+ import base64
7
+ import re
8
+ from datetime import datetime, timedelta
9
+ from typing import List, Dict, Optional
10
+ from email.mime.text import MIMEText
11
+ import googleapiclient.errors
12
+ from oauth_manager import oauth_manager
13
+ from logger import logger
14
+
15
+ class GmailAPIScraper:
16
+ """Gmail API-based email scraper using OAuth authentication"""
17
+
18
+ def __init__(self):
19
+ """Initialize the Gmail API scraper"""
20
+ self.oauth_manager = oauth_manager
21
+
22
+ def _parse_date_string(self, date_str: str) -> datetime:
23
+ """Parse date string in DD-MMM-YYYY format to datetime object"""
24
+ try:
25
+ return datetime.strptime(date_str, "%d-%b-%Y")
26
+ except ValueError:
27
+ raise ValueError(f"Invalid date format: {date_str}. Expected DD-MMM-YYYY")
28
+
29
+ def _format_date_for_query(self, date_obj: datetime) -> str:
30
+ """Format datetime object for Gmail API query"""
31
+ return date_obj.strftime("%Y/%m/%d")
32
+
33
+ def _decode_message_part(self, part: Dict) -> str:
34
+ """Decode message part content"""
35
+ data = part.get('body', {}).get('data', '')
36
+ if data:
37
+ # Decode base64url
38
+ data += '=' * (4 - len(data) % 4) # Add padding if needed
39
+ decoded_bytes = base64.urlsafe_b64decode(data)
40
+ try:
41
+ return decoded_bytes.decode('utf-8')
42
+ except UnicodeDecodeError:
43
+ return decoded_bytes.decode('utf-8', errors='ignore')
44
+ return ''
45
+
46
+ def _extract_email_content(self, message: Dict) -> str:
47
+ """Extract readable content from Gmail API message"""
48
+ content = ""
49
+
50
+ if 'payload' not in message:
51
+ return content
52
+
53
+ payload = message['payload']
54
+
55
+ # Handle multipart messages
56
+ if 'parts' in payload:
57
+ for part in payload['parts']:
58
+ mime_type = part.get('mimeType', '')
59
+
60
+ if mime_type == 'text/plain':
61
+ content += self._decode_message_part(part)
62
+ elif mime_type == 'text/html':
63
+ html_content = self._decode_message_part(part)
64
+ # Simple HTML tag removal
65
+ clean_text = re.sub(r'<[^>]+>', '', html_content)
66
+ content += clean_text
67
+ elif mime_type.startswith('multipart/'):
68
+ # Handle nested multipart
69
+ if 'parts' in part:
70
+ for nested_part in part['parts']:
71
+ nested_mime = nested_part.get('mimeType', '')
72
+ if nested_mime == 'text/plain':
73
+ content += self._decode_message_part(nested_part)
74
+ else:
75
+ # Handle single part messages
76
+ mime_type = payload.get('mimeType', '')
77
+ if mime_type in ['text/plain', 'text/html']:
78
+ raw_content = self._decode_message_part(payload)
79
+ if mime_type == 'text/html':
80
+ # Simple HTML tag removal
81
+ content = re.sub(r'<[^>]+>', '', raw_content)
82
+ else:
83
+ content = raw_content
84
+
85
+ return content.strip()
86
+
87
+ def _get_header_value(self, headers: List[Dict], name: str) -> str:
88
+ """Get header value by name"""
89
+ for header in headers:
90
+ if header.get('name', '').lower() == name.lower():
91
+ return header.get('value', '')
92
+ return ''
93
+
94
+ def _parse_email_message(self, message: Dict) -> Dict:
95
+ """Parse Gmail API message into structured format"""
96
+ headers = message.get('payload', {}).get('headers', [])
97
+
98
+ # Extract headers
99
+ subject = self._get_header_value(headers, 'Subject') or 'No Subject'
100
+ from_header = self._get_header_value(headers, 'From') or 'Unknown Sender'
101
+ date_header = self._get_header_value(headers, 'Date')
102
+ message_id = self._get_header_value(headers, 'Message-ID') or message.get('id', '')
103
+
104
+ # Parse date
105
+ email_date = datetime.now().strftime("%d-%b-%Y")
106
+ email_time = "00:00:00"
107
+
108
+ if date_header:
109
+ try:
110
+ # Parse RFC 2822 date format
111
+ from email.utils import parsedate_to_datetime
112
+ dt_obj = parsedate_to_datetime(date_header)
113
+ # Convert to IST (Indian Standard Time)
114
+ from zoneinfo import ZoneInfo
115
+ ist_dt = dt_obj.astimezone(ZoneInfo("Asia/Kolkata"))
116
+ email_date = ist_dt.strftime("%d-%b-%Y")
117
+ email_time = ist_dt.strftime("%H:%M:%S")
118
+ except Exception as e:
119
+ logger.warning(f"Failed to parse date {date_header}: {e}")
120
+
121
+ # Extract content
122
+ content = self._extract_email_content(message)
123
+
124
+ return {
125
+ "date": email_date,
126
+ "time": email_time,
127
+ "subject": subject,
128
+ "from": from_header,
129
+ "content": content[:2000], # Limit content length
130
+ "message_id": message_id,
131
+ "gmail_id": message.get('id', '')
132
+ }
133
+
134
+ def search_emails(self, keyword: str, start_date: str, end_date: str) -> List[Dict]:
135
+ """Search emails containing keyword within date range using Gmail API
136
+
137
+ Args:
138
+ keyword: Keyword to search for in emails
139
+ start_date: Start date in DD-MMM-YYYY format
140
+ end_date: End date in DD-MMM-YYYY format
141
+
142
+ Returns:
143
+ List of email dictionaries
144
+ """
145
+ logger.info(f"Searching emails containing '{keyword}' between {start_date} and {end_date}")
146
+
147
+ # Get Gmail service
148
+ service = self.oauth_manager.get_gmail_service()
149
+ if not service:
150
+ raise Exception("Not authenticated. Please authenticate first using the setup tool.")
151
+
152
+ try:
153
+ # Parse dates
154
+ start_dt = self._parse_date_string(start_date)
155
+ end_dt = self._parse_date_string(end_date)
156
+
157
+ # Format dates for Gmail API query
158
+ after_date = self._format_date_for_query(start_dt)
159
+ before_date = self._format_date_for_query(end_dt + timedelta(days=1)) # Add 1 day for inclusive end
160
+
161
+ # Build search query
162
+ # Gmail API search syntax: https://developers.google.com/gmail/api/guides/filtering
163
+ query_parts = [
164
+ f'after:{after_date}',
165
+ f'before:{before_date}',
166
+ f'({keyword})' # Search in all fields
167
+ ]
168
+ query = ' '.join(query_parts)
169
+
170
+ logger.info(f"Gmail API query: {query}")
171
+
172
+ # Search for messages
173
+ results = service.users().messages().list(
174
+ userId='me',
175
+ q=query,
176
+ maxResults=500 # Limit to 500 results
177
+ ).execute()
178
+
179
+ messages = results.get('messages', [])
180
+ logger.info(f"Found {len(messages)} messages")
181
+
182
+ if not messages:
183
+ return []
184
+
185
+ # Fetch full message details
186
+ scraped_emails = []
187
+
188
+ for i, msg_ref in enumerate(messages):
189
+ try:
190
+ logger.info(f"Processing email {i+1}/{len(messages)}")
191
+
192
+ # Get full message
193
+ message = service.users().messages().get(
194
+ userId='me',
195
+ id=msg_ref['id'],
196
+ format='full'
197
+ ).execute()
198
+
199
+ # Parse message
200
+ parsed_email = self._parse_email_message(message)
201
+
202
+ # Verify date range (double-check since Gmail search might be inclusive)
203
+ email_dt = self._parse_date_string(parsed_email['date'])
204
+ if start_dt <= email_dt <= end_dt:
205
+ # Verify keyword presence (case-insensitive)
206
+ keyword_lower = keyword.lower()
207
+ if any(keyword_lower in text.lower() for text in [
208
+ parsed_email['subject'],
209
+ parsed_email['from'],
210
+ parsed_email['content']
211
+ ]):
212
+ scraped_emails.append(parsed_email)
213
+
214
+ except googleapiclient.errors.HttpError as e:
215
+ logger.error(f"Error fetching message {msg_ref['id']}: {e}")
216
+ continue
217
+ except Exception as e:
218
+ logger.error(f"Error processing message {msg_ref['id']}: {e}")
219
+ continue
220
+
221
+ # Sort by date (newest first)
222
+ scraped_emails.sort(
223
+ key=lambda x: datetime.strptime(f"{x['date']} {x['time']}", "%d-%b-%Y %H:%M:%S"),
224
+ reverse=True
225
+ )
226
+
227
+ logger.info(f"Successfully processed {len(scraped_emails)} emails containing '{keyword}'")
228
+ return scraped_emails
229
+
230
+ except googleapiclient.errors.HttpError as e:
231
+ logger.error(f"Gmail API error: {e}")
232
+ raise Exception(f"Gmail API error: {e}")
233
+ except Exception as e:
234
+ logger.error(f"Email search failed: {e}")
235
+ raise
236
+
237
+ def get_email_by_id(self, message_id: str) -> Optional[Dict]:
238
+ """Get email details by message ID or Gmail ID
239
+
240
+ Args:
241
+ message_id: Either the Message-ID header or Gmail message ID
242
+
243
+ Returns:
244
+ Email dictionary or None if not found
245
+ """
246
+ service = self.oauth_manager.get_gmail_service()
247
+ if not service:
248
+ raise Exception("Not authenticated. Please authenticate first using the setup tool.")
249
+
250
+ try:
251
+ # Try to get message directly by Gmail ID first
252
+ try:
253
+ message = service.users().messages().get(
254
+ userId='me',
255
+ id=message_id,
256
+ format='full'
257
+ ).execute()
258
+ return self._parse_email_message(message)
259
+ except googleapiclient.errors.HttpError:
260
+ # If direct ID lookup fails, search by Message-ID header
261
+ pass
262
+
263
+ # Search by Message-ID header
264
+ query = f'rfc822msgid:{message_id}'
265
+ results = service.users().messages().list(
266
+ userId='me',
267
+ q=query,
268
+ maxResults=1
269
+ ).execute()
270
+
271
+ messages = results.get('messages', [])
272
+ if not messages:
273
+ return None
274
+
275
+ # Get the message
276
+ message = service.users().messages().get(
277
+ userId='me',
278
+ id=messages[0]['id'],
279
+ format='full'
280
+ ).execute()
281
+
282
+ return self._parse_email_message(message)
283
+
284
+ except Exception as e:
285
+ logger.error(f"Failed to get email {message_id}: {e}")
286
+ return None
287
+
288
+ def is_authenticated(self) -> bool:
289
+ """Check if user is authenticated"""
290
+ return self.oauth_manager.is_authenticated()
291
+
292
+ def get_user_email(self) -> Optional[str]:
293
+ """Get authenticated user's email address"""
294
+ return self.oauth_manager.get_user_email()
295
+
296
+ def authenticate(self) -> bool:
297
+ """Trigger interactive authentication"""
298
+ return self.oauth_manager.authenticate_interactive()
299
+
300
+ # Global scraper instance
301
+ gmail_scraper = GmailAPIScraper()
agentic_implementation/oauth_manager.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import pickle
4
+ import base64
5
+ from pathlib import Path
6
+ from typing import Optional, Dict, Any
7
+ from cryptography.fernet import Fernet
8
+ import google.auth.transport.requests
9
+ import google_auth_oauthlib.flow
10
+ import googleapiclient.discovery
11
+ from google.oauth2.credentials import Credentials
12
+ from google.auth.transport.requests import Request
13
+ import webbrowser
14
+ import threading
15
+ import time
16
+ from http.server import HTTPServer, BaseHTTPRequestHandler
17
+ import urllib.parse as urlparse
18
+ from logger import logger
19
+
20
+ class OAuthCallbackHandler(BaseHTTPRequestHandler):
21
+ """HTTP request handler for OAuth callback"""
22
+
23
+ def do_GET(self):
24
+ """Handle GET request (OAuth callback)"""
25
+ # Parse the callback URL to extract authorization code
26
+ parsed_path = urlparse.urlparse(self.path)
27
+ query_params = urlparse.parse_qs(parsed_path.query)
28
+
29
+ if 'code' in query_params:
30
+ # Store the authorization code
31
+ self.server.auth_code = query_params['code'][0]
32
+
33
+ # Send success response
34
+ self.send_response(200)
35
+ self.send_header('Content-type', 'text/html')
36
+ self.end_headers()
37
+
38
+ success_html = """
39
+ <html>
40
+ <head><title>Authentication Successful</title></head>
41
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
42
+ <h1 style="color: #4CAF50;">βœ… Authentication Successful!</h1>
43
+ <p>You have successfully authenticated with Gmail.</p>
44
+ <p>You can now close this window and return to Claude Desktop.</p>
45
+ <script>
46
+ setTimeout(function() {
47
+ window.close();
48
+ }, 3000);
49
+ </script>
50
+ </body>
51
+ </html>
52
+ """
53
+ self.wfile.write(success_html.encode())
54
+ else:
55
+ # Send error response
56
+ self.send_response(400)
57
+ self.send_header('Content-type', 'text/html')
58
+ self.end_headers()
59
+
60
+ error_html = """
61
+ <html>
62
+ <head><title>Authentication Error</title></head>
63
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
64
+ <h1 style="color: #f44336;">❌ Authentication Failed</h1>
65
+ <p>There was an error during authentication.</p>
66
+ <p>Please try again.</p>
67
+ </body>
68
+ </html>
69
+ """
70
+ self.wfile.write(error_html.encode())
71
+
72
+ def log_message(self, format, *args):
73
+ """Suppress server log messages"""
74
+ pass
75
+
76
+ class GmailOAuthManager:
77
+ """Manages Gmail OAuth 2.0 authentication and token storage for multiple accounts"""
78
+
79
+ # Gmail API scopes
80
+ SCOPES = [
81
+ 'https://www.googleapis.com/auth/gmail.readonly',
82
+ 'https://www.googleapis.com/auth/gmail.modify'
83
+ ]
84
+
85
+ def __init__(self, credentials_dir: str = None):
86
+ """Initialize OAuth manager
87
+
88
+ Args:
89
+ credentials_dir: Directory to store credentials (defaults to ~/.mailquery_oauth)
90
+ """
91
+ if credentials_dir is None:
92
+ credentials_dir = os.path.expanduser("~/.mailquery_oauth")
93
+
94
+ self.credentials_dir = Path(credentials_dir)
95
+ self.credentials_dir.mkdir(exist_ok=True)
96
+
97
+ # File paths
98
+ self.client_secrets_file = self.credentials_dir / "client_secret.json"
99
+ self.accounts_file = self.credentials_dir / "accounts.json"
100
+ self.encryption_key_file = self.credentials_dir / "key.key"
101
+ self.current_account_file = self.credentials_dir / "current_account.txt"
102
+
103
+ # Initialize encryption
104
+ self._init_encryption()
105
+
106
+ # OAuth flow settings
107
+ self.redirect_uri = "http://localhost:8080/oauth2callback"
108
+
109
+ # Current account
110
+ self.current_account_email = self._load_current_account()
111
+
112
+ def _init_encryption(self):
113
+ """Initialize encryption for secure credential storage"""
114
+ if self.encryption_key_file.exists():
115
+ with open(self.encryption_key_file, 'rb') as key_file:
116
+ self.encryption_key = key_file.read()
117
+ else:
118
+ self.encryption_key = Fernet.generate_key()
119
+ with open(self.encryption_key_file, 'wb') as key_file:
120
+ key_file.write(self.encryption_key)
121
+ # Make key file readable only by owner
122
+ os.chmod(self.encryption_key_file, 0o600)
123
+
124
+ self.cipher_suite = Fernet(self.encryption_key)
125
+
126
+ def _load_current_account(self) -> Optional[str]:
127
+ """Load the currently selected account"""
128
+ if self.current_account_file.exists():
129
+ try:
130
+ with open(self.current_account_file, 'r') as f:
131
+ return f.read().strip()
132
+ except Exception as e:
133
+ logger.error(f"Failed to load current account: {e}")
134
+ return None
135
+
136
+ def _save_current_account(self, email: str):
137
+ """Save the currently selected account"""
138
+ try:
139
+ with open(self.current_account_file, 'w') as f:
140
+ f.write(email)
141
+ self.current_account_email = email
142
+ logger.info(f"Set current account to: {email}")
143
+ except Exception as e:
144
+ logger.error(f"Failed to save current account: {e}")
145
+
146
+ def setup_client_secrets(self, client_id: str, client_secret: str):
147
+ """Setup OAuth client secrets
148
+
149
+ Args:
150
+ client_id: Google OAuth 2.0 client ID
151
+ client_secret: Google OAuth 2.0 client secret
152
+ """
153
+ client_config = {
154
+ "web": {
155
+ "client_id": client_id,
156
+ "client_secret": client_secret,
157
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
158
+ "token_uri": "https://oauth2.googleapis.com/token",
159
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
160
+ "redirect_uris": [self.redirect_uri]
161
+ }
162
+ }
163
+
164
+ with open(self.client_secrets_file, 'w') as f:
165
+ json.dump(client_config, f, indent=2)
166
+
167
+ logger.info("Client secrets saved successfully")
168
+
169
+ def _encrypt_data(self, data: Any) -> bytes:
170
+ """Encrypt data using Fernet encryption"""
171
+ serialized_data = pickle.dumps(data)
172
+ return self.cipher_suite.encrypt(serialized_data)
173
+
174
+ def _decrypt_data(self, encrypted_data: bytes) -> Any:
175
+ """Decrypt data using Fernet encryption"""
176
+ decrypted_data = self.cipher_suite.decrypt(encrypted_data)
177
+ return pickle.loads(decrypted_data)
178
+
179
+ def get_authorization_url(self) -> str:
180
+ """Get the authorization URL for OAuth flow
181
+
182
+ Returns:
183
+ Authorization URL that user should visit
184
+ """
185
+ if not self.client_secrets_file.exists():
186
+ raise ValueError("Client secrets not found. Please run setup first.")
187
+
188
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
189
+ str(self.client_secrets_file),
190
+ scopes=self.SCOPES
191
+ )
192
+ flow.redirect_uri = self.redirect_uri
193
+
194
+ auth_url, _ = flow.authorization_url(
195
+ access_type='offline',
196
+ include_granted_scopes='true',
197
+ prompt='consent' # Force consent to get refresh token
198
+ )
199
+
200
+ return auth_url
201
+
202
+ def authenticate_interactive(self) -> bool:
203
+ """Interactive authentication flow that opens browser
204
+
205
+ Returns:
206
+ True if authentication successful, False otherwise
207
+ """
208
+ try:
209
+ # Start local HTTP server for OAuth callback
210
+ server = HTTPServer(('localhost', 8080), OAuthCallbackHandler)
211
+ server.auth_code = None
212
+
213
+ # Get authorization URL
214
+ auth_url = self.get_authorization_url()
215
+
216
+ logger.info("Opening browser for authentication...")
217
+ logger.info(f"If browser doesn't open, visit: {auth_url}")
218
+
219
+ # Open browser
220
+ webbrowser.open(auth_url)
221
+
222
+ # Start server in background thread
223
+ server_thread = threading.Thread(target=server.handle_request)
224
+ server_thread.daemon = True
225
+ server_thread.start()
226
+
227
+ # Wait for callback (max 5 minutes)
228
+ timeout = 300 # 5 minutes
229
+ start_time = time.time()
230
+
231
+ while server.auth_code is None and (time.time() - start_time) < timeout:
232
+ time.sleep(1)
233
+
234
+ if server.auth_code is None:
235
+ logger.error("Authentication timed out")
236
+ return False
237
+
238
+ # Exchange authorization code for credentials
239
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
240
+ str(self.client_secrets_file),
241
+ scopes=self.SCOPES
242
+ )
243
+ flow.redirect_uri = self.redirect_uri
244
+
245
+ flow.fetch_token(code=server.auth_code)
246
+ credentials = flow.credentials
247
+
248
+ # Get user email from credentials
249
+ user_email = self._get_email_from_credentials(credentials)
250
+ if not user_email:
251
+ logger.error("Failed to get user email from credentials")
252
+ return False
253
+
254
+ # Save encrypted credentials for this account
255
+ self._save_credentials(user_email, credentials)
256
+
257
+ # Set as current account
258
+ self._save_current_account(user_email)
259
+
260
+ logger.info("Authentication successful!")
261
+ return True
262
+
263
+ except Exception as e:
264
+ logger.error(f"Authentication failed: {e}")
265
+ return False
266
+
267
+ def _get_email_from_credentials(self, credentials: Credentials) -> Optional[str]:
268
+ """Get email address from credentials"""
269
+ try:
270
+ service = googleapiclient.discovery.build(
271
+ 'gmail', 'v1', credentials=credentials
272
+ )
273
+ profile = service.users().getProfile(userId='me').execute()
274
+ return profile.get('emailAddress')
275
+ except Exception as e:
276
+ logger.error(f"Failed to get email from credentials: {e}")
277
+ return None
278
+
279
+ def _save_credentials(self, email: str, credentials: Credentials):
280
+ """Save encrypted credentials for a specific account"""
281
+ try:
282
+ # Load existing accounts
283
+ accounts = self._load_accounts()
284
+
285
+ # Encrypt and store credentials
286
+ encrypted_credentials = self._encrypt_data(credentials)
287
+ accounts[email] = base64.b64encode(encrypted_credentials).decode('utf-8')
288
+
289
+ # Save accounts file
290
+ with open(self.accounts_file, 'w') as f:
291
+ json.dump(accounts, f, indent=2)
292
+
293
+ # Make accounts file readable only by owner
294
+ os.chmod(self.accounts_file, 0o600)
295
+
296
+ logger.info(f"Credentials saved for account: {email}")
297
+ except Exception as e:
298
+ logger.error(f"Failed to save credentials for {email}: {e}")
299
+ raise
300
+
301
+ def _load_accounts(self) -> Dict[str, str]:
302
+ """Load accounts data"""
303
+ if not self.accounts_file.exists():
304
+ return {}
305
+
306
+ try:
307
+ with open(self.accounts_file, 'r') as f:
308
+ return json.load(f)
309
+ except Exception as e:
310
+ logger.error(f"Failed to load accounts: {e}")
311
+ return {}
312
+
313
+ def _load_credentials(self, email: str) -> Optional[Credentials]:
314
+ """Load and decrypt credentials for a specific account"""
315
+ accounts = self._load_accounts()
316
+
317
+ if email not in accounts:
318
+ return None
319
+
320
+ try:
321
+ encrypted_credentials = base64.b64decode(accounts[email])
322
+ credentials = self._decrypt_data(encrypted_credentials)
323
+ return credentials
324
+ except Exception as e:
325
+ logger.error(f"Failed to load credentials for {email}: {e}")
326
+ return None
327
+
328
+ def get_valid_credentials(self, email: str = None) -> Optional[Credentials]:
329
+ """Get valid credentials for an account, refreshing if necessary
330
+
331
+ Args:
332
+ email: Email address of account (uses current account if None)
333
+
334
+ Returns:
335
+ Valid Credentials object or None if authentication required
336
+ """
337
+ if email is None:
338
+ email = self.current_account_email
339
+
340
+ if not email:
341
+ logger.warning("No current account set")
342
+ return None
343
+
344
+ credentials = self._load_credentials(email)
345
+
346
+ if not credentials:
347
+ logger.warning(f"No stored credentials found for {email}")
348
+ return None
349
+
350
+ # Refresh if expired
351
+ if credentials.expired and credentials.refresh_token:
352
+ try:
353
+ logger.info(f"Refreshing expired credentials for {email}...")
354
+ credentials.refresh(Request())
355
+ self._save_credentials(email, credentials)
356
+ logger.info("Credentials refreshed successfully")
357
+ except Exception as e:
358
+ logger.error(f"Failed to refresh credentials for {email}: {e}")
359
+ return None
360
+
361
+ if not credentials.valid:
362
+ logger.warning(f"Credentials are not valid for {email}")
363
+ return None
364
+
365
+ return credentials
366
+
367
+ def is_authenticated(self, email: str = None) -> bool:
368
+ """Check if user is authenticated
369
+
370
+ Args:
371
+ email: Email address to check (uses current account if None)
372
+
373
+ Returns:
374
+ True if valid credentials exist, False otherwise
375
+ """
376
+ return self.get_valid_credentials(email) is not None
377
+
378
+ def switch_account(self, email: str) -> bool:
379
+ """Switch to a different authenticated account
380
+
381
+ Args:
382
+ email: Email address to switch to
383
+
384
+ Returns:
385
+ True if switch successful, False if account not found or not authenticated
386
+ """
387
+ if self.is_authenticated(email):
388
+ self._save_current_account(email)
389
+ logger.info(f"Switched to account: {email}")
390
+ return True
391
+ else:
392
+ logger.error(f"Account {email} is not authenticated")
393
+ return False
394
+
395
+ def list_accounts(self) -> Dict[str, bool]:
396
+ """List all stored accounts and their authentication status
397
+
398
+ Returns:
399
+ Dictionary mapping email addresses to authentication status
400
+ """
401
+ accounts = self._load_accounts()
402
+ result = {}
403
+
404
+ for email in accounts.keys():
405
+ result[email] = self.is_authenticated(email)
406
+
407
+ return result
408
+
409
+ def remove_account(self, email: str):
410
+ """Remove an account and its credentials
411
+
412
+ Args:
413
+ email: Email address to remove
414
+ """
415
+ accounts = self._load_accounts()
416
+
417
+ if email in accounts:
418
+ del accounts[email]
419
+
420
+ # Save updated accounts
421
+ with open(self.accounts_file, 'w') as f:
422
+ json.dump(accounts, f, indent=2)
423
+
424
+ # If this was the current account, clear it
425
+ if self.current_account_email == email:
426
+ if self.current_account_file.exists():
427
+ self.current_account_file.unlink()
428
+ self.current_account_email = None
429
+
430
+ logger.info(f"Removed account: {email}")
431
+ else:
432
+ logger.warning(f"Account {email} not found")
433
+
434
+ def clear_credentials(self):
435
+ """Clear all stored credentials"""
436
+ if self.accounts_file.exists():
437
+ self.accounts_file.unlink()
438
+ if self.current_account_file.exists():
439
+ self.current_account_file.unlink()
440
+ self.current_account_email = None
441
+ logger.info("All credentials cleared")
442
+
443
+ def get_gmail_service(self, email: str = None):
444
+ """Get authenticated Gmail service object
445
+
446
+ Args:
447
+ email: Email address (uses current account if None)
448
+
449
+ Returns:
450
+ Gmail service object or None if not authenticated
451
+ """
452
+ credentials = self.get_valid_credentials(email)
453
+ if not credentials:
454
+ return None
455
+
456
+ try:
457
+ service = googleapiclient.discovery.build(
458
+ 'gmail', 'v1', credentials=credentials
459
+ )
460
+ return service
461
+ except Exception as e:
462
+ logger.error(f"Failed to build Gmail service: {e}")
463
+ return None
464
+
465
+ def get_user_email(self, email: str = None) -> Optional[str]:
466
+ """Get the authenticated user's email address
467
+
468
+ Args:
469
+ email: Email address (uses current account if None)
470
+
471
+ Returns:
472
+ User's email address or None if not authenticated
473
+ """
474
+ if email is None:
475
+ return self.current_account_email
476
+ return email if self.is_authenticated(email) else None
477
+
478
+ def get_current_account(self) -> Optional[str]:
479
+ """Get the currently selected account
480
+
481
+ Returns:
482
+ Current account email or None if no account selected
483
+ """
484
+ return self.current_account_email
485
+
486
+ # Global OAuth manager instance
487
+ oauth_manager = GmailOAuthManager()
agentic_implementation/requirements_oauth.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core OAuth Gmail MCP Server Dependencies
2
+ gradio
3
+ google-auth
4
+ google-auth-oauthlib
5
+ google-auth-httplib2
6
+ google-api-python-client
7
+ cryptography
8
+ requests
9
+ loguru
10
+ python-dateutil
11
+
12
+ # MCP server support
13
+ mcp
14
+
15
+ # Email processing
16
+ email-validator
17
+ beautifulsoup4
18
+ html2text
19
+
20
+ # Development (optional)
21
+ pytest
22
+ black
agentic_implementation/setup_oauth.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ OAuth Setup Utility for Gmail MCP Server
4
+
5
+ This script helps users set up OAuth authentication for the Gmail MCP server.
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ import json
11
+ from pathlib import Path
12
+ from oauth_manager import oauth_manager
13
+ from logger import logger
14
+
15
+ def print_banner():
16
+ """Print setup banner"""
17
+ print("=" * 60)
18
+ print("πŸ“§ Gmail MCP Server - OAuth Setup")
19
+ print("=" * 60)
20
+ print()
21
+
22
+ def print_step(step_num: int, title: str):
23
+ """Print step header"""
24
+ print(f"\nπŸ”Ή Step {step_num}: {title}")
25
+ print("-" * 50)
26
+
27
+ def check_dependencies():
28
+ """Check if required dependencies are installed"""
29
+ try:
30
+ import google.auth
31
+ import google_auth_oauthlib
32
+ import googleapiclient
33
+ import cryptography
34
+ print("βœ… All required dependencies are installed")
35
+ return True
36
+ except ImportError as e:
37
+ print(f"❌ Missing dependency: {e}")
38
+ print("\nPlease install the required dependencies:")
39
+ print("pip install google-auth google-auth-oauthlib google-api-python-client cryptography")
40
+ return False
41
+
42
+ def setup_google_cloud_project():
43
+ """Guide user through Google Cloud project setup"""
44
+ print_step(1, "Google Cloud Project Setup")
45
+
46
+ print("You need to create a Google Cloud project and enable the Gmail API.")
47
+ print("\nπŸ“‹ Follow these steps:")
48
+ print("1. Go to: https://console.cloud.google.com/")
49
+ print("2. Create a new project or select an existing one")
50
+ print("3. Enable the Gmail API:")
51
+ print(" - Go to 'APIs & Services' > 'Library'")
52
+ print(" - Search for 'Gmail API'")
53
+ print(" - Click 'Enable'")
54
+
55
+ input("\nβœ… Press Enter when you've completed these steps...")
56
+
57
+ def setup_oauth_consent():
58
+ """Guide user through OAuth consent screen setup"""
59
+ print_step(2, "OAuth Consent Screen Setup")
60
+
61
+ print("Now you need to configure the OAuth consent screen.")
62
+ print("\nπŸ“‹ Follow these steps:")
63
+ print("1. Go to: https://console.cloud.google.com/apis/credentials/consent")
64
+ print("2. Choose 'External' user type (unless using Google Workspace)")
65
+ print("3. Fill in the app information:")
66
+ print(" - App name: 'Gmail MCP Server' (or your preferred name)")
67
+ print(" - User support email: Your email address")
68
+ print(" - Developer contact: Your email address")
69
+ print("4. Add these scopes:")
70
+ print(" - https://www.googleapis.com/auth/gmail.readonly")
71
+ print(" - https://www.googleapis.com/auth/gmail.modify")
72
+ print("5. Add your email as a test user")
73
+ print("6. Complete the setup")
74
+
75
+ input("\nβœ… Press Enter when you've completed these steps...")
76
+
77
+ def setup_oauth_credentials():
78
+ """Guide user through OAuth credentials setup"""
79
+ print_step(3, "OAuth Client Credentials Setup")
80
+
81
+ print("Now you need to create OAuth 2.0 client credentials.")
82
+ print("\nπŸ“‹ Follow these steps:")
83
+ print("1. Go to: https://console.cloud.google.com/apis/credentials")
84
+ print("2. Click 'Create Credentials' > 'OAuth client ID'")
85
+ print("3. Choose 'Web application' as the application type")
86
+ print("4. Set the name to 'Gmail MCP Server'")
87
+ print("5. Add this redirect URI:")
88
+ print(" http://localhost:8080/oauth2callback")
89
+ print("6. Click 'Create'")
90
+ print("7. Copy the Client ID and Client Secret")
91
+
92
+ print("\nπŸ”‘ Enter your OAuth credentials:")
93
+
94
+ while True:
95
+ client_id = input("Client ID: ").strip()
96
+ if client_id:
97
+ break
98
+ print("❌ Client ID cannot be empty")
99
+
100
+ while True:
101
+ client_secret = input("Client Secret: ").strip()
102
+ if client_secret:
103
+ break
104
+ print("❌ Client Secret cannot be empty")
105
+
106
+ try:
107
+ oauth_manager.setup_client_secrets(client_id, client_secret)
108
+ print("βœ… OAuth credentials saved successfully")
109
+ return True
110
+ except Exception as e:
111
+ print(f"❌ Failed to save credentials: {e}")
112
+ return False
113
+
114
+ def test_authentication():
115
+ """Test the OAuth authentication flow"""
116
+ print_step(4, "Authentication Test")
117
+
118
+ print("Now let's test the authentication flow.")
119
+ print("This will open your web browser for authentication.")
120
+
121
+ confirm = input("\n🌐 Ready to open browser for authentication? (y/n): ").strip().lower()
122
+ if confirm != 'y':
123
+ print("Authentication test skipped.")
124
+ return False
125
+
126
+ try:
127
+ print("\nπŸ”„ Starting authentication flow...")
128
+ success = oauth_manager.authenticate_interactive()
129
+
130
+ if success:
131
+ print("βœ… Authentication successful!")
132
+
133
+ # Test getting user info
134
+ user_email = oauth_manager.get_user_email()
135
+ if user_email:
136
+ print(f"βœ… Authenticated as: {user_email}")
137
+
138
+ return True
139
+ else:
140
+ print("❌ Authentication failed")
141
+ return False
142
+
143
+ except Exception as e:
144
+ print(f"❌ Authentication error: {e}")
145
+ return False
146
+
147
+ def show_completion_info():
148
+ """Show completion information and next steps"""
149
+ print("\n" + "=" * 60)
150
+ print("πŸŽ‰ Setup Complete!")
151
+ print("=" * 60)
152
+
153
+ print("\nβœ… Your Gmail MCP server is now configured with OAuth authentication!")
154
+ print("\nπŸ“ Next steps:")
155
+ print("1. Start the MCP server:")
156
+ print(" python email_mcp_server_oauth.py")
157
+ print("\n2. Configure Claude Desktop:")
158
+ print(' Add this to your MCP configuration:')
159
+ print(' {')
160
+ print(' "mcpServers": {')
161
+ print(' "gmail-oauth": {')
162
+ print(' "command": "npx",')
163
+ print(' "args": ["mcp-remote", "http://localhost:7860/gradio_api/mcp/sse"]')
164
+ print(' }')
165
+ print(' }')
166
+ print(' }')
167
+
168
+ print("\nπŸ” Security notes:")
169
+ print("- Your credentials are encrypted and stored locally")
170
+ print("- Tokens are automatically refreshed when needed")
171
+ print("- You can revoke access anytime from Google Account settings")
172
+
173
+ credentials_dir = oauth_manager.credentials_dir
174
+ print(f"\nπŸ“ Credentials stored in: {credentials_dir}")
175
+
176
+ def show_help():
177
+ """Show help information"""
178
+ print("Gmail MCP Server OAuth Setup")
179
+ print("\nUsage:")
180
+ print(" python setup_oauth.py # Full interactive setup")
181
+ print(" python setup_oauth.py --help # Show this help")
182
+ print(" python setup_oauth.py --auth # Re-authenticate only")
183
+ print(" python setup_oauth.py --status # Check authentication status")
184
+ print(" python setup_oauth.py --clear # Clear stored credentials")
185
+
186
+ def check_status():
187
+ """Check authentication status"""
188
+ print("πŸ” Checking authentication status...")
189
+
190
+ if oauth_manager.is_authenticated():
191
+ user_email = oauth_manager.get_user_email()
192
+ print(f"βœ… Authenticated as: {user_email}")
193
+ return True
194
+ else:
195
+ print("❌ Not authenticated")
196
+ return False
197
+
198
+ def clear_credentials():
199
+ """Clear stored credentials"""
200
+ confirm = input("⚠️ This will clear all stored credentials. Continue? (y/n): ").strip().lower()
201
+ if confirm == 'y':
202
+ oauth_manager.clear_credentials()
203
+ print("βœ… Credentials cleared")
204
+ else:
205
+ print("Operation cancelled")
206
+
207
+ def main():
208
+ """Main setup function"""
209
+ if len(sys.argv) > 1:
210
+ arg = sys.argv[1].lower()
211
+
212
+ if arg in ['--help', '-h', 'help']:
213
+ show_help()
214
+ return
215
+ elif arg == '--status':
216
+ check_status()
217
+ return
218
+ elif arg == '--auth':
219
+ print("πŸ”„ Starting re-authentication...")
220
+ if test_authentication():
221
+ print("βœ… Re-authentication successful")
222
+ else:
223
+ print("❌ Re-authentication failed")
224
+ return
225
+ elif arg == '--clear':
226
+ clear_credentials()
227
+ return
228
+ else:
229
+ print(f"Unknown argument: {arg}")
230
+ show_help()
231
+ return
232
+
233
+ # Full interactive setup
234
+ print_banner()
235
+
236
+ # Check if already authenticated
237
+ if oauth_manager.is_authenticated():
238
+ user_email = oauth_manager.get_user_email()
239
+ print(f"βœ… Already authenticated as: {user_email}")
240
+
241
+ choice = input("\nπŸ”„ Do you want to re-authenticate? (y/n): ").strip().lower()
242
+ if choice == 'y':
243
+ if test_authentication():
244
+ show_completion_info()
245
+ else:
246
+ print("Setup complete - you're already authenticated!")
247
+ return
248
+
249
+ # Check dependencies
250
+ if not check_dependencies():
251
+ return
252
+
253
+ # Full setup flow
254
+ try:
255
+ setup_google_cloud_project()
256
+ setup_oauth_consent()
257
+
258
+ if not setup_oauth_credentials():
259
+ print("❌ Setup failed at credentials step")
260
+ return
261
+
262
+ if test_authentication():
263
+ show_completion_info()
264
+ else:
265
+ print("❌ Setup completed but authentication test failed")
266
+ print("You can try authentication later with: python setup_oauth.py --auth")
267
+
268
+ except KeyboardInterrupt:
269
+ print("\n\n⚠️ Setup interrupted by user")
270
+ except Exception as e:
271
+ print(f"\n❌ Setup failed: {e}")
272
+
273
+ if __name__ == "__main__":
274
+ main()