Files
Emailsorter/server/services/gmail.mjs
2026-02-03 23:27:25 +01:00

358 lines
9.2 KiB
JavaScript

/**
* 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., "MailFlow/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 MailFlow labels
* Removes all labels starting with "MailFlow/" and old German labels
*/
async cleanupOldLabels() {
// Old labels to remove (German and old format)
const OLD_LABELS = [
// Old "MailFlow/" prefix labels
'MailFlow/',
// 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 MailFlow/ prefix
if (l.name.startsWith('MailFlow/')) 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