From b97c2310b8bfaa47b1094be80f3bab97b6bb5084 Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 2 May 2025 09:36:36 +0200 Subject: [PATCH] courrier msft oauth --- app/api/courrier/microsoft/callback/route.ts | 17 +- lib/services/email-service.ts | 167 +++++++++++-------- lib/services/microsoft-oauth.ts | 38 ++++- 3 files changed, 149 insertions(+), 73 deletions(-) diff --git a/app/api/courrier/microsoft/callback/route.ts b/app/api/courrier/microsoft/callback/route.ts index 7a1c363a..f1835781 100644 --- a/app/api/courrier/microsoft/callback/route.ts +++ b/app/api/courrier/microsoft/callback/route.ts @@ -50,9 +50,18 @@ export async function POST(request: Request) { // Exchange code for tokens const tokens = await exchangeCodeForTokens(code); - // Extract user email from token (would require token decoding in production) - // For this implementation, we'll use a temporary email - const userEmail = `${session.user.email || 'user'}@microsoft.com`; + // Extract user email from session instead of generating a fake one + // Use the logged-in user's email or a properly formatted address + let userEmail = ''; + if (session.user?.email) { + // Use the user's actual email if available + userEmail = session.user.email; + } else { + // Fallback to a default format - don't add @microsoft.com + userEmail = `unknown-user-${session.user.id}@outlook.com`; + } + + console.log(`Using email: ${userEmail} for Microsoft account`); // Create credentials object for Microsoft account const credentials = { @@ -69,7 +78,7 @@ export async function POST(request: Request) { tokenExpiry: Date.now() + (tokens.expires_in * 1000), // Optional fields - display_name: `Microsoft ${userEmail}`, + display_name: `Microsoft (${userEmail})`, color: '#0078D4', // Microsoft blue // SMTP settings for Microsoft diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 3fc73ebd..8ef6abe6 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -19,6 +19,15 @@ import { } from '@/lib/redis'; import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types'; import { ensureFreshToken } from './token-refresh'; +import { createXOAuth2Token } from './microsoft-oauth'; + +// Define EmailCredentials interface with OAuth properties +interface EmailCredentialsExtended extends EmailCredentials { + useOAuth?: boolean; + accessToken?: string; + refreshToken?: string; + tokenExpiry?: number; +} // Types specific to this service export interface EmailListResult { @@ -251,19 +260,22 @@ export async function getImapConnection( await cacheEmailCredentials(userId, accountId, credentials); } + // Cast to extended type + const extendedCreds = credentials as EmailCredentialsExtended; + // If using OAuth, ensure we have a fresh token - if (credentials.useOAuth) { + if (extendedCreds.useOAuth) { try { - console.log(`Ensuring fresh token for OAuth account ${credentials.email}`); - const { accessToken, success } = await ensureFreshToken(userId, credentials.email); + console.log(`Ensuring fresh token for OAuth account ${extendedCreds.email}`); + const { accessToken, success } = await ensureFreshToken(userId, extendedCreds.email); if (success) { - credentials.accessToken = accessToken; + extendedCreds.accessToken = accessToken; } else { - console.error(`Failed to refresh token for ${credentials.email}`); + console.error(`Failed to refresh token for ${extendedCreds.email}`); } } catch (err) { - console.error(`Error refreshing token for ${credentials.email}:`, err); + console.error(`Error refreshing token for ${extendedCreds.email}:`, err); } } @@ -284,8 +296,8 @@ export async function getImapConnection( } }, 60 * 1000); // 60 seconds timeout - // Create connection promise - const connectionPromise = createImapConnection(credentials, connectionKey) + // Create connection promise using the extended credentials + const connectionPromise = createImapConnection(extendedCreds, connectionKey) .then(client => { // Update connection pool entry connectionPool[connectionKey].client = client; @@ -331,22 +343,28 @@ export async function getImapConnection( * Helper function to create a new IMAP connection */ async function createImapConnection(credentials: EmailCredentials, connectionKey: string): Promise { - // Configure auth based on whether we're using OAuth or password - const auth = credentials.useOAuth - ? { - user: credentials.email, - // Use XOAUTH2 authentication for Microsoft accounts - accessToken: credentials.accessToken - } - : { - user: credentials.email, - pass: credentials.password, - }; + // Cast to extended type + const extendedCreds = credentials as EmailCredentialsExtended; + + // Configure auth + let auth: any = { + user: extendedCreds.email + }; + + if (extendedCreds.useOAuth && extendedCreds.accessToken) { + // For OAuth, create the proper XOAUTH2 token format + const xoauth2 = createXOAuth2Token(extendedCreds.email, extendedCreds.accessToken); + auth.xoauth2 = xoauth2; + console.log(`Using XOAUTH2 authentication for ${connectionKey}`); + } else { + auth.pass = extendedCreds.password; + console.log(`Using password authentication for ${connectionKey}`); + } const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: credentials.secure ?? true, + host: extendedCreds.host, + port: extendedCreds.port, + secure: extendedCreds.secure ?? true, auth, logger: false, emitLogs: false, @@ -988,31 +1006,36 @@ export async function sendEmail( }; } + // Cast to extended type + const extendedCreds = credentials as EmailCredentialsExtended; + // Configure SMTP auth based on OAuth or password - const auth = credentials.useOAuth - ? { - user: credentials.email, - accessToken: credentials.accessToken - } - : { - user: credentials.email, - pass: credentials.password, - }; + let auth: any = { + user: extendedCreds.email + }; + + if (extendedCreds.useOAuth && extendedCreds.accessToken) { + // For OAuth, use the XOAuth2 format + auth.type = 'OAuth2'; + auth.accessToken = extendedCreds.accessToken; + } else { + auth.pass = extendedCreds.password; + } // Create SMTP transporter with user's SMTP settings if available const transporter = nodemailer.createTransport({ - host: credentials.smtp_host || 'smtp.infomaniak.com', - port: credentials.smtp_port || 587, - secure: credentials.smtp_secure || false, + host: extendedCreds.smtp_host || 'smtp.infomaniak.com', + port: extendedCreds.smtp_port || 587, + secure: extendedCreds.smtp_secure || false, auth, tls: { rejectUnauthorized: false } - }); + } as any); try { const info = await transporter.sendMail({ - from: credentials.email, + from: extendedCreds.email, to: emailData.to, cc: emailData.cc, bcc: emailData.bcc, @@ -1068,32 +1091,39 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis error?: string; folders?: string[]; }> { + // Cast to extended type to use OAuth properties + const extendedCreds = credentials as EmailCredentialsExtended; + console.log('Testing connection with:', { - ...credentials, - password: credentials.password ? '***' : undefined, - accessToken: credentials.accessToken ? '***' : undefined, - refreshToken: credentials.refreshToken ? '***' : undefined + ...extendedCreds, + password: extendedCreds.password ? '***' : undefined, + accessToken: extendedCreds.accessToken ? '***' : undefined, + refreshToken: extendedCreds.refreshToken ? '***' : undefined }); // Test IMAP connection try { - console.log(`Testing IMAP connection to ${credentials.host}:${credentials.port} for ${credentials.email}`); + console.log(`Testing IMAP connection to ${extendedCreds.host}:${extendedCreds.port} for ${extendedCreds.email}`); // Configure auth based on whether we're using OAuth or password - const auth = credentials.useOAuth - ? { - user: credentials.email, - accessToken: credentials.accessToken - } - : { - user: credentials.email, - pass: credentials.password, - }; + let auth: any = { + user: extendedCreds.email + }; + + if (extendedCreds.useOAuth && extendedCreds.accessToken) { + // For OAuth, create the proper XOAUTH2 token format + const xoauth2 = createXOAuth2Token(extendedCreds.email, extendedCreds.accessToken); + auth.xoauth2 = xoauth2; + console.log('Using XOAUTH2 authentication mechanism'); + } else { + auth.pass = extendedCreds.password; + console.log('Using password authentication mechanism'); + } const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: credentials.secure ?? true, + host: extendedCreds.host, + port: extendedCreds.port, + secure: extendedCreds.secure ?? true, auth, logger: false, tls: { @@ -1101,34 +1131,37 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis } }); + console.log('Attempting to connect to IMAP server...'); await client.connect(); + console.log('IMAP connection successful! Getting mailboxes...'); + const folders = await getMailboxes(client); await client.logout(); - console.log(`IMAP connection successful for ${credentials.email}`); + console.log(`IMAP connection successful for ${extendedCreds.email}`); console.log(`Found ${folders.length} folders:`, folders); // Test SMTP connection if SMTP settings are provided let smtpSuccess = false; - if (credentials.smtp_host && credentials.smtp_port) { + if (extendedCreds.smtp_host && extendedCreds.smtp_port) { try { - console.log(`Testing SMTP connection to ${credentials.smtp_host}:${credentials.smtp_port}`); + console.log(`Testing SMTP connection to ${extendedCreds.smtp_host}:${extendedCreds.smtp_port}`); // Configure SMTP auth based on OAuth or password - const smtpAuth = credentials.useOAuth + const smtpAuth = extendedCreds.useOAuth ? { - user: credentials.email, - accessToken: credentials.accessToken + user: extendedCreds.email, + accessToken: extendedCreds.accessToken } : { - user: credentials.email, - pass: credentials.password, + user: extendedCreds.email, + pass: extendedCreds.password, }; const transporter = nodemailer.createTransport({ - host: credentials.smtp_host, - port: credentials.smtp_port, - secure: credentials.smtp_secure ?? false, + host: extendedCreds.smtp_host, + port: extendedCreds.smtp_port, + secure: extendedCreds.smtp_secure ?? false, auth: smtpAuth, tls: { rejectUnauthorized: false @@ -1136,10 +1169,10 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis }); await transporter.verify(); - console.log(`SMTP connection successful for ${credentials.email}`); + console.log(`SMTP connection successful for ${extendedCreds.email}`); smtpSuccess = true; } catch (smtpError) { - console.error(`SMTP connection failed for ${credentials.email}:`, smtpError); + console.error(`SMTP connection failed for ${extendedCreds.email}:`, smtpError); return { imap: true, smtp: false, @@ -1155,7 +1188,7 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis folders }; } catch (error) { - console.error(`IMAP connection failed for ${credentials.email}:`, error); + console.error(`IMAP connection failed for ${extendedCreds.email}:`, error); return { imap: false, error: `IMAP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` diff --git a/lib/services/microsoft-oauth.ts b/lib/services/microsoft-oauth.ts index 086b6ca6..19d9b79b 100644 --- a/lib/services/microsoft-oauth.ts +++ b/lib/services/microsoft-oauth.ts @@ -61,19 +61,36 @@ export async function exchangeCodeForTokens(code: string): Promise<{ }); try { + console.log(`Exchanging code for tokens. URL: ${MICROSOFT_TOKEN_URL}`); + const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); + console.log('Token exchange successful!'); return { access_token: response.data.access_token, refresh_token: response.data.refresh_token, expires_in: response.data.expires_in }; - } catch (error) { + } catch (error: any) { console.error('Error exchanging code for tokens:', error); + + // Enhanced error logging + if (error.response) { + console.error('Response data:', error.response.data); + console.error('Response status:', error.response.status); + console.error('Response headers:', error.response.headers); + + // Extract the error message from Microsoft's response format + const errorData = error.response.data; + if (errorData && errorData.error_description) { + throw new Error(`Token exchange failed: ${errorData.error_description}`); + } + } + throw new Error('Failed to exchange authorization code for tokens'); } } @@ -95,19 +112,36 @@ export async function refreshAccessToken(refreshToken: string): Promise<{ }); try { + console.log(`Refreshing access token. URL: ${MICROSOFT_TOKEN_URL}`); + const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); + console.log('Token refresh successful!'); return { access_token: response.data.access_token, refresh_token: response.data.refresh_token, expires_in: response.data.expires_in }; - } catch (error) { + } catch (error: any) { console.error('Error refreshing token:', error); + + // Enhanced error logging + if (error.response) { + console.error('Response data:', error.response.data); + console.error('Response status:', error.response.status); + console.error('Response headers:', error.response.headers); + + // Extract the error message from Microsoft's response format + const errorData = error.response.data; + if (errorData && errorData.error_description) { + throw new Error(`Token refresh failed: ${errorData.error_description}`); + } + } + throw new Error('Failed to refresh access token'); } }