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
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

View File

@ -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<ImapFlow> {
// 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'}`

View File

@ -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');
}
}