mcp-gmailcal 0.10.0

A MCP server for google mail, calendar, and contacts.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
/// Gmail Message Tests Module
///
/// This module contains comprehensive tests for the email message parsing functionality,
/// focusing on parsing various message formats and handling edge cases.
///
use mcp_gmailcal::gmail_api::{EmailMessage, GmailService};
use mcp_gmailcal::config::Config;
use mcp_gmailcal::errors::GmailApiError;
use serde_json::{json, Value};
use base64::{encode, decode};
use std::time::{SystemTime, UNIX_EPOCH};
use mockito;
use std::env;

// Helper to create test email JSON
fn create_email_json(
    id: &str,
    thread_id: &str,
    subject: &str,
    from: &str,
    to: &str,
    body_text: &str,
    body_html: Option<&str>,
) -> Value {
    // Create headers
    let headers = vec![
        json!({"name": "Subject", "value": subject}),
        json!({"name": "From", "value": from}),
        json!({"name": "To", "value": to}),
        json!({"name": "Date", "value": format!("Tue, 01 Apr 2025 12:34:56 +0000")}),
    ];
    
    let mut parts = Vec::new();
    let mut payload = json!({});
    
    // Add text and html parts if this is a multipart message
    if let Some(html) = body_html {
        // Multipart message with text and HTML parts
        parts.push(json!({
            "mimeType": "text/plain",
            "body": {
                "data": encode_base64_url_safe(body_text),
                "size": body_text.len()
            }
        }));
        
        parts.push(json!({
            "mimeType": "text/html",
            "body": {
                "data": encode_base64_url_safe(html),
                "size": html.len()
            }
        }));
        
        payload = json!({
            "mimeType": "multipart/alternative",
            "headers": headers,
            "parts": parts
        });
    } else {
        // Simple text-only message
        payload = json!({
            "mimeType": "text/plain",
            "headers": headers,
            "body": {
                "data": encode_base64_url_safe(body_text),
                "size": body_text.len()
            }
        });
    }
    
    // Create complete message
    json!({
        "id": id,
        "threadId": thread_id,
        "snippet": body_text.chars().take(50).collect::<String>(),
        "payload": payload
    })
}

// Helper to encode a string as base64url format (URL-safe base64)
fn encode_base64_url_safe(input: &str) -> String {
    encode(input)
        .replace('+', "-")
        .replace('/', "_")
}

// Helper to create a mock config
fn create_mock_config() -> Config {
    Config {
        client_id: "test_client_id".to_string(),
        client_secret: "test_client_secret".to_string(),
        refresh_token: "test_refresh_token".to_string(),
        access_token: Some("test_access_token".to_string()),
        token_refresh_threshold: 300, // 5 minutes
        token_expiry_buffer: 60,      // 1 minute
    }
}

#[ignore]
#[tokio::test]
async fn test_parse_simple_message() {
    // This test has been disabled due to runtime conflicts
    // between tokio and mockito.
    
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Create a simple test message JSON
    let message_id = "12345";
    let message_json = create_email_json(
        message_id,
        "thread123",
        "Test Subject",
        "sender@example.com",
        "recipient@example.com",
        "This is a test email body",
        None, // No HTML part
    );
    
    // Mock the Gmail API message endpoint
    let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(message_json.to_string())
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    env::set_var("OAUTH_TOKEN_URL", format!("{}/token", server.url()));
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Get message details
    let result = gmail_service.get_message_details(message_id).await;
    
    // Verify the mock was called
    mock.assert();
    
    // Check the result
    assert!(result.is_ok());
    let message = result.unwrap();
    
    // Verify fields
    assert_eq!(message.id, "12345");
    assert_eq!(message.thread_id, "thread123");
    assert_eq!(message.subject.unwrap(), "Test Subject");
    assert_eq!(message.from.unwrap(), "sender@example.com");
    assert_eq!(message.to.unwrap(), "recipient@example.com");
    assert!(message.body_text.is_some());
    assert_eq!(message.body_text.unwrap(), "This is a test email body");
    assert!(message.body_html.is_none()); // No HTML body in this test
}

