Agenda Sync refactor Mig Graph
This commit is contained in:
parent
5ecc7c9500
commit
901b8cd7af
@ -27,7 +27,8 @@ export async function getInfomaniakCalDAVClient(
|
||||
password: string
|
||||
): Promise<WebDAVClient> {
|
||||
// Infomaniak CalDAV base URL (from Infomaniak sync assistant)
|
||||
const baseUrl = 'https://sync.infomaniak.com';
|
||||
// The actual CalDAV endpoint is at /caldav path
|
||||
const baseUrl = 'https://sync.infomaniak.com/caldav';
|
||||
|
||||
const client = createClient(baseUrl, {
|
||||
username: email,
|
||||
|
||||
@ -21,6 +21,12 @@ import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@
|
||||
import { ensureFreshToken } from './token-refresh';
|
||||
import { createXOAuth2Token, refreshAccessToken as refreshMicrosoftAccessToken } from './microsoft-oauth';
|
||||
import { MailCredentials } from '@prisma/client';
|
||||
import {
|
||||
fetchGraphEmails as fetchGraphEmailsAPI,
|
||||
fetchGraphMailFolders,
|
||||
sendGraphEmail as sendGraphEmailAPI,
|
||||
GraphMailMessage,
|
||||
} from './microsoft-graph-mail';
|
||||
import Redis from 'ioredis';
|
||||
import { getRedisClient } from '../redis';
|
||||
import { logger } from '@/lib/logger';
|
||||
@ -779,8 +785,123 @@ interface FetchOptions {
|
||||
bodyParts: { part: string; query: any; limit?: number }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an account should use Microsoft Graph API instead of IMAP
|
||||
*/
|
||||
async function shouldUseGraphAPI(userId: string, accountId?: string): Promise<{ useGraph: boolean; mailCredentialId?: string }> {
|
||||
try {
|
||||
// Resolve accountId if it's 'default'
|
||||
let resolvedAccountId = accountId;
|
||||
if (!resolvedAccountId || resolvedAccountId === 'default') {
|
||||
const accounts = await prisma.mailCredentials.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
take: 1
|
||||
});
|
||||
if (accounts && accounts.length > 0) {
|
||||
resolvedAccountId = accounts[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedAccountId) {
|
||||
return { useGraph: false };
|
||||
}
|
||||
|
||||
// Check if this is a Microsoft account with OAuth
|
||||
const mailCredential = await prisma.mailCredentials.findUnique({
|
||||
where: { id: resolvedAccountId },
|
||||
select: {
|
||||
id: true,
|
||||
host: true,
|
||||
use_oauth: true,
|
||||
refresh_token: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (mailCredential &&
|
||||
mailCredential.host === 'outlook.office365.com' &&
|
||||
mailCredential.use_oauth &&
|
||||
mailCredential.refresh_token) {
|
||||
return { useGraph: true, mailCredentialId: mailCredential.id };
|
||||
}
|
||||
|
||||
return { useGraph: false };
|
||||
} catch (error) {
|
||||
logger.error('[EMAIL] Error checking if should use Graph API', {
|
||||
userId,
|
||||
accountId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return { useGraph: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Graph API folder name to Graph API folder ID
|
||||
* Graph API uses folder IDs, but we can map common names
|
||||
*/
|
||||
function mapFolderNameToGraphId(folderName: string): string {
|
||||
const folderMap: Record<string, string> = {
|
||||
'INBOX': 'Inbox',
|
||||
'Sent': 'SentItems',
|
||||
'Drafts': 'Drafts',
|
||||
'Trash': 'DeletedItems',
|
||||
'Junk': 'JunkEmail',
|
||||
'Archive': 'Archive',
|
||||
};
|
||||
|
||||
return folderMap[folderName] || folderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Graph API mail message to EmailMessage format
|
||||
*/
|
||||
function convertGraphMessageToEmailMessage(
|
||||
graphMessage: GraphMailMessage,
|
||||
folder: string,
|
||||
accountId: string
|
||||
): EmailMessage {
|
||||
return {
|
||||
id: graphMessage.id,
|
||||
from: graphMessage.from ? [{
|
||||
name: graphMessage.from.emailAddress.name || '',
|
||||
address: graphMessage.from.emailAddress.address,
|
||||
}] : [],
|
||||
to: graphMessage.toRecipients?.map(recipient => ({
|
||||
name: recipient.emailAddress.name || '',
|
||||
address: recipient.emailAddress.address,
|
||||
})) || [],
|
||||
cc: graphMessage.ccRecipients?.map(recipient => ({
|
||||
name: recipient.emailAddress.name || '',
|
||||
address: recipient.emailAddress.address,
|
||||
})),
|
||||
subject: graphMessage.subject || '',
|
||||
date: new Date(graphMessage.receivedDateTime),
|
||||
flags: {
|
||||
seen: graphMessage.isRead,
|
||||
flagged: graphMessage.flag?.flagStatus === 'flagged' || graphMessage.flag?.flagStatus === 'complete',
|
||||
answered: false, // Graph API doesn't have this flag directly
|
||||
draft: false, // Would need to check message type
|
||||
deleted: false,
|
||||
},
|
||||
size: 0, // Graph API doesn't provide size directly in this format
|
||||
hasAttachments: graphMessage.hasAttachments || false,
|
||||
folder,
|
||||
contentFetched: false,
|
||||
accountId,
|
||||
content: {
|
||||
text: graphMessage.body?.contentType === 'text' ? (graphMessage.body.content || '') : '',
|
||||
html: graphMessage.body?.contentType === 'html' ? (graphMessage.body.content || '') : (graphMessage.bodyPreview || ''),
|
||||
isHtml: graphMessage.body?.contentType === 'html',
|
||||
direction: 'ltr',
|
||||
},
|
||||
preview: graphMessage.bodyPreview,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of emails for a user
|
||||
* Uses Graph API for Microsoft accounts, IMAP for others
|
||||
*/
|
||||
export async function getEmails(
|
||||
userId: string,
|
||||
@ -801,6 +922,79 @@ export async function getEmails(
|
||||
});
|
||||
|
||||
try {
|
||||
// Check if we should use Graph API for Microsoft accounts
|
||||
const graphCheck = await shouldUseGraphAPI(userId, accountId);
|
||||
|
||||
if (graphCheck.useGraph && graphCheck.mailCredentialId) {
|
||||
// Use Microsoft Graph API
|
||||
logger.debug('[EMAIL] Using Microsoft Graph API', {
|
||||
userId,
|
||||
folder,
|
||||
mailCredentialId: graphCheck.mailCredentialId,
|
||||
});
|
||||
|
||||
try {
|
||||
const graphFolderId = mapFolderNameToGraphId(folder);
|
||||
const skip = (page - 1) * perPage;
|
||||
|
||||
// Fetch emails from Graph API
|
||||
const graphResult = await fetchGraphEmailsAPI(
|
||||
graphCheck.mailCredentialId,
|
||||
graphFolderId,
|
||||
perPage,
|
||||
skip
|
||||
);
|
||||
|
||||
// Get mailboxes (folders)
|
||||
const graphFolders = await fetchGraphMailFolders(graphCheck.mailCredentialId);
|
||||
const mailboxes = graphFolders.map(f => f.displayName);
|
||||
|
||||
// Convert Graph messages to EmailMessage format
|
||||
const emails = graphResult.value.map(msg =>
|
||||
convertGraphMessageToEmailMessage(msg, folder, accountId || 'default')
|
||||
);
|
||||
|
||||
// Calculate total (Graph API doesn't provide total count directly, so we estimate)
|
||||
const totalEmails = graphResult['@odata.nextLink']
|
||||
? (page * perPage) + 1 // Has more pages
|
||||
: emails.length;
|
||||
|
||||
const result: EmailListResult = {
|
||||
emails,
|
||||
totalEmails,
|
||||
page,
|
||||
perPage,
|
||||
totalPages: Math.ceil(totalEmails / perPage),
|
||||
folder,
|
||||
mailboxes,
|
||||
newestEmailId: emails.length > 0 ? parseInt(emails[0].id) || 0 : 0,
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
if (!checkOnly) {
|
||||
await cacheEmailList(
|
||||
userId,
|
||||
accountId || 'default',
|
||||
folder,
|
||||
page,
|
||||
perPage,
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[EMAIL] Error fetching emails from Graph API', {
|
||||
userId,
|
||||
folder,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
// Fall back to IMAP if Graph API fails
|
||||
logger.debug('[EMAIL] Falling back to IMAP after Graph API error');
|
||||
}
|
||||
}
|
||||
|
||||
// Use IMAP for non-Microsoft accounts or as fallback
|
||||
// The getImapConnection function already handles 'default' accountId by finding the first available account
|
||||
const client = await getImapConnection(userId, accountId);
|
||||
|
||||
@ -1387,9 +1581,52 @@ export async function sendEmail(
|
||||
content: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
},
|
||||
accountId?: string
|
||||
): Promise<{ success: boolean; messageId?: string; error?: string }> {
|
||||
const credentials = await getUserEmailCredentials(userId);
|
||||
// Check if we should use Graph API for Microsoft accounts
|
||||
const graphCheck = await shouldUseGraphAPI(userId, accountId);
|
||||
|
||||
if (graphCheck.useGraph && graphCheck.mailCredentialId) {
|
||||
// Use Microsoft Graph API to send email
|
||||
logger.debug('[EMAIL] Sending email via Microsoft Graph API', {
|
||||
userId,
|
||||
mailCredentialId: graphCheck.mailCredentialId,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await sendGraphEmailAPI(graphCheck.mailCredentialId, {
|
||||
to: emailData.to,
|
||||
cc: emailData.cc,
|
||||
bcc: emailData.bcc,
|
||||
subject: emailData.subject,
|
||||
body: emailData.body,
|
||||
bodyType: 'html',
|
||||
attachments: emailData.attachments?.map(att => ({
|
||||
name: att.name,
|
||||
content: att.content, // Should be base64 encoded
|
||||
contentType: att.type,
|
||||
})),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
messageId: result.id,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[EMAIL] Error sending email via Graph API', {
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Use SMTP for non-Microsoft accounts
|
||||
const credentials = await getUserEmailCredentials(userId, accountId);
|
||||
|
||||
if (!credentials) {
|
||||
return {
|
||||
|
||||
343
lib/services/microsoft-graph-mail.ts
Normal file
343
lib/services/microsoft-graph-mail.ts
Normal file
@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Microsoft Graph API Mail Service
|
||||
*
|
||||
* This service replaces IMAP/SMTP for Microsoft accounts by using Graph API.
|
||||
* It provides email reading, sending, and folder management capabilities.
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { ensureFreshToken } from './token-refresh';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// Graph API base URL
|
||||
const GRAPH_API_BASE = 'https://graph.microsoft.com/v1.0';
|
||||
|
||||
/**
|
||||
* Get a configured Axios instance for Microsoft Graph API
|
||||
*/
|
||||
async function getMicrosoftGraphClient(mailCredentialId: string): Promise<AxiosInstance> {
|
||||
// Get fresh access token
|
||||
const tokenResult = await ensureFreshToken(mailCredentialId);
|
||||
|
||||
if (!tokenResult || !tokenResult.accessToken) {
|
||||
throw new Error('Failed to obtain valid access token for Microsoft Graph API. The account may need to be re-authenticated with Mail.Read and Mail.Send permissions.');
|
||||
}
|
||||
|
||||
return axios.create({
|
||||
baseURL: GRAPH_API_BASE,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${tokenResult.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Graph Mail Message interface
|
||||
*/
|
||||
export interface GraphMailMessage {
|
||||
id: string;
|
||||
subject?: string;
|
||||
from?: {
|
||||
emailAddress: {
|
||||
name?: string;
|
||||
address: string;
|
||||
};
|
||||
};
|
||||
toRecipients?: Array<{
|
||||
emailAddress: {
|
||||
name?: string;
|
||||
address: string;
|
||||
};
|
||||
}>;
|
||||
ccRecipients?: Array<{
|
||||
emailAddress: {
|
||||
name?: string;
|
||||
address: string;
|
||||
};
|
||||
}>;
|
||||
body?: {
|
||||
contentType: 'text' | 'html';
|
||||
content: string;
|
||||
};
|
||||
bodyPreview?: string;
|
||||
receivedDateTime: string;
|
||||
sentDateTime?: string;
|
||||
isRead: boolean;
|
||||
hasAttachments: boolean;
|
||||
importance?: 'low' | 'normal' | 'high';
|
||||
flag?: {
|
||||
flagStatus: 'notFlagged' | 'flagged' | 'complete';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft Graph Mail Folder interface
|
||||
*/
|
||||
export interface GraphMailFolder {
|
||||
id: string;
|
||||
displayName: string;
|
||||
parentFolderId?: string;
|
||||
childFolderCount: number;
|
||||
unreadItemCount: number;
|
||||
totalItemCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch emails from a Microsoft mailbox folder using Graph API
|
||||
*/
|
||||
export async function fetchGraphEmails(
|
||||
mailCredentialId: string,
|
||||
folderId: string = 'Inbox',
|
||||
top: number = 50,
|
||||
skip: number = 0,
|
||||
filter?: string
|
||||
): Promise<{
|
||||
value: GraphMailMessage[];
|
||||
'@odata.nextLink'?: string;
|
||||
}> {
|
||||
try {
|
||||
const client = await getMicrosoftGraphClient(mailCredentialId);
|
||||
|
||||
// Build the query URL
|
||||
let url = `/me/mailFolders/${folderId}/messages`;
|
||||
const params = new URLSearchParams({
|
||||
'$top': top.toString(),
|
||||
'$skip': skip.toString(),
|
||||
'$orderby': 'receivedDateTime desc',
|
||||
'$select': 'id,subject,from,toRecipients,ccRecipients,body,bodyPreview,receivedDateTime,sentDateTime,isRead,hasAttachments,importance,flag',
|
||||
});
|
||||
|
||||
if (filter) {
|
||||
params.append('$filter', filter);
|
||||
}
|
||||
|
||||
url += `?${params.toString()}`;
|
||||
|
||||
const response = await client.get(url);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching emails from Microsoft Graph', {
|
||||
mailCredentialId,
|
||||
folderId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
|
||||
// Check for authentication errors
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
throw new Error('Microsoft Graph API access denied - may need to re-authenticate with Mail.Read and Mail.Send permissions');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single email by ID from Microsoft Graph
|
||||
*/
|
||||
export async function fetchGraphEmail(
|
||||
mailCredentialId: string,
|
||||
messageId: string
|
||||
): Promise<GraphMailMessage> {
|
||||
try {
|
||||
const client = await getMicrosoftGraphClient(mailCredentialId);
|
||||
|
||||
const response = await client.get(`/me/messages/${messageId}`, {
|
||||
params: {
|
||||
'$select': 'id,subject,from,toRecipients,ccRecipients,bccRecipients,body,bodyPreview,receivedDateTime,sentDateTime,isRead,hasAttachments,importance,flag,attachments',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching email from Microsoft Graph', {
|
||||
mailCredentialId,
|
||||
messageId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
throw new Error('Microsoft Graph API access denied - may need to re-authenticate with Mail.Read permissions');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of mail folders from Microsoft Graph
|
||||
*/
|
||||
export async function fetchGraphMailFolders(
|
||||
mailCredentialId: string
|
||||
): Promise<GraphMailFolder[]> {
|
||||
try {
|
||||
const client = await getMicrosoftGraphClient(mailCredentialId);
|
||||
|
||||
const response = await client.get('/me/mailFolders', {
|
||||
params: {
|
||||
'$select': 'id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount',
|
||||
'$top': 100,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data.value || [];
|
||||
} catch (error: any) {
|
||||
logger.error('Error fetching mail folders from Microsoft Graph', {
|
||||
mailCredentialId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
throw new Error('Microsoft Graph API access denied - may need to re-authenticate with Mail.Read permissions');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email using Microsoft Graph API
|
||||
*/
|
||||
export async function sendGraphEmail(
|
||||
mailCredentialId: string,
|
||||
emailData: {
|
||||
to: string | string[];
|
||||
cc?: string | string[];
|
||||
bcc?: string | string[];
|
||||
subject: string;
|
||||
body: string;
|
||||
bodyType?: 'text' | 'html';
|
||||
attachments?: Array<{
|
||||
name: string;
|
||||
content: string;
|
||||
contentType: string;
|
||||
}>;
|
||||
}
|
||||
): Promise<{ id: string }> {
|
||||
try {
|
||||
const client = await getMicrosoftGraphClient(mailCredentialId);
|
||||
|
||||
// Get the mail credential to get the sender email
|
||||
const mailCredential = await prisma.mailCredentials.findUnique({
|
||||
where: { id: mailCredentialId },
|
||||
select: { email: true },
|
||||
});
|
||||
|
||||
if (!mailCredential) {
|
||||
throw new Error('Mail credential not found');
|
||||
}
|
||||
|
||||
// Build the message payload
|
||||
const message: any = {
|
||||
subject: emailData.subject,
|
||||
body: {
|
||||
contentType: emailData.bodyType || 'html',
|
||||
content: emailData.body,
|
||||
},
|
||||
toRecipients: Array.isArray(emailData.to)
|
||||
? emailData.to.map(email => ({ emailAddress: { address: email } }))
|
||||
: [{ emailAddress: { address: emailData.to } }],
|
||||
};
|
||||
|
||||
if (emailData.cc) {
|
||||
message.ccRecipients = Array.isArray(emailData.cc)
|
||||
? emailData.cc.map(email => ({ emailAddress: { address: email } }))
|
||||
: [{ emailAddress: { address: emailData.cc } }];
|
||||
}
|
||||
|
||||
if (emailData.bcc) {
|
||||
message.bccRecipients = Array.isArray(emailData.bcc)
|
||||
? emailData.bcc.map(email => ({ emailAddress: { address: email } }))
|
||||
: [{ emailAddress: { address: emailData.bcc } }];
|
||||
}
|
||||
|
||||
if (emailData.attachments && emailData.attachments.length > 0) {
|
||||
message.attachments = emailData.attachments.map(att => ({
|
||||
'@odata.type': '#microsoft.graph.fileAttachment',
|
||||
name: att.name,
|
||||
contentType: att.contentType,
|
||||
contentBytes: att.content, // Should be base64 encoded
|
||||
}));
|
||||
}
|
||||
|
||||
// Create a draft message first to get the message ID
|
||||
const draftResponse = await client.post('/me/messages', message);
|
||||
const draftId = draftResponse.data.id;
|
||||
|
||||
// Send the draft
|
||||
await client.post(`/me/messages/${draftId}/send`);
|
||||
|
||||
return { id: draftId };
|
||||
} catch (error: any) {
|
||||
logger.error('Error sending email via Microsoft Graph', {
|
||||
mailCredentialId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
throw new Error('Microsoft Graph API access denied - may need to re-authenticate with Mail.Send permissions');
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an email as read/unread using Graph API
|
||||
*/
|
||||
export async function markGraphEmailAsRead(
|
||||
mailCredentialId: string,
|
||||
messageId: string,
|
||||
isRead: boolean = true
|
||||
): Promise<void> {
|
||||
try {
|
||||
const client = await getMicrosoftGraphClient(mailCredentialId);
|
||||
|
||||
await client.patch(`/me/messages/${messageId}`, {
|
||||
isRead,
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error('Error marking email as read via Microsoft Graph', {
|
||||
mailCredentialId,
|
||||
messageId,
|
||||
isRead,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread email count for a folder
|
||||
*/
|
||||
export async function getGraphUnreadCount(
|
||||
mailCredentialId: string,
|
||||
folderId: string = 'Inbox'
|
||||
): Promise<number> {
|
||||
try {
|
||||
const client = await getMicrosoftGraphClient(mailCredentialId);
|
||||
|
||||
const response = await client.get(`/me/mailFolders/${folderId}`, {
|
||||
params: {
|
||||
'$select': 'unreadItemCount',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data.unreadItemCount || 0;
|
||||
} catch (error: any) {
|
||||
logger.error('Error getting unread count from Microsoft Graph', {
|
||||
mailCredentialId,
|
||||
folderId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -14,12 +14,13 @@ const redirectUri = process.env.MICROSOFT_REDIRECT_URI;
|
||||
|
||||
// NOTE: In production we do not log Microsoft OAuth configuration to avoid noise and potential leakage.
|
||||
|
||||
// Required scopes for IMAP, SMTP, and Calendar access
|
||||
// Required scopes for Microsoft Graph API (Mail and Calendar)
|
||||
// All scopes use the same resource (graph.microsoft.com) to avoid multi-resource errors
|
||||
const REQUIRED_SCOPES = [
|
||||
'offline_access',
|
||||
'https://outlook.office.com/IMAP.AccessAsUser.All',
|
||||
'https://outlook.office.com/SMTP.Send',
|
||||
'https://graph.microsoft.com/Calendars.Read', // Microsoft Graph API scope for calendar read access
|
||||
'https://graph.microsoft.com/Mail.Read', // Read mail via Graph API
|
||||
'https://graph.microsoft.com/Mail.Send', // Send mail via Graph API
|
||||
'https://graph.microsoft.com/Calendars.Read', // Read calendars via Graph API
|
||||
].join(' ');
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user