import {google, gmail_v1} from 'googleapis';
import {OAuth2Client} from 'google-auth-library';
import {logger} from '../logger.js';
export interface GmailAuthContext {
googleAccessToken: string;
email: string;
allowedLabels: string[];
}
interface ListMessagesOptions {
maxResults?: number;
pageToken?: string;
labels?: string[];
}
interface SearchMessagesOptions {
query: string;
maxResults?: number;
pageToken?: string;
labels?: string[];
}
export class GmailClient {
private gmail: gmail_v1.Gmail;
private resolvedLabelIds: string[] = [];
private configuredLabelNames: string[] = [];
private labelNameToId: Map<string, string> = new Map();
private constructor(auth: OAuth2Client, allowedLabels: string[] = []) {
this.gmail = google.gmail({version: 'v1', auth});
this.configuredLabelNames = allowedLabels;
}
static create(context: GmailAuthContext): GmailClient {
const auth = new google.auth.OAuth2();
auth.setCredentials({access_token: context.googleAccessToken});
return new GmailClient(auth, context.allowedLabels);
}
async listLabels(): Promise<gmail_v1.Schema$Label[]> {
const response = await this.gmail.users.labels.list({userId: 'me'});
return response.data.labels || [];
}
async initializeLabelFilter(): Promise<void> {
const allLabels = await this.listLabels();
for (const label of allLabels) {
if (label.id && label.name) {
this.labelNameToId.set(label.name.toLowerCase(), label.id);
}
}
if (this.configuredLabelNames.length === 0) {
this.resolvedLabelIds = [];
return;
}
this.resolvedLabelIds = this.configuredLabelNames.map((name) => {
const resolved = this.labelNameToId.get(name.toLowerCase());
if (resolved) {
return resolved;
}
return name;
});
}
getResolvedLabelIds(): string[] | undefined {
if (this.resolvedLabelIds.length === 0) {
return undefined;
}
return this.resolvedLabelIds;
}
hasAllowedLabel(labelIds: string[]): boolean {
if (this.resolvedLabelIds.length === 0) {
return true;
}
return labelIds.some((id) => this.resolvedLabelIds.includes(id));
}
isLabelAllowed(label: string): boolean {
if (this.configuredLabelNames.length === 0) {
return true;
}
const lowerLabel = label.toLowerCase();
return this.configuredLabelNames.some((name) => name.toLowerCase() === lowerLabel);
}
validateLabels(labels: string[]): void {
const invalidLabels = labels.filter((label) => !this.isLabelAllowed(label));
if (invalidLabels.length > 0) {
throw new Error(`Labels not allowed: ${invalidLabels.join(', ')}`);
}
}
resolveLabelsToIds(labels: string[]): string[] {
return labels.map((name) => {
const resolved = this.labelNameToId.get(name.toLowerCase());
return resolved || name;
});
}
rejectLabelsInQuery(query: string): void {
if (/-?label:/i.test(query)) {
throw new Error('Use labels parameter instead of label: in query');
}
}
async listMessages(options: ListMessagesOptions = {}): Promise<gmail_v1.Schema$Message[]> {
let labelIds: string[] | undefined;
if (options.labels && options.labels.length > 0) {
this.validateLabels(options.labels);
labelIds = this.resolveLabelsToIds(options.labels);
} else {
labelIds = this.getResolvedLabelIds();
}
logger.debug({
maxResults: options.maxResults || 10,
labelIds: labelIds,
}, 'listMessages request');
const response = await this.gmail.users.messages.list({
userId: 'me',
maxResults: options.maxResults || 10,
pageToken: options.pageToken,
labelIds: labelIds,
});
logger.debug({
resultSizeEstimate: response.data.resultSizeEstimate,
messagesCount: response.data.messages?.length || 0,
}, 'listMessages response');
const messages = response.data.messages || [];
const detailedMessages: gmail_v1.Schema$Message[] = [];
for (const message of messages) {
if (message.id) {
const detail = await this.getMessage(message.id);
detailedMessages.push(detail);
}
}
return detailedMessages;
}
async getMessage(messageId: string): Promise<gmail_v1.Schema$Message> {
const response = await this.gmail.users.messages.get({
userId: 'me',
id: messageId,
format: 'full',
});
return response.data;
}
async searchMessages(options: SearchMessagesOptions): Promise<gmail_v1.Schema$Message[]> {
this.rejectLabelsInQuery(options.query);
let query = options.query;
let labelsToFilter: string[];
if (options.labels && options.labels.length > 0) {
this.validateLabels(options.labels);
labelsToFilter = options.labels;
} else {
labelsToFilter = this.configuredLabelNames;
}
if (labelsToFilter.length > 0) {
const labelQuery = labelsToFilter.map((name) => `label:${name}`).join(' OR ');
query = `(${query}) AND (${labelQuery})`;
}
const response = await this.gmail.users.messages.list({
userId: 'me',
q: query,
maxResults: options.maxResults || 10,
pageToken: options.pageToken,
});
const messages = response.data.messages || [];
const detailedMessages: gmail_v1.Schema$Message[] = [];
for (const message of messages) {
if (message.id) {
const detail = await this.getMessage(message.id);
detailedMessages.push(detail);
}
}
return detailedMessages;
}
}