#[ignore]
#[tokio::test]
async fn test_parse_multipart_message() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Create a multipart test message JSON
    let message_id = "67890";
    let message_json = create_email_json(
        message_id,
        "thread456",
        "Multipart Test",
        "sender@example.com",
        "recipient@example.com",
        "This is the plain text version",
        Some("<html><body>This is the HTML version</body></html>"), // HTML part
    );
    
    // Mock the Gmail API message endpoint
    let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(message_json.to_string())
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Get message details
    let result = gmail_service.get_message_details(message_id).await;
    
    // Verify the mock was called
    mock.assert();
    
    // Check the result
    assert!(result.is_ok());
    let message = result.unwrap();
    
    // Verify fields
    assert_eq!(message.id, "67890");
    assert_eq!(message.thread_id, "thread456");
    assert_eq!(message.subject.unwrap(), "Multipart Test");
    assert_eq!(message.from.unwrap(), "sender@example.com");
    assert_eq!(message.to.unwrap(), "recipient@example.com");
    
    // Both text and HTML parts should be present
    assert!(message.body_text.is_some());
    assert!(message.body_html.is_some());
    assert_eq!(message.body_text.unwrap(), "This is the plain text version");
    assert_eq!(message.body_html.unwrap(), "<html><body>This is the HTML version</body></html>");
}

#[ignore]
#[tokio::test]
async fn test_parse_malformed_message() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Create a malformed test message JSON (missing fields, etc.)
    let message_id = "malformed";
    let malformed_json = json!({
        "id": "malformed",
        // Missing threadId
        "snippet": "This is a malformed email",
        "payload": {
            // Missing headers
            "body": {
                "data": encode_base64_url_safe("Malformed email body"),
                "size": 19
            },
            "mimeType": "text/plain"
        }
    });
    
    // Mock the Gmail API message endpoint
    let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(malformed_json.to_string())
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Get message details - this should result in an error due to missing threadId
    let result = gmail_service.get_message_details(message_id).await;
    
    // Verify the mock was called
    mock.assert();
    
    // The result should be an error for missing threadId
    assert!(result.is_err());
    match result {
        Err(GmailApiError::MessageFormatError(err)) => {
            assert!(err.contains("threadId"), "Error should mention missing threadId field");
        }
        _ => panic!("Expected MessageFormatError"),
    }
}

#[ignore]
#[tokio::test]
async fn test_list_messages() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Create a mock message list response
    let messages_json = json!({
        "messages": [
            { "id": "msg1", "threadId": "thread1" },
            { "id": "msg2", "threadId": "thread2" },
            { "id": "msg3", "threadId": "thread3" }
        ],
        "nextPageToken": "abc123",
        "resultSizeEstimate": 3
    });
    
    // Mock the list messages endpoint
    let list_mock = server.mock("GET", "/gmail/v1/users/me/messages?maxResults=10")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(messages_json.to_string())
        .create();
    
    // Mock message details endpoints for each message
    let msg1_json = create_email_json(
        "msg1", "thread1", "First Email", "sender1@example.com", 
        "recipient@example.com", "First email body", None
    );
    
    let msg1_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(msg1_json.to_string())
        .create();
    
    let msg2_json = create_email_json(
        "msg2", "thread2", "Second Email", "sender2@example.com", 
        "recipient@example.com", "Second email body", 
        Some("<html><body>HTML content</body></html>")
    );
    
    let msg2_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg2?format=full")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(msg2_json.to_string())
        .create();
    
    let msg3_json = create_email_json(
        "msg3", "thread3", "Third Email", "sender3@example.com", 
        "recipient@example.com", "Third email body", None
    );
    
    let msg3_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg3?format=full")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(msg3_json.to_string())
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // List messages
    let result = gmail_service.list_messages(10, None).await;
    
    // Verify the mocks were called
    list_mock.assert();
    msg1_mock.assert();
    msg2_mock.assert();
    msg3_mock.assert();
    
    // Check the result
    assert!(result.is_ok());
    let messages = result.unwrap();
    
    // Verify we got 3 messages
    assert_eq!(messages.len(), 3);
    
    // Verify each message has the correct data
    assert_eq!(messages[0].id, "msg1");
    assert_eq!(messages[0].subject.as_ref().unwrap(), "First Email");
    
    assert_eq!(messages[1].id, "msg2");
    assert_eq!(messages[1].subject.as_ref().unwrap(), "Second Email");
    assert!(messages[1].body_html.is_some()); // This message has HTML
    
    assert_eq!(messages[2].id, "msg3");
    assert_eq!(messages[2].subject.as_ref().unwrap(), "Third Email");
}

