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

336 lines
8.9 KiB
JavaScript

/**
* Outlook Service
* Handles Microsoft Graph API operations for Outlook mail
*/
import { log } from '../middleware/logger.mjs'
const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0'
/**
* Outlook Service Class
*/
export class OutlookService {
constructor(accessToken) {
this.accessToken = accessToken
this.headers = {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
}
}
/**
* Make Graph API request
* @private
*/
async _request(endpoint, options = {}) {
const url = endpoint.startsWith('http') ? endpoint : `${GRAPH_API_BASE}${endpoint}`
const response = await fetch(url, {
...options,
headers: {
...this.headers,
...options.headers,
},
})
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.error?.message || `Graph API Error: ${response.status}`)
}
// Handle 204 No Content
if (response.status === 204) {
return null
}
return response.json()
}
/**
* Get user profile
*/
async getProfile() {
return this._request('/me')
}
/**
* List emails from inbox
* @param {number} top - Number of emails to fetch
* @param {string} skip - Skip token for pagination
* @param {string} filter - OData filter
*/
async listEmails(top = 50, skip = null, filter = null) {
let endpoint = `/me/mailFolders/inbox/messages?$top=${top}&$select=id,subject,from,bodyPreview,receivedDateTime,isRead,categories`
if (filter) {
endpoint += `&$filter=${encodeURIComponent(filter)}`
}
if (skip) {
endpoint += `&$skip=${skip}`
}
const data = await this._request(endpoint)
return {
messages: data.value || [],
nextLink: data['@odata.nextLink'],
count: data['@odata.count'],
}
}
/**
* Get full email details
* @param {string} messageId - Message ID
*/
async getEmail(messageId) {
return this._request(`/me/messages/${messageId}?$select=id,subject,from,body,bodyPreview,receivedDateTime,isRead,categories,importance,flag`)
}
/**
* Batch get multiple emails
* @param {string[]} messageIds - Array of message IDs
*/
async batchGetEmails(messageIds) {
// Use Graph batch API for efficiency
const batchRequest = {
requests: messageIds.map((id, index) => ({
id: String(index),
method: 'GET',
url: `/me/messages/${id}?$select=id,subject,from,bodyPreview,receivedDateTime,categories`,
})),
}
const response = await this._request('/$batch', {
method: 'POST',
body: JSON.stringify(batchRequest),
})
return response.responses
.filter(r => r.status === 200)
.map(r => r.body)
}
/**
* Update message properties
* @param {string} messageId - Message ID
* @param {object} updates - Properties to update
*/
async updateMessage(messageId, updates) {
return this._request(`/me/messages/${messageId}`, {
method: 'PATCH',
body: JSON.stringify(updates),
})
}
/**
* Add categories to a message
* @param {string} messageId - Message ID
* @param {string[]} categories - Categories to add
*/
async addCategories(messageId, categories) {
const email = await this.getEmail(messageId)
const existingCategories = email.categories || []
const newCategories = [...new Set([...existingCategories, ...categories])]
return this.updateMessage(messageId, { categories: newCategories })
}
/**
* Remove categories from a message
* @param {string} messageId - Message ID
* @param {string[]} categories - Categories to remove
*/
async removeCategories(messageId, categories) {
const email = await this.getEmail(messageId)
const existingCategories = email.categories || []
const newCategories = existingCategories.filter(c => !categories.includes(c))
return this.updateMessage(messageId, { categories: newCategories })
}
/**
* Archive a message (move to archive folder)
* @param {string} messageId - Message ID
*/
async archiveEmail(messageId) {
// First, try to get or create archive folder
let archiveFolder
try {
archiveFolder = await this._request('/me/mailFolders/archive')
} catch {
// Archive folder might not exist, try to find it
const folders = await this._request('/me/mailFolders?$filter=displayName eq \'Archive\'')
if (folders.value?.length) {
archiveFolder = folders.value[0]
}
}
if (archiveFolder) {
return this._request(`/me/messages/${messageId}/move`, {
method: 'POST',
body: JSON.stringify({ destinationId: archiveFolder.id }),
})
}
// Fallback: just mark as read
return this.markAsRead(messageId)
}
/**
* Move message to deleted items
* @param {string} messageId - Message ID
*/
async deleteEmail(messageId) {
return this._request(`/me/messages/${messageId}/move`, {
method: 'POST',
body: JSON.stringify({ destinationId: 'deleteditems' }),
})
}
/**
* Move message to a folder
* @param {string} messageId - Message ID
* @param {string} folderId - Destination folder ID
*/
async moveEmail(messageId, folderId) {
return this._request(`/me/messages/${messageId}/move`, {
method: 'POST',
body: JSON.stringify({ destinationId: folderId }),
})
}
/**
* Mark message as read
* @param {string} messageId - Message ID
*/
async markAsRead(messageId) {
return this.updateMessage(messageId, { isRead: true })
}
/**
* Mark message as unread
* @param {string} messageId - Message ID
*/
async markAsUnread(messageId) {
return this.updateMessage(messageId, { isRead: false })
}
/**
* Flag a message
* @param {string} messageId - Message ID
* @param {string} flagStatus - 'flagged' | 'complete' | 'notFlagged'
*/
async flagEmail(messageId, flagStatus = 'flagged') {
return this.updateMessage(messageId, {
flag: { flagStatus },
})
}
/**
* Set message importance
* @param {string} messageId - Message ID
* @param {string} importance - 'low' | 'normal' | 'high'
*/
async setImportance(messageId, importance) {
return this.updateMessage(messageId, { importance })
}
/**
* Get all mail folders
*/
async getFolders() {
const data = await this._request('/me/mailFolders')
return data.value || []
}
/**
* Create a mail folder
* @param {string} displayName - Folder name
* @param {string} parentFolderId - Parent folder ID (optional)
*/
async createFolder(displayName, parentFolderId = null) {
const endpoint = parentFolderId
? `/me/mailFolders/${parentFolderId}/childFolders`
: '/me/mailFolders'
return this._request(endpoint, {
method: 'POST',
body: JSON.stringify({ displayName }),
})
}
/**
* Get available categories
*/
async getCategories() {
const data = await this._request('/me/outlook/masterCategories')
return data.value || []
}
/**
* Create a category
* @param {string} displayName - Category name
* @param {string} color - Color preset (e.g., 'preset0' to 'preset24')
*/
async createCategory(displayName, color = 'preset0') {
try {
return await this._request('/me/outlook/masterCategories', {
method: 'POST',
body: JSON.stringify({ displayName, color }),
})
} catch (error) {
// Category might already exist
log.warn(`Kategorie erstellen fehlgeschlagen: ${displayName}`, { error: error.message })
return null
}
}
/**
* Create subscription for webhook notifications
* @param {string} webhookUrl - Notification URL
* @param {number} expirationMinutes - Subscription expiration (max 4230 minutes / ~3 days)
*/
async createSubscription(webhookUrl, expirationMinutes = 4230) {
const expirationDateTime = new Date(Date.now() + expirationMinutes * 60000).toISOString()
return this._request('/subscriptions', {
method: 'POST',
body: JSON.stringify({
changeType: 'created,updated',
notificationUrl: webhookUrl,
resource: 'me/mailFolders(\'inbox\')/messages',
expirationDateTime,
clientState: 'mailflow-webhook',
}),
})
}
/**
* Renew subscription
* @param {string} subscriptionId - Subscription ID
* @param {number} expirationMinutes - New expiration time
*/
async renewSubscription(subscriptionId, expirationMinutes = 4230) {
const expirationDateTime = new Date(Date.now() + expirationMinutes * 60000).toISOString()
return this._request(`/subscriptions/${subscriptionId}`, {
method: 'PATCH',
body: JSON.stringify({ expirationDateTime }),
})
}
/**
* Delete subscription
* @param {string} subscriptionId - Subscription ID
*/
async deleteSubscription(subscriptionId) {
return this._request(`/subscriptions/${subscriptionId}`, {
method: 'DELETE',
})
}
}
export default OutlookService