import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { google } from 'googleapis'; import { IntegrationStrategy, IntegrationResult, EmailAttachment, EmailAttachmentValidator, } from '../integration.strategy'; @Injectable() export class GmailApiStrategy implements IntegrationStrategy { constructor(private readonly configService: ConfigService) {} async sendMessage( to: string | string[], message: string, config: any, ): Promise { try { if (!config || !config.refreshToken) { throw new Error( 'Invalid Gmail API configuration - missing refresh token', ); } const { subject, cc, bcc, attachments } = config; console.log('---- Using Google SDK Gmail API ----'); const oauth2Client = await this.setupOAuth2Client(config); const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); const toRecipients = Array.isArray(to) ? to.join(', ') : to; const ccRecipients = cc ? Array.isArray(cc) ? cc.join(', ') : cc : undefined; const bccRecipients = bcc ? Array.isArray(bcc) ? bcc.join(', ') : bcc : undefined; const fromEmail = config.email; const fromName = config.fromName; const emailContent = attachments && attachments.length > 0 ? this.buildMultipartEmail( toRecipients, ccRecipients, bccRecipients, subject, config.richText || message, attachments, fromEmail, fromName, ) : this.buildSimpleEmail( toRecipients, ccRecipients, bccRecipients, subject, config.richText || message, fromEmail, fromName, ); const encodedMessage = Buffer.from(emailContent) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); const result = await gmail.users.messages.send({ userId: 'me', requestBody: { raw: encodedMessage, }, }); const credentials = oauth2Client.credentials; const freshAccessToken = credentials.access_token; return { success: true, messageId: result.data.id || undefined, provider: 'gmail', service: 'API', timestamp: new Date(), message: 'Email sent successfully', }; } catch (error) { console.error('---- Gmail SDK API Error ----'); console.error('Error message:', error.message); console.error('Error details:', error.response?.data || error.errors); return { success: false, provider: 'gmail', service: 'API', error: error.response?.data?.error?.message || error.message || 'Gmail API error', timestamp: new Date(), }; } } validateConfig(config: any): boolean { if (!config) return false; // For Google SDK strategy, we need a refresh token return config.refreshToken && typeof config.refreshToken === 'string'; } private async setupOAuth2Client(config: any): Promise { try { const { refreshToken } = config; if (!refreshToken) { throw new Error('Missing refresh token for OAuth2 setup'); } // Get system OAuth credentials const clientId = this.configService.get('CLIENT_ID'); const clientSecret = this.configService.get('CLIENT_SECRET'); const callbackurl = this.configService.get('CALLBACK_URL'); if (!clientId || !clientSecret) { throw new Error('Gmail OAuth system credentials not configured'); } console.log('---- Setting up OAuth2 client with Google SDK ----'); // Create OAuth2 client using Google SDK const oauth2Client = new google.auth.OAuth2( clientId, clientSecret, callbackurl, // For server-to-server ); // Set the refresh token oauth2Client.setCredentials({ refresh_token: refreshToken, }); // Refresh access token automatically await oauth2Client.getAccessToken(); console.log('---- OAuth2 client setup completed ----'); return oauth2Client; } catch (error) { console.error('Failed to setup OAuth2 client:', error.message); throw new Error(`Failed to setup Gmail OAuth2 client: ${error.message}`); } } async refreshAccessToken(config: any): Promise { try { const oauth2Client = await this.setupOAuth2Client(config); const { token } = await oauth2Client.getAccessToken(); if (!token) { throw new Error('No access token received from OAuth2 client'); } console.log('---- Token refreshed successfully via Google SDK ----'); return token; } catch (error) { console.error( 'Failed to refresh access token via Google SDK:', error.message, ); throw new Error(`Failed to refresh Gmail access token: ${error.message}`); } } async validateConnection(config: any): Promise { try { if (!this.validateConfig(config)) { return false; } // Setup OAuth2 client and test the connection const oauth2Client = await this.setupOAuth2Client(config); const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); // Test the connection by getting user profile const profile = await gmail.users.getProfile({ userId: 'me', }); return !!(profile.data && profile.data.emailAddress); } catch (error) { console.error('Gmail connection validation failed:', error.message); return false; } } private buildSimpleEmail( to: string, cc?: string, bcc?: string, subject?: string, message?: string, fromEmail?: string, fromName?: string, ): string { const fromHeader = fromEmail ? `From: "${fromName || fromEmail}" <${fromEmail}>` : undefined; const emailLines = [ ...(fromHeader ? [fromHeader] : []), `To: ${to}`, `Subject: ${subject || 'Notification'}`, ]; if (cc) { emailLines.push(`Cc: ${cc}`); } if (bcc) { emailLines.push(`Bcc: ${bcc}`); } emailLines.push('Content-Type: text/html; charset=utf-8'); emailLines.push(''); emailLines.push(message || ''); return emailLines.join('\n'); } private buildMultipartEmail( to: string, cc?: string, bcc?: string, subject?: string, message?: string, attachments?: EmailAttachment[], fromEmail?: string, fromName?: string, ): string { const boundary = `----GmailBoundary${Date.now()}${Math.random().toString(36).substr(2, 9)}`; const fromHeader = fromEmail ? `From: "${fromName || fromEmail}" <${fromEmail}>` : undefined; const emailLines = [ ...(fromHeader ? [fromHeader] : []), `To: ${to}`, `Subject: ${subject || 'Notification'}`, ]; if (cc) { emailLines.push(`Cc: ${cc}`); } if (bcc) { emailLines.push(`Bcc: ${bcc}`); } emailLines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`); emailLines.push(''); // Add main content part emailLines.push(`--${boundary}`); emailLines.push('Content-Type: text/html; charset=utf-8'); emailLines.push(''); emailLines.push(message || ''); // Add attachment parts if (attachments) { for (const attachment of attachments) { emailLines.push(`--${boundary}`); emailLines.push(`Content-Type: ${attachment.contentType}`); emailLines.push( `Content-Disposition: ${attachment.disposition || 'attachment'}; filename="${attachment.filename}"`, ); emailLines.push('Content-Transfer-Encoding: base64'); if (attachment.contentId) { emailLines.push(`Content-ID: <${attachment.contentId}>`); } emailLines.push(''); // Add base64 content in chunks of 76 characters (RFC 2045) const base64Content = attachment.content; const chunks = base64Content.match(/.{1,76}/g) || []; emailLines.push(...chunks); } } emailLines.push(`--${boundary}--`); return emailLines.join('\n'); } }