#[ignore]
#[tokio::test]
async fn test_list_messages_with_query() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Create a mock message list response
    let messages_json = json!({
        "messages": [
            { "id": "msg1", "threadId": "thread1" }
        ],
        "resultSizeEstimate": 1
    });
    
    // Mock the list messages endpoint with query
    let list_mock = server.mock("GET", "/gmail/v1/users/me/messages?maxResults=10&q=important")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(messages_json.to_string())
        .create();
    
    // Mock message details endpoint
    let msg1_json = create_email_json(
        "msg1", "thread1", "Important Email", "sender1@example.com", 
        "recipient@example.com", "This is an important email", None
    );
    
    let msg1_mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(msg1_json.to_string())
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // List messages with query
    let result = gmail_service.list_messages(10, Some("important")).await;
    
    // Verify the mocks were called
    list_mock.assert();
    msg1_mock.assert();
    
    // Check the result
    assert!(result.is_ok());
    let messages = result.unwrap();
    
    // Verify we got 1 message
    assert_eq!(messages.len(), 1);
    
    // Verify the message has the correct data
    assert_eq!(messages[0].id, "msg1");
    assert_eq!(messages[0].subject.as_ref().unwrap(), "Important Email");
    assert!(messages[0].body_text.as_ref().unwrap().contains("important"));
}

#[ignore]
#[tokio::test]
async fn test_error_handling_not_found() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Mock a 404 Not Found response
    let mock = server.mock("GET", "/gmail/v1/users/me/messages/nonexistent?format=full")
        .with_status(404)
        .with_header("content-type", "application/json")
        .with_body(r#"{"error":{"code":404,"message":"Not Found","status":"NOT_FOUND"}}"#)
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Try to get a nonexistent message
    let result = gmail_service.get_message_details("nonexistent").await;
    
    // Verify the mock was called
    mock.assert();
    
    // The result should be an error
    assert!(result.is_err());
    match result {
        Err(GmailApiError::MessageRetrievalError(err)) => {
            assert!(err.contains("Not Found"), "Error should contain 'Not Found'");
        }
        _ => panic!("Expected MessageRetrievalError"),
    }
}

#[ignore]
#[tokio::test]
async fn test_error_handling_unauthorized() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Mock a 401 Unauthorized response
    let mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
        .with_status(401)
        .with_header("content-type", "application/json")
        .with_body(r#"{"error":{"code":401,"message":"Unauthorized","status":"UNAUTHENTICATED"}}"#)
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Try to get a message with an invalid token
    let result = gmail_service.get_message_details("msg1").await;
    
    // Verify the mock was called
    mock.assert();
    
    // The result should be an auth error
    assert!(result.is_err());
    match result {
        Err(GmailApiError::AuthError(err)) => {
            assert!(err.contains("Authentication failed"), "Error should indicate auth failure");
        }
        _ => panic!("Expected AuthError"),
    }
}

