import { google, gmail_v1 } from 'googleapis';
import { OAuth2Client } from 'google-auth-library';
import * as Handlebars from 'handlebars';
export interface GmailConfig {
client: OAuth2Client;
senderEmail: string;
senderName: string;
}
export interface EmailRecipient {
name?: string;
email: string;
}
export interface EmailTemplate {
name: string;
subject: string;
html: string;
text?: string;
}
export interface SendEmailRequest {
to: EmailRecipient[];
subject: string;
template: string;
templateVars: Record<string, any>;
}
export interface SendEmailResponse {
messageId: string;
threadId: string;
}
export class GmailAdapter {
private gmail: gmail_v1.Gmail;
private templates: Map<string, EmailTemplate> = new Map();
constructor(config: GmailConfig) {
this.gmail = google.gmail({ version: 'v1', auth: config.client });
this.senderEmail = config.senderEmail;
this.senderName = config.senderName;
}
private senderEmail: string;
private senderName: string;
registerTemplate(template: EmailTemplate): void {
this.templates.set(template.name, template);
}
async sendEmail(request: SendEmailRequest): Promise<SendEmailResponse> {
try {
const template = this.templates.get(request.template);
if (!template) {
throw new Error(`Template '${request.template}' not found`);
}
const subjectTemplate = Handlebars.compile(template.subject);
const htmlTemplate = Handlebars.compile(template.html);
const textTemplate = template.text ? Handlebars.compile(template.text) : null;
const renderedSubject = subjectTemplate(request.templateVars);
const renderedHtml = htmlTemplate(request.templateVars);
const renderedText = textTemplate ? textTemplate(request.templateVars) : this.htmlToText(renderedHtml);
const mimeMessage = this.createMimeMessage({
to: request.to,
subject: renderedSubject,
html: renderedHtml,
text: renderedText
});
const response = await this.gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: mimeMessage
}
});
return {
messageId: response.data.id!,
threadId: response.data.threadId!
};
} catch (error) {
throw this.normalizeError(error);
}
}
private createMimeMessage(options: {
to: EmailRecipient[];
subject: string;
html: string;
text: string;
}): string {
const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const headers = [
`From: ${this.senderName} <${this.senderEmail}>`,
`To: ${options.to.map(r => r.name ? `${r.name} <${r.email}>` : r.email).join(', ')}`,
`Subject: ${options.subject}`,
`MIME-Version: 1.0`,
`Content-Type: multipart/alternative; boundary="${boundary}"`
];
const textPart = [
`--${boundary}`,
`Content-Type: text/plain; charset=utf-8`,
`Content-Transfer-Encoding: 7bit`,
``,
options.text
].join('\r\n');
const htmlPart = [
`--${boundary}`,
`Content-Type: text/html; charset=utf-8`,
`Content-Transfer-Encoding: 7bit`,
``,
options.html
].join('\r\n');
const closingBoundary = `--${boundary}--`;
const message = [
headers.join('\r\n'),
'',
textPart,
htmlPart,
closingBoundary
].join('\r\n');
return Buffer.from(message).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
private htmlToText(html: string): string {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<p\s*\/?>/gi, '\n\n')
.replace(/<div\s*\/?>/gi, '\n')
.replace(/<h[1-6]\s*\/?>/gi, '\n')
.replace(/<[^>]*>/g, '')
.replace(/\s+/g, ' ')
.trim();
}
async sendSimpleEmail(
to: EmailRecipient[],
subject: string,
text: string,
html?: string
): Promise<SendEmailResponse> {
try {
const mimeMessage = this.createMimeMessage({
to,
subject,
html: html || text,
text
});
const response = await this.gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: mimeMessage
}
});
return {
messageId: response.data.id!,
threadId: response.data.threadId!
};
} catch (error) {
throw this.normalizeError(error);
}
}
getTemplates(): string[] {
return Array.from(this.templates.keys());
}
hasTemplate(templateName: string): boolean {
return this.templates.has(templateName);
}
private normalizeError(error: any): Error {
const status = error?.status || error?.code;
const message = error?.message || 'Unknown Gmail API error';
if (status === 401) {
return new Error('UNAUTHENTICATED: Invalid or expired credentials');
}
if (status === 403) {
if (error?.message?.includes('rateLimitExceeded')) {
return new Error('RATE_LIMITED: Gmail API rate limit exceeded');
}
if (error?.message?.includes('quotaExceeded')) {
return new Error('RATE_LIMITED: Gmail API quota exceeded');
}
return new Error('FORBIDDEN: Insufficient permissions for Gmail access');
}
if (status === 404) {
return new Error('NOT_FOUND: Gmail resource not found');
}
if (status === 429) {
return new Error('RATE_LIMITED: Too many requests to Gmail API');
}
if (status === 400) {
if (error?.message?.includes('invalidArgument')) {
return new Error('INVALID_ARGUMENT: Invalid email parameters');
}
return new Error(`INVALID_ARGUMENT: Gmail API error: ${message}`);
}
return new Error(`INTERNAL: Gmail API error: ${message}`);
}
}