Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte das es noch nicht fertig ist und verkaufs bereit
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., "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
|