#[ignore]
#[tokio::test]
async fn test_error_handling_rate_limit() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Mock a 429 Too Many Requests response
    let mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
        .with_status(429)
        .with_header("content-type", "application/json")
        .with_body(r#"{"error":{"code":429,"message":"Rate Limit Exceeded","status":"RESOURCE_EXHAUSTED"}}"#)
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Try to get a message when rate limited
    let result = gmail_service.get_message_details("msg1").await;
    
    // Verify the mock was called
    mock.assert();
    
    // The result should be a rate limit error
    assert!(result.is_err());
    match result {
        Err(GmailApiError::RateLimitError(err)) => {
            assert!(err.contains("Rate limit exceeded"), "Error should indicate rate limiting");
        }
        _ => panic!("Expected RateLimitError"),
    }
}

#[ignore]
#[tokio::test]
async fn test_error_handling_server_error() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Mock a 500 Internal Server Error response
    let mock = server.mock("GET", "/gmail/v1/users/me/messages/msg1?format=full")
        .with_status(500)
        .with_header("content-type", "application/json")
        .with_body(r#"{"error":{"code":500,"message":"Internal Server Error","status":"INTERNAL"}}"#)
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Try to get a message when the server has an error
    let result = gmail_service.get_message_details("msg1").await;
    
    // Verify the mock was called
    mock.assert();
    
    // The result should be an API error
    assert!(result.is_err());
    match result {
        Err(GmailApiError::ApiError(err)) => {
            assert!(err.contains("500"), "Error should contain status code");
            assert!(err.contains("Internal Server Error"), "Error should contain error message");
        }
        _ => panic!("Expected ApiError"),
    }
}

#[ignore]
#[tokio::test]
async fn test_error_handling_network_error() {
    // Create a mock server but don't start it
    let server = mockito::Server::new();
    
    // Use an invalid/unreachable URL
    let invalid_url = format!("{}/invalid_endpoint", server.url());
    env::set_var("GMAIL_API_BASE_URL", invalid_url);
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Try to get a message with an invalid URL
    let result = gmail_service.get_message_details("msg1").await;
    
    // The result should be a network error
    assert!(result.is_err());
    match result {
        Err(GmailApiError::NetworkError(err)) => {
            // The exact error message will depend on the environment
            assert!(err.contains("error") || err.contains("failed") || err.contains("connection"), 
                   "Error should indicate network problem");
        }
        _ => panic!("Expected NetworkError, got: {:?}", result),
    }
}

#[ignore]
#[tokio::test]
async fn test_special_characters_in_messages() {
    // Create a mock server
    let mut server = mockito::Server::new();
    
    // Create a message with special characters
    let message_id = "special";
    let message_json = create_email_json(
        message_id,
        "thread_special",
        "Special Characters: äöü 😊",
        "sender@example.com",
        "recipient@example.com",
        "Email with emoji 🌍 and special chars: äöüß",
        Some("<html><body>HTML with emoji 🎉 and special chars: äöüß</body></html>"),
    );
    
    // Mock the Gmail API message endpoint
    let mock = server.mock("GET", format!("/gmail/v1/users/me/messages/{}?format=full", message_id).as_str())
        .with_status(200)
        .with_header("content-type", "application/json")
        .with_body(message_json.to_string())
        .create();
    
    // Override Gmail API URL
    env::set_var("GMAIL_API_BASE_URL", server.url());
    
    // Create GmailService with mock config
    let mut gmail_service = GmailService::new(&create_mock_config()).unwrap();
    
    // Get message details
    let result = gmail_service.get_message_details(message_id).await;
    
    // Verify the mock was called
    mock.assert();
    
    // Check the result
    assert!(result.is_ok());
    let message = result.unwrap();
    
    // Verify special characters were preserved
    assert_eq!(message.id, "special");
    assert!(message.subject.as_ref().unwrap().contains("äöü"));
    assert!(message.subject.as_ref().unwrap().contains("😊"));
    assert!(message.body_text.as_ref().unwrap().contains("🌍"));
    assert!(message.body_text.as_ref().unwrap().contains("äöüß"));
    assert!(message.body_html.as_ref().unwrap().contains("🎉"));
}