358 lines
9.2 KiB
JavaScript
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
|