diff --git a/app/api/courrier/microsoft/callback/route.ts b/app/api/courrier/microsoft/callback/route.ts index f1835781..87f19848 100644 --- a/app/api/courrier/microsoft/callback/route.ts +++ b/app/api/courrier/microsoft/callback/route.ts @@ -66,13 +66,15 @@ export async function POST(request: Request) { // Create credentials object for Microsoft account const credentials = { email: userEmail, + // Password is empty for OAuth accounts + password: '', // Use Microsoft's IMAP server for Outlook/Office365 host: 'outlook.office365.com', port: 993, secure: true, // OAuth specific fields - useOAuth: true, + useOAuth: true, // Make sure this is explicitly set accessToken: tokens.access_token, refreshToken: tokens.refresh_token, tokenExpiry: Date.now() + (tokens.expires_in * 1000), @@ -87,6 +89,15 @@ export async function POST(request: Request) { smtp_secure: false }; + // Log Microsoft authentication details + console.log(`Microsoft OAuth credentials prepared for ${userEmail}:`, { + useOAuth: credentials.useOAuth, + host: credentials.host, + hasAccessToken: !!credentials.accessToken, + hasRefreshToken: !!credentials.refreshToken, + tokenExpiry: new Date(credentials.tokenExpiry).toISOString() + }); + // Test connection before saving console.log(`Testing Microsoft OAuth connection for user ${session.user.id}`); const testResult = await testEmailConnection(credentials); diff --git a/lib/redis.ts b/lib/redis.ts index d722684f..801581b6 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -150,6 +150,10 @@ interface EmailCredentials { smtp_secure?: boolean; display_name?: string; color?: string; + useOAuth?: boolean; + accessToken?: string; + refreshToken?: string; + tokenExpiry?: number; } interface ImapSessionData { @@ -172,7 +176,7 @@ export async function cacheEmailCredentials( const key = KEYS.CREDENTIALS(userId, accountId); // Validate credentials before caching - if (!credentials.email || !credentials.host || !credentials.password) { + if (!credentials.email || !credentials.host || (!credentials.password && !credentials.useOAuth)) { console.error(`Cannot cache incomplete credentials for user ${userId}`); return; } @@ -191,10 +195,15 @@ export async function cacheEmailCredentials( ...(credentials.smtp_port && { smtp_port: credentials.smtp_port }), ...(credentials.smtp_secure !== undefined && { smtp_secure: credentials.smtp_secure }), ...(credentials.display_name && { display_name: credentials.display_name }), - ...(credentials.color && { color: credentials.color }) + ...(credentials.color && { color: credentials.color }), + // Include OAuth fields + ...(credentials.useOAuth !== undefined && { useOAuth: credentials.useOAuth }), + ...(credentials.accessToken && { accessToken: credentials.accessToken }), + ...(credentials.refreshToken && { refreshToken: credentials.refreshToken }), + ...(credentials.tokenExpiry && { tokenExpiry: credentials.tokenExpiry }) }; - // Encrypt password + // Encrypt password if provided if (credentials.password) { try { const encrypted = encryptData(credentials.password); @@ -202,12 +211,8 @@ export async function cacheEmailCredentials( secureCredentials.encryptedPassword = encrypted; } catch (encryptError) { console.error(`Failed to encrypt password for user ${userId}:`, encryptError); - // Don't proceed with caching if encryption fails - return; + // Continue anyway since we might have OAuth tokens } - } else { - console.warn(`No password provided for user ${userId}, skipping credential caching`); - return; } await redis.set(key, JSON.stringify(secureCredentials), 'EX', TTL.CREDENTIALS); @@ -236,35 +241,44 @@ export async function getEmailCredentials( const creds = JSON.parse(credStr) as EmailCredentials; - if (!creds.encryptedPassword) { - console.warn(`No encrypted password found for user ${userId}`); - return null; + let password: string | undefined; + + // Handle OAuth accounts (they might not have a password) + if (creds.encryptedPassword) { + try { + // Decrypt the password + password = decryptData(creds.encryptedPassword); + } catch (decryptError) { + console.error(`Failed to decrypt password for user ${userId}:`, decryptError); + // For OAuth accounts, we can continue without a password + if (!creds.useOAuth) { + return null; + } + } } - try { - // Decrypt the password - const password = decryptData(creds.encryptedPassword); - - // Return the full credentials with decrypted password - return { - email: creds.email, - password, - host: creds.host, - port: creds.port, - secure: creds.secure ?? true, - // Include the extended fields if they exist in the cache - ...(creds.smtp_host && { smtp_host: creds.smtp_host }), - ...(creds.smtp_port && { smtp_port: creds.smtp_port }), - ...(creds.smtp_secure !== undefined && { smtp_secure: creds.smtp_secure }), - ...(creds.display_name && { display_name: creds.display_name }), - ...(creds.color && { color: creds.color }) - }; - } catch (decryptError) { - console.error(`Failed to decrypt password for user ${userId}:`, decryptError); - return null; - } + // Return the full credentials with decrypted password if available + const result: EmailCredentials = { + email: creds.email, + host: creds.host, + port: creds.port, + secure: creds.secure ?? true, + ...(password && { password }), + ...(creds.smtp_host && { smtp_host: creds.smtp_host }), + ...(creds.smtp_port && { smtp_port: creds.smtp_port }), + ...(creds.smtp_secure !== undefined && { smtp_secure: creds.smtp_secure }), + ...(creds.display_name && { display_name: creds.display_name }), + ...(creds.color && { color: creds.color }), + // Include OAuth fields + ...(creds.useOAuth !== undefined && { useOAuth: creds.useOAuth }), + ...(creds.accessToken && { accessToken: creds.accessToken }), + ...(creds.refreshToken && { refreshToken: creds.refreshToken }), + ...(creds.tokenExpiry && { tokenExpiry: creds.tokenExpiry }) + }; + + return result; } catch (error) { - console.error(`Error retrieving credentials for user ${userId}:`, error); + console.error(`Error getting credentials for user ${userId}:`, error); return null; } } diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 85221d55..6e92c9fb 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -386,30 +386,27 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey let authParams: any; - // Check if we should use OAuth + // Check if we have valid OAuth tokens if (extendedCreds.useOAuth && extendedCreds.accessToken) { - // Configure for XOAUTH2 - console.log(`Using XOAUTH2 authentication for ${connectionKey}`); - - // Generate the XOAUTH2 token - const xoauth2Token = createXOAuth2Token(extendedCreds.email, extendedCreds.accessToken); + console.log(`Using XOAUTH2 authentication for ${connectionKey} (OAuth enabled)`); // Set auth parameters for ImapFlow authParams = { user: extendedCreds.email, - // IMPORTANT: For ImapFlow, we need to provide the accessToken directly - // The library will handle converting it to XOAUTH2 format internally accessToken: extendedCreds.accessToken }; console.log(`XOAUTH2 auth configured for ${connectionKey}`); - } else { + } else if (extendedCreds.password) { // Use regular password authentication - console.log(`Using password authentication for ${connectionKey}`); + console.log(`Using password authentication for ${connectionKey} (OAuth not enabled or no token)`); authParams = { user: extendedCreds.email, pass: extendedCreds.password }; + } else { + // No authentication method available + throw new Error(`No authentication method available for ${connectionKey} - need either password or OAuth token`); } console.log(`Creating ImapFlow client for ${connectionKey} with authentication type: ${extendedCreds.useOAuth ? 'OAuth' : 'Password'}`);