diff --git a/src/app/api/drafts/fetch/route.js b/src/app/api/drafts/fetch/route.js new file mode 100644 index 0000000..1fb7798 --- /dev/null +++ b/src/app/api/drafts/fetch/route.js @@ -0,0 +1,108 @@ +import { google } from 'googleapis'; +import { getSession } from '@auth0/nextjs-auth0'; +import getUserTokens from '@/lib/getUserTokens'; + +export async function GET(req) { + const { searchParams } = new URL(req.url); + const email = searchParams.get('email'); + + if (!email) { + return new Response(JSON.stringify({ message: 'Email is required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + try { + const session = await getSession(req); + const user = session?.user; + + if (!user) { + return new Response(JSON.stringify({ message: 'User not authenticated' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const userTokens = await getUserTokens(email); + if (!userTokens || !userTokens.access_token || !userTokens.refresh_token) { + return new Response(JSON.stringify({ message: 'User tokens not found or incomplete' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const oauth2Client = new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_CLIENT_SECRET + ); + oauth2Client.setCredentials({ + access_token: userTokens.access_token, + refresh_token: userTokens.refresh_token, + }); + + const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); + + const draftsResponse = await gmail.users.drafts.list({ + userId: 'me', + maxResults: 10, + }); + + if (!draftsResponse.data.drafts) { + return new Response(JSON.stringify({ drafts: [] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const drafts = await Promise.all( + draftsResponse.data.drafts.map(async (draft) => { + const draftData = await gmail.users.drafts.get({ + userId: 'me', + id: draft.id, + }); + + const decodeBase64 = (data) => Buffer.from(data || '', 'base64').toString('utf-8'); + + const getDraftBody = (msg) => { + let body = ''; + const parts = msg.payload.parts || [msg.payload]; + for (const part of parts) { + if (part.mimeType === 'text/html' && part.body?.data) { + body = decodeBase64(part.body.data); + break; + } else if (part.mimeType === 'text/plain' && part.body?.data) { + body = decodeBase64(part.body.data); + } + } + return body || 'No content available'; + }; + + const body = getDraftBody(draftData.data.message); + + return { + id: draftData.data.id, + subject: draftData.data.message.payload.headers.find((h) => h.name === 'Subject')?.value || '(No Subject)', + to: draftData.data.message.payload.headers.find((h) => h.name === 'To')?.value || '', + body, + snippet: draftData.data.message.snippet, + timestamp: new Date(parseInt(draftData.data.message.internalDate || Date.now())), + }; + }) + ); + + return new Response(JSON.stringify({ drafts }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error('Error fetching drafts:', error); + return new Response( + JSON.stringify({ + message: 'Internal server error', + details: error.message, + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/src/app/api/drafts/save/route.js b/src/app/api/drafts/save/route.js new file mode 100644 index 0000000..134ab4f --- /dev/null +++ b/src/app/api/drafts/save/route.js @@ -0,0 +1,107 @@ +import { google } from 'googleapis'; +import { getSession } from '@auth0/nextjs-auth0'; +import getUserTokens from '@/lib/getUserTokens'; + +export async function POST(req) { + try { + const body = await req.json(); // Parse the request body + const { to, subject, body: emailBody, userEmail, draftId } = body; + + if (!userEmail || !to || !subject || !emailBody) { + return new Response( + JSON.stringify({ message: 'Missing required fields' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Get user session + const session = await getSession(req); + if (!session || !session.user) { + return new Response( + JSON.stringify({ message: 'User not authenticated' }), + { status: 401, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Fetch user's tokens from the database + const userTokens = await getUserTokens(userEmail); + if (!userTokens || !userTokens.access_token || !userTokens.refresh_token) { + return new Response( + JSON.stringify({ message: 'User tokens not found or incomplete' }), + { status: 401, headers: { 'Content-Type': 'application/json' } } + ); + } + + // Initialize OAuth2 client + const oauth2Client = new google.auth.OAuth2( + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_CLIENT_SECRET + ); + oauth2Client.setCredentials({ + access_token: userTokens.access_token, + refresh_token: userTokens.refresh_token, + }); + + const gmail = google.gmail({ version: 'v1', auth: oauth2Client }); + + // Format email for draft + const rawEmail = [ + `To: ${to}`, + `Subject: ${subject}`, + 'Content-Type: text/html; charset=UTF-8', + '', + emailBody, + ].join('\n'); + + const encodedEmail = Buffer.from(rawEmail) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + + if (draftId) { + // Update existing draft + const response = await gmail.users.drafts.update({ + userId: 'me', + id: draftId, + requestBody: { + message: { + raw: encodedEmail, + }, + }, + }); + + return new Response( + JSON.stringify({ + message: 'Draft updated successfully', + draftId: response.data.id, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + } else { + // Create new draft + const response = await gmail.users.drafts.create({ + userId: 'me', + requestBody: { + message: { + raw: encodedEmail, + }, + }, + }); + + return new Response( + JSON.stringify({ + message: 'Draft saved successfully', + draftId: response.data.id, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + } + } catch (error) { + console.error('Error saving draft to Gmail:', error); + return new Response( + JSON.stringify({ message: 'Internal server error', error: error.message }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/src/app/dashboard/[slug]/components/Drafts.js b/src/app/dashboard/[slug]/components/Drafts.js new file mode 100644 index 0000000..2614c81 --- /dev/null +++ b/src/app/dashboard/[slug]/components/Drafts.js @@ -0,0 +1,104 @@ +// 'use client'; + +// import { useEffect, useState } from 'react'; + +// const Drafts = ({ email }) => { +// const [drafts, setDrafts] = useState([]); +// const [loading, setLoading] = useState(true); + +// const fetchDrafts = async () => { +// try { +// const response = await fetch(`/api/drafts/fetch?email=${encodeURIComponent(email)}`); +// const data = await response.json(); +// setDrafts(data.drafts || []); +// } catch (error) { +// console.error('Error fetching drafts:', error.message); +// } finally { +// setLoading(false); +// } +// }; + +// useEffect(() => { +// if (email) { +// fetchDrafts(); +// } +// }, [email]); + +// if (loading) return

Loading drafts...

; +// if (drafts.length === 0) return

No drafts found.

; + +// return ( +//
+//

Drafts

+// +//
+// ); +// }; + +// export default Drafts; + + + + + + + + + +'use client'; + +import { useEffect, useState } from 'react'; + +const Drafts = ({ email, onDraftClick }) => { + const [drafts, setDrafts] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchDrafts = async () => { + try { + const response = await fetch(`/api/drafts/fetch?email=${encodeURIComponent(email)}`); + const data = await response.json(); + setDrafts(data.drafts || []); + } catch (error) { + console.error('Error fetching drafts:', error.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (email) { + fetchDrafts(); + } + }, [email]); + + if (loading) return

Loading drafts...

; + if (drafts.length === 0) return

No drafts found.

; + + return ( +
+ +
+ ); +}; + +export default Drafts; diff --git a/src/app/dashboard/[slug]/page.js b/src/app/dashboard/[slug]/page.js index 2ce53d2..301292b 100644 --- a/src/app/dashboard/[slug]/page.js +++ b/src/app/dashboard/[slug]/page.js @@ -8,8 +8,7 @@ import MessageDetails from '../components/MessageDetails'; import Compose from '../components/Compose'; import { FaExclamationCircle, FaSearch } from 'react-icons/fa'; import Search from '../components/search'; - - +import Drafts from './components/Drafts'; const DashboardPage = () => { const router = useRouter(); @@ -25,6 +24,7 @@ const DashboardPage = () => { const [isComposeOpen, setIsComposeOpen] = useState(false); const [isClassifying, setIsClassifying] = useState(false); const [isBlocking, setIsBlocking] = useState(false); + const [composeData, setComposeData] = useState(null); // State to hold draft data for Compose useEffect(() => { if (!slug) { @@ -69,12 +69,6 @@ const DashboardPage = () => { } }; - - - - - - const classifyEmails = async (emails) => { const classifiedEmails = []; @@ -85,7 +79,6 @@ const DashboardPage = () => { body: truncateContent(email.snippet || '', 800), }; - // Log the email content for inspection console.log('Sending email for classification:', emailContent); const response = await fetch('/api/ai/email/classifyEmail', { @@ -114,16 +107,10 @@ const DashboardPage = () => { return classifiedEmails; }; - - // Utility function to truncate text to avoid token limit issues const truncateContent = (text, limit) => { return text.length > limit ? text.slice(0, limit - 1) + '…' : text; }; - - - - // Email labeling function reintroduced from the backup code const labelEmails = async () => { return await Promise.all( emails.map(async (email) => { @@ -176,14 +163,12 @@ const DashboardPage = () => { const handleOpenMessage = async (message) => { if (message && message.threadId) { - try { - // Mark the message as read if it hasn't been already if (!message.isRead) { await markAsRead(message.id); } - setSelectedMessage(message); // Set the selected message for display - await fetchThread(message.threadId); // Fetch the thread for the selected message + setSelectedMessage(message); + await fetchThread(message.threadId); } catch (error) { console.error('Error opening message:', error.message); } @@ -226,11 +211,7 @@ const DashboardPage = () => { setThreadMessages([]); }; - - const handleBackToInbox = () => { - setSearchMode(false); - setSearchQuery(''); setEmails([]); const label = getGmailLabel('inbox'); fetchEmails(label, user.email); @@ -241,9 +222,15 @@ const DashboardPage = () => { }; const closeComposeModal = () => { + setComposeData(null); // Clear draft data setIsComposeOpen(false); }; + const handleDraftClick = (draft) => { + setComposeData(draft); // Pass draft data to Compose + setIsComposeOpen(true); // Open the Compose modal + }; + const handleLoadMore = () => { const label = getGmailLabel(slug); if (user?.email && nextPageToken) { @@ -265,28 +252,24 @@ const DashboardPage = () => { ); } + const handleSearchSubmit = async (query) => { - setEmails([]); // Clear existing emails - setLoading(true); // Show loading state - const label = getGmailLabel(slug); // Use the appropriate label + setEmails([]); + setLoading(true); + const label = getGmailLabel(slug); try { - // Make API call to fetch emails based on the query const url = `/api/auth/google/fetchEmails?label=${label}&email=${encodeURIComponent(user?.email)}&query=${encodeURIComponent(query)}`; const response = await fetch(url); if (!response.ok) throw new Error('Error fetching emails'); const data = await response.json(); - setEmails(data.messages || []); // Update email list + setEmails(data.messages || []); } catch (error) { console.error('Error fetching emails:', error.message); } finally { - setLoading(false); // Hide loading state + setLoading(false); } }; - - - - return (
@@ -295,20 +278,15 @@ const DashboardPage = () => { {!selectedMessage && ( <> - {/* Add the Search Component */} { - console.log('Search query submitted:', query); // Debug search query - const label = getGmailLabel(slug); // Dynamically determine the label - fetchEmails(label, user?.email, query); // Pass the query + console.log('Search query submitted:', query); + const label = getGmailLabel(slug); + fetchEmails(label, user?.email, query); }} /> - - - -
@@ -336,8 +312,9 @@ const DashboardPage = () => { )} - - {loading && emails.length === 0 ? ( + {slug === 'drafts' ? ( + + ) : loading && emails.length === 0 ? (

Loading emails...

) : selectedMessage ? ( { key={email.id} className={`relative p-6 rounded-lg shadow hover:shadow-lg transition cursor-pointer ${email.isRead ? 'bg-gray-300 text-gray-900' - : 'bg-white text-gray-900' - } ${email.category === 'Spam' ? 'border border-red-500' : ''}`} + : 'bg-white text-gray-900'} ${email.category === 'Spam' ? 'border border-red-500' : ''}`} onClick={() => handleOpenMessage(email)} >

{email.subject || '(No Subject)'}

@@ -368,8 +344,7 @@ const DashboardPage = () => { {email.priority && ( )} @@ -389,7 +364,7 @@ const DashboardPage = () => { )}
- +
); }; diff --git a/src/app/dashboard/components/Compose.js b/src/app/dashboard/components/Compose.js index d1c7e5a..90fc4e4 100644 --- a/src/app/dashboard/components/Compose.js +++ b/src/app/dashboard/components/Compose.js @@ -1,1057 +1,164 @@ -// "use client"; + "use client"; -// import React, { useState, useRef, useEffect } from 'react'; -// import { FaBold, FaItalic, FaUnderline, FaListUl, FaListOl, FaAlignCenter, FaAlignLeft, FaAlignRight, FaHeading } from "react-icons/fa"; -// import DOMPurify from 'dompurify'; - -// const Compose = ({ isOpen, onClose, userEmail }) => { -// const [message, setMessage] = useState({ to: '', cc: '', bcc: '', subject: '' }); -// const [htmlInput, setHtmlInput] = useState(''); -// const [showHtmlEditor, setShowHtmlEditor] = useState(false); -// const editorRef = useRef(null); - -// const handleChange = (e) => { -// const { name, value } = e.target; -// setMessage((prevMessage) => ({ ...prevMessage, [name]: value })); -// }; - -// const validateEmailList = (emails) => { -// if (!emails) return true; -// const emailList = emails.split(',').map((email) => email.trim()); -// const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -// return emailList.every((email) => emailRegex.test(email)); -// }; - -// const decodeHtmlEntities = (str) => { -// const textArea = document.createElement("textarea"); -// textArea.innerHTML = str; -// return textArea.value; -// }; - -// const encodeHtmlEntities = (str) => { -// const textArea = document.createElement("textarea"); -// textArea.textContent = str; -// return textArea.innerHTML; -// }; - -// const handleHtmlInput = (e) => { -// const rawHtml = e.target.value; -// setHtmlInput(rawHtml); -// if (editorRef.current) { -// editorRef.current.innerHTML = DOMPurify.sanitize(decodeHtmlEntities(rawHtml)); -// } -// }; - -// useEffect(() => { -// if (editorRef.current) { -// editorRef.current.innerHTML = DOMPurify.sanitize(decodeHtmlEntities(htmlInput)); -// } -// }, [htmlInput]); - -// const handleEditorChange = () => { -// if (editorRef.current) { -// const rawHtml = editorRef.current.innerHTML; -// setHtmlInput(encodeHtmlEntities(rawHtml)); -// } -// }; - -// const applyFormat = (command, value = null) => { -// document.execCommand(command, false, value); -// }; - -// const handleSubmit = async (e) => { -// e.preventDefault(); -// const body = editorRef.current.innerHTML; - -// if (!validateEmailList(message.to) || !validateEmailList(message.cc) || !validateEmailList(message.bcc)) { -// alert('Please provide valid email addresses.'); -// return; -// } - -// console.log("Email Body:", body); -// alert('Email sent successfully!'); -// }; - -// if (!isOpen) return null; - -// return ( -//
-//
-//

Compose a Message

-//
-// -// -// -// - -//
-// -// -// -// -// -// -// -// -// -//
- -//
-//
- -// {showHtmlEditor && ( -//
-//