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
JavaScript
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();