Files
gowtham-darkseid 44ea4d2e64 feat: Complete Mail Composer with EmailJS integration
- Rich email composer with formatting tools (bold, italic, underline, links)
- Multi-recipient support (To, CC, BCC fields)
- File attachments with drag & drop support
- Priority levels and email validation
- Draft management with auto-save and cloud sync
- Real email sending via EmailJS integration
- Responsive design for mobile and desktop
- Comprehensive error handling and fallback modes
- Complete documentation and setup guides
- Firebase integration ready for advanced features

Features:
 Real email sending (EmailJS)
 Rich text editor
 File attachments
 Draft management
 Form validation
 Responsive UI
 Error handling
 Documentation
2025-09-09 21:17:46 +05:30

677 lines
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<span>📎 ${file.name} (${this.formatFileSize(file.size)})</span>
<button type="button" class="attachment-remove" data-filename="${file.name}">×</button>
`;
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 = '<p class="no-drafts">No drafts saved yet</p>';
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 `
<div class="draft-item" data-draft-id="${draft.id}">
<div class="draft-subject">${draft.subject || 'No Subject'} ${draft.isAutoSave ? '(Auto-saved)' : ''}</div>
<div class="draft-preview">${preview}${preview.length === 100 ? '...' : ''}</div>
<div class="draft-meta">
<span>To: ${draft.to || 'No recipient'}</span>
<span>${date} ${time}</span>
</div>
<div class="draft-actions">
<button type="button" class="draft-load" onclick="mailComposer.loadDraft(${JSON.stringify(draft).replace(/"/g, '&quot;')})">Load</button>
<button type="button" class="draft-delete" onclick="mailComposer.deleteDraft('${draft.id}')">Delete</button>
</div>
</div>
`;
})
.join('');
this.draftsList.innerHTML = draftsHtml;
}
getTextFromHtml(html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
}
clearForm() {
this.form.reset();
this.messageEditor.innerHTML = '';
this.attachments = [];
this.attachmentList.innerHTML = '';
this.updateCharCount();
this.hideMessage();
}
showMessage(message, type) {
this.messageStatus.textContent = message;
this.messageStatus.className = `message-status ${type}`;
// Auto-hide success and warning messages after 5 seconds
if (type === 'success' || type === 'warning') {
setTimeout(() => this.hideMessage(), 5000);
}
}
hideMessage() {
this.messageStatus.className = 'message-status';
}
}
// Initialize the application when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.mailComposer = new MailComposer();
});
// Prevent accidental page navigation when there's unsaved content
window.addEventListener('beforeunload', (e) => {
const composer = window.mailComposer;
if (composer) {
const draft = composer.getDraftData();
if (draft.to || draft.subject || draft.message) {
e.preventDefault();
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
}
}
});