/** * Gmail Service * Handles Gmail API operations */ import { google } from 'googleapis' import { OAuth2Client } from 'google-auth-library' import { config } from '../config/index.mjs' import { log } from '../middleware/logger.mjs' /** * Gmail Service Class */ export class GmailService { constructor(accessToken, refreshToken = null) { this.auth = new OAuth2Client( config.google.clientId, config.google.clientSecret, config.google.redirectUri ) this.auth.setCredentials({ access_token: accessToken, refresh_token: refreshToken, }) this.gmail = google.gmail({ version: 'v1', auth: this.auth }) } /** * Get user's email address */ async getProfile() { const { data } = await this.gmail.users.getProfile({ userId: 'me' }) return data } /** * List emails from inbox * @param {number} maxResults - Maximum number of emails to fetch * @param {string} pageToken - Pagination token * @param {string} query - Gmail search query */ async listEmails(maxResults = 50, pageToken = null, query = 'in:inbox is:unread') { const params = { userId: 'me', maxResults, q: query, } if (pageToken) { params.pageToken = pageToken } const { data } = await this.gmail.users.messages.list(params) return { messages: data.messages || [], nextPageToken: data.nextPageToken, resultSizeEstimate: data.resultSizeEstimate, } } /** * Get full email details * @param {string} messageId - Message ID */ async getEmail(messageId) { const { data } = await this.gmail.users.messages.get({ userId: 'me', id: messageId, format: 'full', }) // Parse headers const headers = {} data.payload?.headers?.forEach(h => { headers[h.name.toLowerCase()] = h.value }) return { id: data.id, threadId: data.threadId, snippet: data.snippet, labelIds: data.labelIds || [], headers, internalDate: data.internalDate, sizeEstimate: data.sizeEstimate, } } /** * Batch get multiple emails * @param {string[]} messageIds - Array of message IDs */ async batchGetEmails(messageIds) { // Gmail API supports batch requests, but we'll do simple parallel for now const emails = await Promise.all( messageIds.map(id => this.getEmail(id).catch(e => { log.warn(`E-Mail abrufen fehlgeschlagen: ${id}`, { error: e.message }) return null })) ) return emails.filter(Boolean) } /** * Create or get a label * @param {string} name - Label name (e.g., "EmailSorter/VIP") * @param {string} color - Optional label color (must be from Gmail's palette) */ async createLabel(name, color = null) { // Gmail's allowed label colors (background colors) const GMAIL_COLORS = { red: { backgroundColor: '#fb4c2f', textColor: '#ffffff' }, orange: { backgroundColor: '#ffad47', textColor: '#000000' }, yellow: { backgroundColor: '#fad165', textColor: '#000000' }, green: { backgroundColor: '#16a766', textColor: '#ffffff' }, teal: { backgroundColor: '#43d692', textColor: '#000000' }, blue: { backgroundColor: '#4a86e8', textColor: '#ffffff' }, purple: { backgroundColor: '#a479e2', textColor: '#ffffff' }, pink: { backgroundColor: '#f691b3', textColor: '#000000' }, gray: { backgroundColor: '#666666', textColor: '#ffffff' }, } // Map our colors to Gmail colors const colorMap = { '#ff0000': GMAIL_COLORS.red, // VIP '#4285f4': GMAIL_COLORS.blue, // Kunden '#0f9d58': GMAIL_COLORS.green, // Rechnungen '#9c27b0': GMAIL_COLORS.purple, // Newsletter '#ff9800': GMAIL_COLORS.orange, // Werbung '#00bcd4': GMAIL_COLORS.teal, // Social '#f44336': GMAIL_COLORS.red, // Security '#673ab7': GMAIL_COLORS.purple, // Kalender '#607d8b': GMAIL_COLORS.gray, // Review } try { // Check if label exists const { data } = await this.gmail.users.labels.list({ userId: 'me' }) const existing = data.labels?.find(l => l.name === name) if (existing) { return existing } // Create new label const labelData = { name, labelListVisibility: 'labelShow', messageListVisibility: 'show', } // Use mapped Gmail color if available if (color && colorMap[color]) { labelData.color = colorMap[color] } else if (color) { // Default to blue if color not in map labelData.color = GMAIL_COLORS.blue } const { data: created } = await this.gmail.users.labels.create({ userId: 'me', requestBody: labelData, }) log.success(`Gmail Label erstellt: ${name}`) return created } catch (error) { log.error(`Label erstellen fehlgeschlagen: ${name}`, { error: error.message }) return null } } /** * Add labels to a message * @param {string} messageId - Message ID * @param {string[]} labelIds - Label IDs to add */ async addLabels(messageId, labelIds) { await this.gmail.users.messages.modify({ userId: 'me', id: messageId, requestBody: { addLabelIds: labelIds, }, }) } /** * Remove labels from a message * @param {string} messageId - Message ID * @param {string[]} labelIds - Label IDs to remove */ async removeLabels(messageId, labelIds) { await this.gmail.users.messages.modify({ userId: 'me', id: messageId, requestBody: { removeLabelIds: labelIds, }, }) } /** * Archive a message (remove from INBOX) * @param {string} messageId - Message ID */ async archiveEmail(messageId) { await this.removeLabels(messageId, ['INBOX']) } /** * Move message to trash * @param {string} messageId - Message ID */ async trashEmail(messageId) { await this.gmail.users.messages.trash({ userId: 'me', id: messageId, }) } /** * Mark message as read * @param {string} messageId - Message ID */ async markAsRead(messageId) { await this.removeLabels(messageId, ['UNREAD']) } /** * Mark message as unread * @param {string} messageId - Message ID */ async markAsUnread(messageId) { await this.addLabels(messageId, ['UNREAD']) } /** * Star a message * @param {string} messageId - Message ID */ async starEmail(messageId) { await this.addLabels(messageId, ['STARRED']) } /** * Get all labels */ async getLabels() { const { data } = await this.gmail.users.labels.list({ userId: 'me' }) return data.labels || [] } /** * Delete a label by ID */ async deleteLabel(labelId) { try { await this.gmail.users.labels.delete({ userId: 'me', id: labelId, }) return true } catch (error) { log.warn(`Label löschen fehlgeschlagen: ${labelId}`, { error: error.message }) return false } } /** * Cleanup old EmailSorter labels * Removes all labels starting with "EmailSorter/" and old German labels */ async cleanupOldLabels() { // Old labels to remove (German and old format) const OLD_LABELS = [ // Old "EmailSorter/" prefix labels 'EmailSorter/', // Old German labels 'Wichtig', 'Kunden', 'Rechnungen', 'Sicherheit', 'Termine', 'Prüfen', 'Werbung', 'VIP / Wichtig', 'Kunden / Projekte', 'Rechnungen / Belege', 'Werbung / Promotions', 'Social / Benachrichtigungen', 'Security / 2FA', 'Kalender / Events', 'Review / Unklar', ] try { const labels = await this.getLabels() // Filter labels that match old patterns const labelsToDelete = labels.filter(l => { if (!l.name) return false // Check for EmailSorter/ prefix if (l.name.startsWith('EmailSorter/')) return true // Check for exact matches with old labels if (OLD_LABELS.includes(l.name)) return true return false }) let deleted = 0 for (const label of labelsToDelete) { if (await this.deleteLabel(label.id)) { log.info(`Old label deleted: ${label.name}`) deleted++ } } return deleted } catch (error) { log.error('Cleanup failed', { error: error.message }) return 0 } } /** * Setup Gmail push notifications * @param {string} webhookUrl - Webhook URL for notifications */ async setupWatch(webhookUrl) { const { data } = await this.gmail.users.watch({ userId: 'me', requestBody: { labelIds: ['INBOX'], topicName: webhookUrl, // Should be a Cloud Pub/Sub topic labelFilterAction: 'include', }, }) return data } /** * Stop watching for push notifications */ async stopWatch() { await this.gmail.users.stop({ userId: 'me' }) } /** * Get history changes since a historyId * @param {string} startHistoryId - History ID to start from */ async getHistory(startHistoryId) { const { data } = await this.gmail.users.history.list({ userId: 'me', startHistoryId, historyTypes: ['messageAdded', 'labelAdded', 'labelRemoved'], }) return data.history || [] } } export default GmailService