courrier msft oauth

This commit is contained in:
alma 2025-05-02 09:36:36 +02:00
parent ad7cf7514e
commit b97c2310b8
3 changed files with 149 additions and 73 deletions

View File

@ -50,9 +50,18 @@ export async function POST(request: Request) {
// Exchange code for tokens // Exchange code for tokens
const tokens = await exchangeCodeForTokens(code); const tokens = await exchangeCodeForTokens(code);
// Extract user email from token (would require token decoding in production) // Extract user email from session instead of generating a fake one
// For this implementation, we'll use a temporary email // Use the logged-in user's email or a properly formatted address
const userEmail = `${session.user.email || 'user'}@microsoft.com`; 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 // Create credentials object for Microsoft account
const credentials = { const credentials = {
@ -69,7 +78,7 @@ export async function POST(request: Request) {
tokenExpiry: Date.now() + (tokens.expires_in * 1000), tokenExpiry: Date.now() + (tokens.expires_in * 1000),
// Optional fields // Optional fields
display_name: `Microsoft ${userEmail}`, display_name: `Microsoft (${userEmail})`,
color: '#0078D4', // Microsoft blue color: '#0078D4', // Microsoft blue
// SMTP settings for Microsoft // SMTP settings for Microsoft

View File

@ -19,6 +19,15 @@ import {
} from '@/lib/redis'; } from '@/lib/redis';
import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types'; import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types';
import { ensureFreshToken } from './token-refresh'; 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 // Types specific to this service
export interface EmailListResult { export interface EmailListResult {
@ -251,19 +260,22 @@ export async function getImapConnection(
await cacheEmailCredentials(userId, accountId, credentials); await cacheEmailCredentials(userId, accountId, credentials);
} }
// Cast to extended type
const extendedCreds = credentials as EmailCredentialsExtended;
// If using OAuth, ensure we have a fresh token // If using OAuth, ensure we have a fresh token
if (credentials.useOAuth) { if (extendedCreds.useOAuth) {
try { try {
console.log(`Ensuring fresh token for OAuth account ${credentials.email}`); console.log(`Ensuring fresh token for OAuth account ${extendedCreds.email}`);
const { accessToken, success } = await ensureFreshToken(userId, credentials.email); const { accessToken, success } = await ensureFreshToken(userId, extendedCreds.email);
if (success) { if (success) {
credentials.accessToken = accessToken; extendedCreds.accessToken = accessToken;
} else { } else {
console.error(`Failed to refresh token for ${credentials.email}`); console.error(`Failed to refresh token for ${extendedCreds.email}`);
} }
} catch (err) { } 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 }, 60 * 1000); // 60 seconds timeout
// Create connection promise // Create connection promise using the extended credentials
const connectionPromise = createImapConnection(credentials, connectionKey) const connectionPromise = createImapConnection(extendedCreds, connectionKey)
.then(client => { .then(client => {
// Update connection pool entry // Update connection pool entry
connectionPool[connectionKey].client = client; connectionPool[connectionKey].client = client;
@ -331,22 +343,28 @@ export async function getImapConnection(
* Helper function to create a new IMAP connection * Helper function to create a new IMAP connection
*/ */
async function createImapConnection(credentials: EmailCredentials, connectionKey: string): Promise<ImapFlow> { async function createImapConnection(credentials: EmailCredentials, connectionKey: string): Promise<ImapFlow> {
// Configure auth based on whether we're using OAuth or password // Cast to extended type
const auth = credentials.useOAuth const extendedCreds = credentials as EmailCredentialsExtended;
? {
user: credentials.email, // Configure auth
// Use XOAUTH2 authentication for Microsoft accounts let auth: any = {
accessToken: credentials.accessToken user: extendedCreds.email
} };
: {
user: credentials.email, if (extendedCreds.useOAuth && extendedCreds.accessToken) {
pass: credentials.password, // 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({ const client = new ImapFlow({
host: credentials.host, host: extendedCreds.host,
port: credentials.port, port: extendedCreds.port,
secure: credentials.secure ?? true, secure: extendedCreds.secure ?? true,
auth, auth,
logger: false, logger: false,
emitLogs: 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 // Configure SMTP auth based on OAuth or password
const auth = credentials.useOAuth let auth: any = {
? { user: extendedCreds.email
user: credentials.email, };
accessToken: credentials.accessToken
} if (extendedCreds.useOAuth && extendedCreds.accessToken) {
: { // For OAuth, use the XOAuth2 format
user: credentials.email, auth.type = 'OAuth2';
pass: credentials.password, auth.accessToken = extendedCreds.accessToken;
}; } else {
auth.pass = extendedCreds.password;
}
// Create SMTP transporter with user's SMTP settings if available // Create SMTP transporter with user's SMTP settings if available
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: credentials.smtp_host || 'smtp.infomaniak.com', host: extendedCreds.smtp_host || 'smtp.infomaniak.com',
port: credentials.smtp_port || 587, port: extendedCreds.smtp_port || 587,
secure: credentials.smtp_secure || false, secure: extendedCreds.smtp_secure || false,
auth, auth,
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized: false
} }
}); } as any);
try { try {
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: credentials.email, from: extendedCreds.email,
to: emailData.to, to: emailData.to,
cc: emailData.cc, cc: emailData.cc,
bcc: emailData.bcc, bcc: emailData.bcc,
@ -1068,32 +1091,39 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
error?: string; error?: string;
folders?: string[]; folders?: string[];
}> { }> {
// Cast to extended type to use OAuth properties
const extendedCreds = credentials as EmailCredentialsExtended;
console.log('Testing connection with:', { console.log('Testing connection with:', {
...credentials, ...extendedCreds,
password: credentials.password ? '***' : undefined, password: extendedCreds.password ? '***' : undefined,
accessToken: credentials.accessToken ? '***' : undefined, accessToken: extendedCreds.accessToken ? '***' : undefined,
refreshToken: credentials.refreshToken ? '***' : undefined refreshToken: extendedCreds.refreshToken ? '***' : undefined
}); });
// Test IMAP connection // Test IMAP connection
try { 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 // Configure auth based on whether we're using OAuth or password
const auth = credentials.useOAuth let auth: any = {
? { user: extendedCreds.email
user: credentials.email, };
accessToken: credentials.accessToken
} if (extendedCreds.useOAuth && extendedCreds.accessToken) {
: { // For OAuth, create the proper XOAUTH2 token format
user: credentials.email, const xoauth2 = createXOAuth2Token(extendedCreds.email, extendedCreds.accessToken);
pass: credentials.password, auth.xoauth2 = xoauth2;
}; console.log('Using XOAUTH2 authentication mechanism');
} else {
auth.pass = extendedCreds.password;
console.log('Using password authentication mechanism');
}
const client = new ImapFlow({ const client = new ImapFlow({
host: credentials.host, host: extendedCreds.host,
port: credentials.port, port: extendedCreds.port,
secure: credentials.secure ?? true, secure: extendedCreds.secure ?? true,
auth, auth,
logger: false, logger: false,
tls: { tls: {
@ -1101,34 +1131,37 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
} }
}); });
console.log('Attempting to connect to IMAP server...');
await client.connect(); await client.connect();
console.log('IMAP connection successful! Getting mailboxes...');
const folders = await getMailboxes(client); const folders = await getMailboxes(client);
await client.logout(); 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); console.log(`Found ${folders.length} folders:`, folders);
// Test SMTP connection if SMTP settings are provided // Test SMTP connection if SMTP settings are provided
let smtpSuccess = false; let smtpSuccess = false;
if (credentials.smtp_host && credentials.smtp_port) { if (extendedCreds.smtp_host && extendedCreds.smtp_port) {
try { 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 // Configure SMTP auth based on OAuth or password
const smtpAuth = credentials.useOAuth const smtpAuth = extendedCreds.useOAuth
? { ? {
user: credentials.email, user: extendedCreds.email,
accessToken: credentials.accessToken accessToken: extendedCreds.accessToken
} }
: { : {
user: credentials.email, user: extendedCreds.email,
pass: credentials.password, pass: extendedCreds.password,
}; };
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
host: credentials.smtp_host, host: extendedCreds.smtp_host,
port: credentials.smtp_port, port: extendedCreds.smtp_port,
secure: credentials.smtp_secure ?? false, secure: extendedCreds.smtp_secure ?? false,
auth: smtpAuth, auth: smtpAuth,
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized: false
@ -1136,10 +1169,10 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
}); });
await transporter.verify(); await transporter.verify();
console.log(`SMTP connection successful for ${credentials.email}`); console.log(`SMTP connection successful for ${extendedCreds.email}`);
smtpSuccess = true; smtpSuccess = true;
} catch (smtpError) { } catch (smtpError) {
console.error(`SMTP connection failed for ${credentials.email}:`, smtpError); console.error(`SMTP connection failed for ${extendedCreds.email}:`, smtpError);
return { return {
imap: true, imap: true,
smtp: false, smtp: false,
@ -1155,7 +1188,7 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis
folders folders
}; };
} catch (error) { } catch (error) {
console.error(`IMAP connection failed for ${credentials.email}:`, error); console.error(`IMAP connection failed for ${extendedCreds.email}:`, error);
return { return {
imap: false, imap: false,
error: `IMAP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` error: `IMAP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}`

View File

@ -61,19 +61,36 @@ export async function exchangeCodeForTokens(code: string): Promise<{
}); });
try { try {
console.log(`Exchanging code for tokens. URL: ${MICROSOFT_TOKEN_URL}`);
const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), { const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
}); });
console.log('Token exchange successful!');
return { return {
access_token: response.data.access_token, access_token: response.data.access_token,
refresh_token: response.data.refresh_token, refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in expires_in: response.data.expires_in
}; };
} catch (error) { } catch (error: any) {
console.error('Error exchanging code for tokens:', error); 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'); throw new Error('Failed to exchange authorization code for tokens');
} }
} }
@ -95,19 +112,36 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
}); });
try { try {
console.log(`Refreshing access token. URL: ${MICROSOFT_TOKEN_URL}`);
const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), { const response = await axios.post(MICROSOFT_TOKEN_URL, params.toString(), {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
}); });
console.log('Token refresh successful!');
return { return {
access_token: response.data.access_token, access_token: response.data.access_token,
refresh_token: response.data.refresh_token, refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in expires_in: response.data.expires_in
}; };
} catch (error) { } catch (error: any) {
console.error('Error refreshing token:', error); 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'); throw new Error('Failed to refresh access token');
} }
} }