// Import Firebase functions import { sendEmail } from './firebase-config.js'; import { collection, addDoc, getDocs, deleteDoc, doc } from 'firebase/firestore'; import { db } from './firebase-config.js'; // Import EmailJS as simpler alternative import { initEmailJS, sendEmailViaEmailJS, isEmailJSConfigured, testEmailJS } from './emailjs-config.js'; // Check if Firebase is properly configured const isFirebaseConfigured = () => { try { return import.meta.env.VITE_FIREBASE_PROJECT_ID && import.meta.env.VITE_FIREBASE_PROJECT_ID !== "your-project-id"; } catch { return false; } }; // Mail Composer Application class MailComposer { constructor() { this.initializeElements(); this.attachEventListeners(); this.loadDrafts(); this.initializeEditor(); // Initialize EmailJS initEmailJS(); // Update service status this.updateServiceStatus(); } initializeElements() { this.form = document.getElementById('mailForm'); this.toField = document.getElementById('to'); this.ccField = document.getElementById('cc'); this.bccField = document.getElementById('bcc'); this.subjectField = document.getElementById('subject'); this.priorityField = document.getElementById('priority'); this.messageEditor = document.getElementById('message'); this.attachmentsField = document.getElementById('attachments'); this.attachmentList = document.getElementById('attachmentList'); this.sendBtn = document.getElementById('sendBtn'); this.saveDraftBtn = document.getElementById('saveDraftBtn'); this.clearBtn = document.getElementById('clearBtn'); this.testEmailJSBtn = document.getElementById('testEmailJSBtn'); this.messageStatus = document.getElementById('messageStatus'); this.draftsList = document.getElementById('draftsList'); this.charCount = document.getElementById('charCount'); this.emailServiceStatus = document.getElementById('emailServiceStatus'); // Toolbar buttons this.boldBtn = document.getElementById('boldBtn'); this.italicBtn = document.getElementById('italicBtn'); this.underlineBtn = document.getElementById('underlineBtn'); this.linkBtn = document.getElementById('linkBtn'); this.attachments = []; this.drafts = []; } updateServiceStatus() { const statusElement = this.emailServiceStatus.querySelector('.status-indicator'); if (isEmailJSConfigured()) { statusElement.textContent = 'โ EmailJS Active'; statusElement.className = 'status-indicator emailjs'; this.testEmailJSBtn.style.display = 'inline-block'; // Show test button } else if (isFirebaseConfigured()) { statusElement.textContent = '๐ฅ Firebase Active'; statusElement.className = 'status-indicator firebase'; this.testEmailJSBtn.style.display = 'none'; } else { statusElement.textContent = '๐ Demo Mode - Setup EmailJS for real sending'; statusElement.className = 'status-indicator demo'; this.testEmailJSBtn.style.display = 'none'; } } async testEmailJSConnection() { this.showMessage('๐งช Testing EmailJS connection...', 'warning'); try { const result = await testEmailJS(); if (result.success) { this.showMessage('โ EmailJS test successful! Configuration is working.', 'success'); } else { this.showMessage(`โ EmailJS test failed: ${result.error}`, 'error'); } } catch (error) { this.showMessage(`โ EmailJS test error: ${error.message}`, 'error'); } } attachEventListeners() { // Form submission this.form.addEventListener('submit', (e) => this.handleSubmit(e)); // Button events this.saveDraftBtn.addEventListener('click', () => this.saveDraft()); this.clearBtn.addEventListener('click', () => this.clearForm()); this.testEmailJSBtn.addEventListener('click', () => this.testEmailJSConnection()); // File upload this.attachmentsField.addEventListener('change', (e) => this.handleFileUpload(e)); // Message editor events this.messageEditor.addEventListener('input', () => this.updateCharCount()); this.messageEditor.addEventListener('keydown', (e) => this.handleEditorKeydown(e)); // Toolbar events this.boldBtn.addEventListener('click', () => this.formatText('bold')); this.italicBtn.addEventListener('click', () => this.formatText('italic')); this.underlineBtn.addEventListener('click', () => this.formatText('underline')); this.linkBtn.addEventListener('click', () => this.insertLink()); // Auto-save draft every 30 seconds setInterval(() => this.autoSaveDraft(), 30000); } initializeEditor() { // Set up the rich text editor this.messageEditor.addEventListener('mouseup', () => this.updateToolbarState()); this.messageEditor.addEventListener('keyup', () => this.updateToolbarState()); } handleSubmit(e) { e.preventDefault(); if (!this.validateForm()) { return; } this.sendEmail(); } validateForm() { const to = this.toField.value.trim(); const subject = this.subjectField.value.trim(); const message = this.messageEditor.textContent.trim(); if (!to) { this.showMessage('Please enter at least one recipient email address.', 'error'); this.toField.focus(); return false; } if (!this.validateEmails(to)) { this.showMessage('Please enter valid email addresses in the "To" field.', 'error'); this.toField.focus(); return false; } if (!subject) { this.showMessage('Please enter a subject for your email.', 'error'); this.subjectField.focus(); return false; } if (!message) { this.showMessage('Please enter a message body.', 'error'); this.messageEditor.focus(); return false; } return true; } validateEmails(emailString) { const emails = emailString.split(',').map(email => email.trim()); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emails.every(email => emailRegex.test(email)); } async sendEmail() { this.setLoading(true); try { const emailData = { to: this.toField.value.split(',').map(email => email.trim()), cc: this.ccField.value ? this.ccField.value.split(',').map(email => email.trim()) : [], bcc: this.bccField.value ? this.bccField.value.split(',').map(email => email.trim()) : [], subject: this.subjectField.value, message: this.messageEditor.innerHTML, priority: this.priorityField.value, attachments: this.attachments.map(file => ({ name: file.name, size: file.size, type: file.type })) }; // Try EmailJS first (simpler setup) if (isEmailJSConfigured()) { const result = await sendEmailViaEmailJS(emailData); this.showMessage('โ Email sent successfully via EmailJS!', 'success'); this.clearForm(); this.removeDraftIfExists(); return; } // Fallback to Firebase if configured if (isFirebaseConfigured()) { const result = await sendEmail(emailData); if (result.data.success) { this.showMessage('โ Email sent successfully via Firebase!', 'success'); this.clearForm(); this.removeDraftIfExists(); await this.logSentEmail(emailData); } else { throw new Error(result.data.error || 'Failed to send email'); } return; } // Demo mode if neither service is configured await this.simulateEmailSending(); this.showMessage('๐ง Demo mode: Email would be sent! Configure EmailJS or Firebase for real sending.', 'warning'); this.clearForm(); this.removeDraftIfExists(); } catch (error) { console.error('Error sending email:', error); // Provide more specific error messages if (error.message.includes('EmailJS')) { this.showMessage(`โ EmailJS Error: ${error.message}`, 'error'); } else if (error.code === 'functions/unauthenticated') { this.showMessage('โ Authentication required. Please check your Firebase configuration.', 'error'); } else if (error.code === 'functions/permission-denied') { this.showMessage('โ Permission denied. Please check your Firebase security rules.', 'error'); } else if (error.code === 'functions/unavailable') { this.showMessage('โ Email service temporarily unavailable. Please try again later.', 'error'); } else { this.showMessage(`โ Failed to send email: ${error.message}`, 'error'); } } finally { this.setLoading(false); } } async simulateEmailSending() { // Simulate network delay for demo mode return new Promise((resolve) => { setTimeout(() => { resolve(); }, 2000); }); } async logSentEmail(emailData) { try { await addDoc(collection(db, 'sentEmails'), { ...emailData, timestamp: new Date(), status: 'sent' }); } catch (error) { console.warn('Failed to log sent email:', error); } } async simulateEmailSending() { // Simulate network delay return new Promise((resolve, reject) => { setTimeout(() => { // Simulate 90% success rate if (Math.random() > 0.1) { resolve(); } else { reject(new Error('Network error')); } }, 2000); }); } setLoading(isLoading) { this.sendBtn.disabled = isLoading; this.sendBtn.classList.toggle('loading', isLoading); } formatText(command) { document.execCommand(command, false, null); this.messageEditor.focus(); this.updateToolbarState(); } insertLink() { const url = prompt('Enter the URL:'); if (url) { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const selectedText = range.toString(); const linkText = selectedText || url; const link = document.createElement('a'); link.href = url; link.textContent = linkText; link.target = '_blank'; range.deleteContents(); range.insertNode(link); // Move cursor after the link range.setStartAfter(link); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } } this.messageEditor.focus(); } updateToolbarState() { this.boldBtn.classList.toggle('active', document.queryCommandState('bold')); this.italicBtn.classList.toggle('active', document.queryCommandState('italic')); this.underlineBtn.classList.toggle('active', document.queryCommandState('underline')); } handleEditorKeydown(e) { // Handle keyboard shortcuts if (e.ctrlKey || e.metaKey) { switch(e.key) { case 'b': e.preventDefault(); this.formatText('bold'); break; case 'i': e.preventDefault(); this.formatText('italic'); break; case 'u': e.preventDefault(); this.formatText('underline'); break; case 's': e.preventDefault(); this.saveDraft(); break; } } } updateCharCount() { const count = this.messageEditor.textContent.length; this.charCount.textContent = count.toLocaleString(); } handleFileUpload(e) { const files = Array.from(e.target.files); files.forEach(file => { if (this.validateFile(file)) { this.attachments.push(file); this.addAttachmentToList(file); } }); // Clear the input so the same file can be selected again e.target.value = ''; } validateFile(file) { const maxSize = 10 * 1024 * 1024; // 10MB const allowedTypes = [ 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'image/jpeg', 'image/png', 'image/gif' ]; if (file.size > maxSize) { this.showMessage(`File "${file.name}" is too large. Maximum size is 10MB.`, 'warning'); return false; } if (!allowedTypes.includes(file.type)) { this.showMessage(`File type "${file.type}" is not allowed.`, 'warning'); return false; } return true; } addAttachmentToList(file) { const attachmentItem = document.createElement('div'); attachmentItem.className = 'attachment-item'; attachmentItem.innerHTML = ` ๐ ${file.name} (${this.formatFileSize(file.size)}) `; attachmentItem.querySelector('.attachment-remove').addEventListener('click', (e) => { this.removeAttachment(file.name); attachmentItem.remove(); }); this.attachmentList.appendChild(attachmentItem); } removeAttachment(filename) { this.attachments = this.attachments.filter(file => file.name !== filename); } formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } async saveDraft() { const draft = this.getDraftData(); if (!draft.to && !draft.subject && !draft.message) { this.showMessage('Nothing to save as draft.', 'warning'); return; } draft.id = Date.now().toString(); draft.timestamp = new Date().toISOString(); try { // Save to Firebase Firestore if configured if (isFirebaseConfigured()) { await addDoc(collection(db, 'drafts'), draft); this.showMessage('๐ Draft saved to cloud!', 'success'); } else { this.showMessage('๐ Draft saved locally! (Configure Firebase for cloud sync)', 'warning'); } // Also save locally as backup this.drafts.push(draft); this.saveDraftsToStorage(); this.renderDrafts(); } catch (error) { console.warn('Failed to save draft to Firebase, saving locally:', error); // Fallback to local storage this.drafts.push(draft); this.saveDraftsToStorage(); this.renderDrafts(); this.showMessage('๐ Draft saved locally!', 'warning'); } } async loadDraftsFromFirebase() { if (!isFirebaseConfigured()) { return; // Skip Firebase loading if not configured } try { const querySnapshot = await getDocs(collection(db, 'drafts')); const firebaseDrafts = []; querySnapshot.forEach((doc) => { firebaseDrafts.push({ firebaseId: doc.id, ...doc.data() }); }); // Merge with local drafts this.drafts = [...firebaseDrafts, ...this.drafts.filter(draft => !draft.firebaseId)]; this.renderDrafts(); } catch (error) { console.warn('Failed to load drafts from Firebase:', error); } } async deleteDraftFromFirebase(draft) { if (draft.firebaseId) { try { await deleteDoc(doc(db, 'drafts', draft.firebaseId)); } catch (error) { console.warn('Failed to delete draft from Firebase:', error); } } } autoSaveDraft() { const draft = this.getDraftData(); if (draft.to || draft.subject || draft.message) { // Check if there's already an auto-saved draft const existingDraftIndex = this.drafts.findIndex(d => d.isAutoSave); draft.id = existingDraftIndex >= 0 ? this.drafts[existingDraftIndex].id : Date.now().toString(); draft.timestamp = new Date().toISOString(); draft.isAutoSave = true; if (existingDraftIndex >= 0) { this.drafts[existingDraftIndex] = draft; } else { this.drafts.push(draft); } this.saveDraftsToStorage(); this.renderDrafts(); } } getDraftData() { return { to: this.toField.value, cc: this.ccField.value, bcc: this.bccField.value, subject: this.subjectField.value, priority: this.priorityField.value, message: this.messageEditor.innerHTML, attachments: this.attachments.map(file => ({ name: file.name, size: file.size, type: file.type })) }; } loadDraft(draft) { this.toField.value = draft.to || ''; this.ccField.value = draft.cc || ''; this.bccField.value = draft.bcc || ''; this.subjectField.value = draft.subject || ''; this.priorityField.value = draft.priority || 'normal'; this.messageEditor.innerHTML = draft.message || ''; // Note: File attachments cannot be restored for security reasons this.attachments = []; this.attachmentList.innerHTML = ''; if (draft.attachments && draft.attachments.length > 0) { this.showMessage('Note: File attachments from drafts cannot be restored for security reasons.', 'warning'); } this.updateCharCount(); this.showMessage('๐ Draft loaded successfully!', 'success'); } async deleteDraft(draftId) { const draftIndex = this.drafts.findIndex(draft => draft.id === draftId); if (draftIndex >= 0) { const draft = this.drafts[draftIndex]; // Delete from Firebase if it exists there await this.deleteDraftFromFirebase(draft); // Remove from local array this.drafts.splice(draftIndex, 1); this.saveDraftsToStorage(); this.renderDrafts(); this.showMessage('๐๏ธ Draft deleted.', 'success'); } } removeDraftIfExists() { // Remove auto-saved draft when email is sent this.drafts = this.drafts.filter(draft => !draft.isAutoSave); this.saveDraftsToStorage(); this.renderDrafts(); } saveDraftsToStorage() { try { localStorage.setItem('mailComposerDrafts', JSON.stringify(this.drafts)); } catch (error) { console.warn('Failed to save drafts to localStorage:', error); } } loadDrafts() { try { const savedDrafts = localStorage.getItem('mailComposerDrafts'); if (savedDrafts) { this.drafts = JSON.parse(savedDrafts); } // Also load drafts from Firebase this.loadDraftsFromFirebase(); this.renderDrafts(); } catch (error) { console.warn('Failed to load drafts from localStorage:', error); this.drafts = []; } } renderDrafts() { if (this.drafts.length === 0) { this.draftsList.innerHTML = '
No drafts saved yet
'; return; } const draftsHtml = this.drafts .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) .map(draft => { const preview = this.getTextFromHtml(draft.message).substring(0, 100); const date = new Date(draft.timestamp).toLocaleDateString(); const time = new Date(draft.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return `