gmail-reader
Version:
A Gmail reader package for Google Workspace accounts with domain-wide delegation support
300 lines • 11.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GmailClient = void 0;
// src/gmailClient.ts
const googleapis_1 = require("googleapis");
const google_auth_library_1 = require("google-auth-library");
const gmail = googleapis_1.google.gmail('v1');
/**
* Gmail client for reading emails from a workspace account
* Supports domain-wide delegation for admin access
*/
class GmailClient {
/**
* Creates a new GmailClient instance
*
* @param config - Client configuration including clientId and clientSecret
* @param refreshToken - OAuth2 refresh token (for user-level access)
* @param options - Additional options
*/
constructor(config, refreshToken, options = {}) {
this.config = config;
this.options = options;
this.scopes = [
'https://www.googleapis.com/auth/gmail.readonly'
];
this.refreshToken = refreshToken;
this.impersonateEmail = options.impersonateEmail;
if (options.scopes) {
this.scopes = options.scopes;
}
// Initialize auth client based on authentication method
if (this.impersonateEmail && options.keyFile) {
// Domain-wide delegation with service account
this.auth = new googleapis_1.google.auth.JWT({
email: this.config.clientId,
key: options.keyFile,
scopes: this.scopes,
subject: this.impersonateEmail
});
}
else {
// Standard OAuth2 authentication
this.auth = new googleapis_1.google.auth.OAuth2(this.config.clientId, this.config.clientSecret, this.config.redirectUri);
if (this.refreshToken) {
this.setRefreshToken(this.refreshToken);
}
}
}
/**
* Set the refresh token for OAuth2 authentication
*
* @param refreshToken - OAuth2 refresh token
*/
setRefreshToken(refreshToken) {
this.refreshToken = refreshToken;
if (this.auth instanceof google_auth_library_1.OAuth2Client) {
this.auth.setCredentials({
refresh_token: refreshToken
});
}
}
/**
* Set domain-wide delegation for impersonating a user
*
* @param email - Email address to impersonate
* @param keyFile - Service account key file content
*/
setDomainWideDelegation(email, keyFile) {
this.impersonateEmail = email;
this.auth = new googleapis_1.google.auth.JWT({
email: this.config.clientId,
key: keyFile,
scopes: this.scopes,
subject: email
});
}
/**
* Authenticate with Google API
*
* @returns Promise resolving when authentication is complete
*/
async authenticate() {
if (this.auth instanceof google_auth_library_1.OAuth2Client && this.refreshToken) {
this.auth.setCredentials({
refresh_token: this.refreshToken,
});
}
else if (this.auth instanceof google_auth_library_1.JWT) {
await this.auth.authorize();
}
else {
throw new Error('Authentication failed: No valid authentication method provided');
}
}
/**
* List emails matching the given query options
*
* @param options - Query options for filtering emails
* @returns Promise resolving to a list of emails
*/
async listEmails(options = {}) {
try {
await this.authenticate();
const userId = this.impersonateEmail || 'me';
const res = await gmail.users.messages.list({
userId,
auth: this.auth,
maxResults: options.maxResults || 10,
labelIds: options.labelIds,
includeSpamTrash: options.includeSpamTrash,
q: options.q,
pageToken: options.pageToken
});
const messages = res.data.messages || [];
const nextPageToken = res.data.nextPageToken || undefined;
const resultSizeEstimate = res.data.resultSizeEstimate !== null && res.data.resultSizeEstimate !== undefined ? res.data.resultSizeEstimate : undefined;
if (messages.length === 0) {
return { emails: [], nextPageToken, resultSizeEstimate };
}
const emails = await Promise.all(messages.map(message => this.getEmail(message.id)));
return {
emails: emails.filter(Boolean),
nextPageToken,
resultSizeEstimate
};
}
catch (error) {
console.error('Error listing emails:', error);
throw new Error(`Failed to list emails: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Get a specific email by ID
*
* @param emailId - The ID of the email to retrieve
* @param options - Options for retrieving the email
* @returns Promise resolving to the email details
*/
async getEmail(emailId, options = {}) {
try {
await this.authenticate();
const userId = this.impersonateEmail || 'me';
const messageData = await gmail.users.messages.get({
userId,
id: emailId,
auth: this.auth,
format: options.format || 'full',
metadataHeaders: options.metadataHeaders
});
if (!messageData.data) {
throw new Error(`Email with ID ${emailId} not found`);
}
const { data } = messageData;
const headers = data.payload?.headers || [];
const subject = headers.find(header => header.name === 'Subject')?.value || '';
const from = headers.find(header => header.name === 'From')?.value || '';
const to = headers.find(header => header.name === 'To')?.value || '';
const date = headers.find(header => header.name === 'Date')?.value || '';
const snippet = data.snippet || '';
const threadId = data.threadId || '';
const labels = data.labelIds || [];
// Extract email body
const body = this.extractEmailBody(data.payload);
// Extract attachments
const attachments = this.extractAttachments(data.payload);
return {
id: emailId,
threadId,
subject,
from,
to,
date,
snippet,
body,
attachments,
labels
};
}
catch (error) {
console.error(`Error getting email ${emailId}:`, error);
throw new Error(`Failed to get email: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Search for emails matching a query
*
* @param query - Search query string in Gmail query format
* @param options - Additional query options
* @returns Promise resolving to a list of emails
*/
async searchEmails(query, options = {}) {
return this.listEmails({ ...options, q: query });
}
/**
* Get emails from a specific sender
*
* @param fromEmail - Email address of the sender
* @param options - Additional query options
* @returns Promise resolving to a list of emails
*/
async getEmailsFrom(fromEmail, options = {}) {
return this.searchEmails(`from:${fromEmail}`, options);
}
/**
* Extract email body from message payload
*
* @param payload - Gmail message payload
* @returns Object containing plain text and HTML body
*/
extractEmailBody(payload) {
if (!payload)
return {};
const body = {};
// Check if the current part has a body with data
if (payload.mimeType === 'text/plain' && payload.body?.data) {
body.plain = Buffer.from(payload.body.data, 'base64').toString('utf-8');
}
else if (payload.mimeType === 'text/html' && payload.body?.data) {
body.html = Buffer.from(payload.body.data, 'base64').toString('utf-8');
}
// If this part has child parts, process them recursively
if (payload.parts && payload.parts.length > 0) {
for (const part of payload.parts) {
if (part.mimeType === 'text/plain' && part.body?.data) {
body.plain = Buffer.from(part.body.data, 'base64').toString('utf-8');
}
else if (part.mimeType === 'text/html' && part.body?.data) {
body.html = Buffer.from(part.body.data, 'base64').toString('utf-8');
}
else if (part.parts) {
// Recursively check nested parts
const nestedBody = this.extractEmailBody(part);
if (nestedBody.plain && !body.plain)
body.plain = nestedBody.plain;
if (nestedBody.html && !body.html)
body.html = nestedBody.html;
}
}
}
return body;
}
/**
* Extract attachments from message payload
*
* @param payload - Gmail message payload
* @returns Array of email attachments
*/
extractAttachments(payload) {
const attachments = [];
if (!payload)
return attachments;
// Function to process a message part
const processPart = (part) => {
// Check if this part is an attachment
if (part.filename && part.filename.length > 0 && part.body?.attachmentId) {
attachments.push({
attachmentId: part.body.attachmentId,
filename: part.filename,
mimeType: part.mimeType || 'application/octet-stream',
size: part.body.size !== null && part.body.size !== undefined ? part.body.size : 0
});
}
// Process child parts recursively
if (part.parts && part.parts.length > 0) {
part.parts.forEach(processPart);
}
};
processPart(payload);
return attachments;
}
/**
* Get an attachment by ID
*
* @param messageId - ID of the email containing the attachment
* @param attachmentId - ID of the attachment to retrieve
* @returns Promise resolving to attachment data as a Buffer
*/
async getAttachment(messageId, attachmentId) {
try {
await this.authenticate();
const userId = this.impersonateEmail || 'me';
const response = await gmail.users.messages.attachments.get({
userId,
messageId,
id: attachmentId,
auth: this.auth
});
if (!response.data.data) {
throw new Error('Attachment data not found');
}
return Buffer.from(response.data.data, 'base64');
}
catch (error) {
console.error(`Error getting attachment ${attachmentId}:`, error);
throw new Error(`Failed to get attachment: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
exports.GmailClient = GmailClient;
//# sourceMappingURL=gmailClient.js.map