UNPKG

gmail-mcp-server

Version:

Gmail MCP Server with on-demand authentication for SIYA/Claude Desktop. Complete Gmail integration with multi-user support and OAuth2 security.

213 lines (212 loc) • 7.39 kB
import nodemailer from 'nodemailer'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { logger } from './api.js'; // Configuration const CONFIG_DIR = path.join(os.homedir(), '.gmail-mcp'); const APP_PASSWORD_FILE = path.join(CONFIG_DIR, 'app-password.json'); /** * Gmail authentication using App Passwords with SMTP * This replaces OAuth2 Gmail API with traditional SMTP protocol for sending emails * Note: This implementation only supports sending emails. * Reading emails would require additional IMAP implementation with separate dependencies. */ export class AppPasswordAuth { constructor() { this.credentials = null; this.smtpTransporter = null; this.ensureConfigDir(); } /** * Ensure configuration directory exists */ ensureConfigDir() { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); logger.log('Created Gmail MCP configuration directory'); } } /** * Load app password credentials from file or environment */ async loadCredentials() { try { // Try environment variables first if (process.env.GMAIL_EMAIL && process.env.GMAIL_APP_PASSWORD) { this.credentials = { email: process.env.GMAIL_EMAIL, appPassword: process.env.GMAIL_APP_PASSWORD }; logger.log('Loaded Gmail app password from environment variables'); return true; } // Try credentials file if (fs.existsSync(APP_PASSWORD_FILE)) { const credentialsData = fs.readFileSync(APP_PASSWORD_FILE, 'utf8'); this.credentials = JSON.parse(credentialsData); logger.log('Loaded Gmail app password from file'); return true; } return false; } catch (error) { logger.error('Error loading Gmail app password:', error); return false; } } /** * Save credentials to file */ async saveCredentials(credentials) { try { fs.writeFileSync(APP_PASSWORD_FILE, JSON.stringify(credentials, null, 2)); logger.log('Saved Gmail app password to file'); } catch (error) { logger.error('Error saving Gmail app password:', error); throw new Error('Failed to save app password credentials'); } } /** * Initialize SMTP transporter for sending emails */ initializeSMTP() { if (!this.credentials) { throw new Error('App password credentials not loaded'); } this.smtpTransporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.credentials.email, pass: this.credentials.appPassword } }); return this.smtpTransporter; } /** * Test authentication by attempting to verify SMTP connection */ async testAuthentication() { try { if (!this.credentials) { return false; } // Test SMTP const smtpTransporter = this.initializeSMTP(); await smtpTransporter.verify(); logger.log('SMTP authentication successful'); return true; } catch (error) { logger.error('Authentication test failed:', error); return false; } } /** * Get SMTP transporter for sending emails */ getSMTPTransporter() { if (!this.smtpTransporter) { this.smtpTransporter = this.initializeSMTP(); } return this.smtpTransporter; } /** * Check if credentials are configured */ async isConfigured() { await this.loadCredentials(); return this.credentials !== null; } /** * Check if authenticated (test connection) */ async isAuthenticated() { if (!this.credentials) { await this.loadCredentials(); } if (!this.credentials) { return false; } return await this.testAuthentication(); } /** * Setup credentials interactively */ async setupCredentials() { const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { console.log('\nšŸ” Gmail App Password Setup'); console.log('====================================='); console.log('You need to generate an App Password from your Google Account:'); console.log('1. Go to https://myaccount.google.com/security'); console.log('2. Enable 2-Step Verification (if not already enabled)'); console.log('3. Go to "App passwords" section'); console.log('4. Generate a new app password for "Mail"'); console.log('5. Use the 16-character password below\n'); const email = await new Promise((resolve) => { rl.question('Enter your Gmail email address: ', resolve); }); const appPassword = await new Promise((resolve) => { rl.question('Enter your 16-character app password (spaces will be removed): ', resolve); }); // Clean up app password (remove spaces) const cleanAppPassword = appPassword.replace(/\s/g, ''); if (cleanAppPassword.length !== 16) { throw new Error('App password must be exactly 16 characters'); } const credentials = { email: email.trim(), appPassword: cleanAppPassword }; // Test the credentials console.log('\nšŸ”„ Testing credentials...'); this.credentials = credentials; if (await this.testAuthentication()) { await this.saveCredentials(credentials); console.log('āœ… App password setup successful!'); } else { throw new Error('App password authentication failed. Please check your credentials.'); } } catch (error) { console.error('āŒ Setup failed:', error instanceof Error ? error.message : 'Unknown error'); throw error; } finally { rl.close(); } } /** * Reset authentication (remove stored credentials) */ resetAuth() { try { if (fs.existsSync(APP_PASSWORD_FILE)) { fs.unlinkSync(APP_PASSWORD_FILE); logger.log('App password credentials removed'); } this.credentials = null; this.smtpTransporter = null; console.log('āœ… App password authentication cleared'); } catch (error) { logger.error('Error resetting app password auth:', error); throw new Error('Failed to reset app password authentication'); } } /** * Get user email address */ getUserEmail() { return this.credentials?.email || null; } } // Export singleton instance export const appPasswordAuth = new AppPasswordAuth();