336 lines
8.9 KiB
JavaScript
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
|