courrier msft oauth
This commit is contained in:
parent
ad7cf7514e
commit
b97c2310b8
@ -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
|
||||||
|
|||||||
@ -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'}`
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user