mirror of
https://github.com/Gowtham-Darkseid/MailComposer.git
synced 2026-08-06 09:18:36 +02:00
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
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
# Firebase Configuration
|
||||||
|
# Copy this file to .env and fill in your actual values
|
||||||
|
|
||||||
|
# Firebase Project Configuration
|
||||||
|
VITE_FIREBASE_API_KEY=your-api-key-here
|
||||||
|
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
||||||
|
VITE_FIREBASE_PROJECT_ID=your-project-id
|
||||||
|
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
||||||
|
VITE_FIREBASE_MESSAGING_SENDER_ID=123456789
|
||||||
|
VITE_FIREBASE_APP_ID=your-app-id
|
||||||
|
|
||||||
|
# Email Configuration (for Firebase Functions)
|
||||||
|
# These should be set in Firebase Functions configuration
|
||||||
|
GMAIL_USER=your-email@gmail.com
|
||||||
|
GMAIL_PASSWORD=your-app-password
|
||||||
|
|
||||||
|
# Alternative email service configuration
|
||||||
|
# EMAIL_USER=your-email@example.com
|
||||||
|
# EMAIL_PASSWORD=your-password
|
||||||
|
# EMAIL_HOST=smtp.example.com
|
||||||
|
# EMAIL_PORT=587
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
.firebase/
|
||||||
|
firebase-debug.log
|
||||||
|
firebase-debug.*.log
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Quick Email Setup with EmailJS
|
||||||
|
|
||||||
|
## 🚀 Get Real Email Sending in 5 Minutes!
|
||||||
|
|
||||||
|
EmailJS is the fastest way to get your mail composer sending real emails without server setup.
|
||||||
|
|
||||||
|
## Step 1: Create EmailJS Account
|
||||||
|
|
||||||
|
1. Go to [https://www.emailjs.com/](https://www.emailjs.com/)
|
||||||
|
2. Sign up for a free account
|
||||||
|
3. Verify your email
|
||||||
|
|
||||||
|
## Step 2: Create Email Service
|
||||||
|
|
||||||
|
1. In EmailJS dashboard, go to **Email Services**
|
||||||
|
2. Click **Add New Service**
|
||||||
|
3. Choose your email provider:
|
||||||
|
- **Gmail** (easiest for personal use)
|
||||||
|
- **Outlook**
|
||||||
|
- **Yahoo**
|
||||||
|
- **Custom SMTP**
|
||||||
|
|
||||||
|
### For Gmail:
|
||||||
|
1. Select "Gmail"
|
||||||
|
2. Click "Connect Account"
|
||||||
|
3. Sign in with your Gmail account
|
||||||
|
4. Allow EmailJS access
|
||||||
|
5. Copy the **Service ID** (e.g., `service_abc123`)
|
||||||
|
|
||||||
|
## Step 3: Create Email Template
|
||||||
|
|
||||||
|
1. Go to **Email Templates**
|
||||||
|
2. Click **Create New Template**
|
||||||
|
3. Use this template content:
|
||||||
|
|
||||||
|
**Subject:**
|
||||||
|
```
|
||||||
|
{{subject}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Content:**
|
||||||
|
```html
|
||||||
|
<p><strong>From:</strong> {{from_name}}</p>
|
||||||
|
<p><strong>To:</strong> {{to_email}}</p>
|
||||||
|
<p><strong>CC:</strong> {{cc_email}}</p>
|
||||||
|
<p><strong>Priority:</strong> {{priority}}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{{message}}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p><em>Sent via Mail Composer App</em></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Save the template and copy the **Template ID** (e.g., `template_xyz789`)
|
||||||
|
|
||||||
|
## Step 4: Get Public Key
|
||||||
|
|
||||||
|
1. Go to **Account** → **General**
|
||||||
|
2. Copy your **Public Key** (e.g., `user_abcd1234`)
|
||||||
|
|
||||||
|
## Step 5: Configure Your App
|
||||||
|
|
||||||
|
1. Open `emailjs-config.js`
|
||||||
|
2. Replace the placeholder values:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const EMAILJS_CONFIG = {
|
||||||
|
SERVICE_ID: 'service_abc123', // Your Service ID
|
||||||
|
TEMPLATE_ID: 'template_xyz789', // Your Template ID
|
||||||
|
PUBLIC_KEY: 'user_abcd1234' // Your Public Key
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Test Email Sending
|
||||||
|
|
||||||
|
1. Start your app: `npm run dev`
|
||||||
|
2. Open http://localhost:5173
|
||||||
|
3. Compose and send a test email
|
||||||
|
4. Check your recipient's inbox!
|
||||||
|
|
||||||
|
## ✅ That's It!
|
||||||
|
|
||||||
|
Your mail composer can now send real emails!
|
||||||
|
|
||||||
|
### Free Plan Limits:
|
||||||
|
- **200 emails/month**
|
||||||
|
- **EmailJS branding** in emails
|
||||||
|
- **Basic support**
|
||||||
|
|
||||||
|
### Need More?
|
||||||
|
- Upgrade to paid plan for higher limits
|
||||||
|
- Or set up Firebase for unlimited sending
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues:
|
||||||
|
|
||||||
|
1. **"Service not found" error**
|
||||||
|
- Double-check your Service ID
|
||||||
|
- Make sure the service is active in EmailJS dashboard
|
||||||
|
|
||||||
|
2. **"Template not found" error**
|
||||||
|
- Verify your Template ID
|
||||||
|
- Ensure template is published
|
||||||
|
|
||||||
|
3. **"Invalid public key" error**
|
||||||
|
- Check your Public Key in Account settings
|
||||||
|
- Make sure it's the correct format
|
||||||
|
|
||||||
|
4. **Emails not delivered**
|
||||||
|
- Check spam/junk folder
|
||||||
|
- Verify recipient email addresses
|
||||||
|
- Check EmailJS dashboard for send logs
|
||||||
|
|
||||||
|
### Test Mode
|
||||||
|
If you haven't configured EmailJS yet, the app runs in demo mode with simulated sending.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Once EmailJS is working:
|
||||||
|
- **Customize the email template** with your branding
|
||||||
|
- **Add more email services** as backups
|
||||||
|
- **Set up Firebase** for advanced features (unlimited sending, analytics)
|
||||||
|
- **Add user authentication** for multi-user support
|
||||||
|
|
||||||
|
Happy emailing! 📧
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# 🔧 Fix EmailJS Template - "Recipients address is empty" Error
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
EmailJS is expecting specific variable names in your template that match what the app is sending.
|
||||||
|
|
||||||
|
## Solution: Update Your EmailJS Template
|
||||||
|
|
||||||
|
### 1. Go to EmailJS Dashboard
|
||||||
|
1. Visit [EmailJS Dashboard](https://dashboard.emailjs.com/)
|
||||||
|
2. Go to **Email Templates**
|
||||||
|
3. Click on your template (`template_nlh9jpe`)
|
||||||
|
|
||||||
|
### 2. Update Template Settings
|
||||||
|
|
||||||
|
**To Email Field:**
|
||||||
|
```
|
||||||
|
{{to_email}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Subject Field:**
|
||||||
|
```
|
||||||
|
{{subject}}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Content/Body Field:**
|
||||||
|
```html
|
||||||
|
<h3>New Email from Mail Composer</h3>
|
||||||
|
|
||||||
|
<p><strong>From:</strong> {{from_name}}</p>
|
||||||
|
<p><strong>To:</strong> {{to_email}}</p>
|
||||||
|
<p><strong>CC:</strong> {{cc_email}}</p>
|
||||||
|
<p><strong>BCC:</strong> {{bcc_email}}</p>
|
||||||
|
<p><strong>Priority:</strong> {{priority}}</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{{message}}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p><em>Sent via Mail Composer App</em></p>
|
||||||
|
<p><em>Reply to: {{reply_to}}</em></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Important Settings
|
||||||
|
|
||||||
|
Make sure these settings are correct in your template:
|
||||||
|
|
||||||
|
- **To Email**: `{{to_email}}`
|
||||||
|
- **From Name**: Your name or "Mail Composer"
|
||||||
|
- **From Email**: Your verified email address
|
||||||
|
- **Subject**: `{{subject}}`
|
||||||
|
|
||||||
|
### 4. Save Template
|
||||||
|
|
||||||
|
Click **Save** after making these changes.
|
||||||
|
|
||||||
|
## 🧪 Test Again
|
||||||
|
|
||||||
|
After updating the template:
|
||||||
|
|
||||||
|
1. **Refresh your mail composer app**
|
||||||
|
2. **Try sending a test email** to yourself
|
||||||
|
3. **Check for success message**: "Email sent successfully via EmailJS!"
|
||||||
|
|
||||||
|
## 🔍 Debug Tips
|
||||||
|
|
||||||
|
If it still doesn't work:
|
||||||
|
|
||||||
|
1. **Check browser console** (F12) for detailed error messages
|
||||||
|
2. **Verify email addresses** are valid
|
||||||
|
3. **Try sending to yourself first**
|
||||||
|
4. **Check EmailJS dashboard** for send logs
|
||||||
|
|
||||||
|
## ✅ Expected Result
|
||||||
|
|
||||||
|
After fixing the template, you should see:
|
||||||
|
- ✅ Success message in the app
|
||||||
|
- 📧 Email in recipient's inbox
|
||||||
|
- 📊 Successful send in EmailJS dashboard
|
||||||
|
|
||||||
|
Try updating your template and test again!
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 📧 Email Not Working? Quick Fix Guide
|
||||||
|
|
||||||
|
## Current Status: DEMO MODE
|
||||||
|
|
||||||
|
Your mail composer is currently running in **demo mode** because no email service is configured yet. When you click "Send Email", it simulates sending but doesn't actually send real emails.
|
||||||
|
|
||||||
|
## 🚀 Quick Fix - Get Real Email in 5 Minutes with EmailJS
|
||||||
|
|
||||||
|
### Option 1: EmailJS (Recommended - No Server Required)
|
||||||
|
|
||||||
|
1. **Go to [EmailJS.com](https://www.emailjs.com/)** and create a free account
|
||||||
|
|
||||||
|
2. **Set up Gmail service:**
|
||||||
|
- Dashboard → Email Services → Add New Service → Gmail
|
||||||
|
- Connect your Gmail account
|
||||||
|
- Copy the Service ID (e.g., `service_abc123`)
|
||||||
|
|
||||||
|
3. **Create email template:**
|
||||||
|
- Dashboard → Email Templates → Create New Template
|
||||||
|
- Copy this template content:
|
||||||
|
|
||||||
|
**Subject:** `{{subject}}`
|
||||||
|
|
||||||
|
**Body:**
|
||||||
|
```html
|
||||||
|
<p><strong>To:</strong> {{to_email}}</p>
|
||||||
|
<p><strong>CC:</strong> {{cc_email}}</p>
|
||||||
|
<p><strong>Priority:</strong> {{priority}}</p>
|
||||||
|
<hr>
|
||||||
|
<div>{{{message}}}</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Save and copy Template ID (e.g., `template_xyz789`)
|
||||||
|
|
||||||
|
4. **Get your Public Key:**
|
||||||
|
- Account → General → Copy Public Key (e.g., `user_abcd1234`)
|
||||||
|
|
||||||
|
5. **Configure the app:**
|
||||||
|
- Open `emailjs-config.js` in your editor
|
||||||
|
- Replace these values:
|
||||||
|
```javascript
|
||||||
|
const EMAILJS_CONFIG = {
|
||||||
|
SERVICE_ID: 'your-actual-service-id',
|
||||||
|
TEMPLATE_ID: 'your-actual-template-id',
|
||||||
|
PUBLIC_KEY: 'your-actual-public-key'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Test it!**
|
||||||
|
- Refresh the app (it should show "✅ EmailJS Active")
|
||||||
|
- Send a test email
|
||||||
|
- Check your recipient's inbox!
|
||||||
|
|
||||||
|
## 🔍 How to Tell If It's Working
|
||||||
|
|
||||||
|
**Demo Mode (not working):**
|
||||||
|
- Status shows: "🔄 Demo Mode"
|
||||||
|
- Sending shows warning message
|
||||||
|
- No real emails sent
|
||||||
|
|
||||||
|
**EmailJS Active (working):**
|
||||||
|
- Status shows: "✅ EmailJS Active"
|
||||||
|
- Real emails are sent
|
||||||
|
- Success message shows "via EmailJS"
|
||||||
|
|
||||||
|
## 📝 For Detailed Setup
|
||||||
|
|
||||||
|
- **EmailJS Setup:** See `EMAILJS_SETUP.md`
|
||||||
|
- **Firebase Setup:** See `FIREBASE_SETUP.md` (more complex but unlimited)
|
||||||
|
|
||||||
|
## 🆘 Still Not Working?
|
||||||
|
|
||||||
|
1. **Check browser console** (F12) for error messages
|
||||||
|
2. **Verify EmailJS credentials** in `emailjs-config.js`
|
||||||
|
3. **Check spam folder** for sent emails
|
||||||
|
4. **Try sending to yourself** first
|
||||||
|
|
||||||
|
## 💡 Tips
|
||||||
|
|
||||||
|
- **Free EmailJS:** 200 emails/month
|
||||||
|
- **Gmail works best** for personal use
|
||||||
|
- **Check recipient's spam folder**
|
||||||
|
- **Test with your own email first**
|
||||||
|
|
||||||
|
Ready to send real emails? Follow Option 1 above! 🚀
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
# Firebase Setup Guide for Mail Composer
|
||||||
|
|
||||||
|
This guide will help you set up Firebase for your Mail Composer application to send real emails.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A Google account
|
||||||
|
2. Node.js installed on your machine
|
||||||
|
3. Firebase CLI installed globally: `npm install -g firebase-tools`
|
||||||
|
|
||||||
|
## Step 1: Create a Firebase Project
|
||||||
|
|
||||||
|
1. Go to the [Firebase Console](https://console.firebase.google.com/)
|
||||||
|
2. Click "Add project"
|
||||||
|
3. Enter a project name (e.g., "mail-composer")
|
||||||
|
4. Enable Google Analytics (optional)
|
||||||
|
5. Create the project
|
||||||
|
|
||||||
|
## Step 2: Enable Firestore Database
|
||||||
|
|
||||||
|
1. In your Firebase project console, go to "Firestore Database"
|
||||||
|
2. Click "Create database"
|
||||||
|
3. Choose "Start in test mode" for now
|
||||||
|
4. Select a location for your database
|
||||||
|
|
||||||
|
## Step 3: Set up Firebase Functions
|
||||||
|
|
||||||
|
1. In the Firebase console, go to "Functions"
|
||||||
|
2. Click "Get started"
|
||||||
|
3. This will enable the Cloud Functions API
|
||||||
|
|
||||||
|
## Step 4: Configure Email Service
|
||||||
|
|
||||||
|
### Option A: Gmail (Recommended for development)
|
||||||
|
|
||||||
|
1. Enable 2-factor authentication on your Gmail account
|
||||||
|
2. Generate an App Password:
|
||||||
|
- Go to Google Account settings
|
||||||
|
- Security → 2-Step Verification → App passwords
|
||||||
|
- Generate a password for "Mail"
|
||||||
|
- Save this password securely
|
||||||
|
|
||||||
|
### Option B: Other Email Providers
|
||||||
|
|
||||||
|
Configure SMTP settings for your email provider (SendGrid, Mailgun, etc.)
|
||||||
|
|
||||||
|
## Step 5: Configure Your Local Environment
|
||||||
|
|
||||||
|
1. Copy the environment file:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Get your Firebase configuration:
|
||||||
|
- In Firebase console, go to Project Settings (gear icon)
|
||||||
|
- Scroll down to "Your apps"
|
||||||
|
- Click the web icon `</>`
|
||||||
|
- Register your app with a nickname
|
||||||
|
- Copy the config object values
|
||||||
|
|
||||||
|
3. Update your `.env` file with the Firebase config values:
|
||||||
|
```
|
||||||
|
VITE_FIREBASE_API_KEY=your-actual-api-key
|
||||||
|
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
|
||||||
|
VITE_FIREBASE_PROJECT_ID=your-actual-project-id
|
||||||
|
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
||||||
|
VITE_FIREBASE_MESSAGING_SENDER_ID=your-actual-sender-id
|
||||||
|
VITE_FIREBASE_APP_ID=your-actual-app-id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 6: Deploy Firebase Functions
|
||||||
|
|
||||||
|
1. Login to Firebase CLI:
|
||||||
|
```bash
|
||||||
|
firebase login
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Initialize Firebase in your project:
|
||||||
|
```bash
|
||||||
|
firebase init
|
||||||
|
```
|
||||||
|
- Select "Functions", "Firestore", and "Hosting"
|
||||||
|
- Choose your existing project
|
||||||
|
- Accept defaults for most options
|
||||||
|
|
||||||
|
3. Install function dependencies:
|
||||||
|
```bash
|
||||||
|
cd functions
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Set environment variables for functions:
|
||||||
|
```bash
|
||||||
|
firebase functions:config:set gmail.user="your-email@gmail.com"
|
||||||
|
firebase functions:config:set gmail.password="your-app-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Deploy functions:
|
||||||
|
```bash
|
||||||
|
firebase deploy --only functions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: Update Firestore Security Rules
|
||||||
|
|
||||||
|
1. Go to Firestore Database → Rules
|
||||||
|
2. Replace the rules with the content from `firestore.rules`
|
||||||
|
3. Publish the rules
|
||||||
|
|
||||||
|
## Step 8: Test the Application
|
||||||
|
|
||||||
|
1. Start your development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Open the application and try sending a test email
|
||||||
|
|
||||||
|
## Alternative Email Services
|
||||||
|
|
||||||
|
### SendGrid
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In functions/index.js, replace the transporter with:
|
||||||
|
const sgMail = require('@sendgrid/mail');
|
||||||
|
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mailgun
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In functions/index.js, replace the transporter with:
|
||||||
|
const formData = require('form-data');
|
||||||
|
const Mailgun = require('mailgun.js');
|
||||||
|
const mailgun = new Mailgun(formData);
|
||||||
|
const mg = mailgun.client({
|
||||||
|
username: 'api',
|
||||||
|
key: process.env.MAILGUN_API_KEY,
|
||||||
|
url: 'https://api.mailgun.net' // or 'https://api.eu.mailgun.net'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Never commit your `.env` file** - it's already in `.gitignore`
|
||||||
|
2. **Use Firebase Security Rules** to protect your Firestore data
|
||||||
|
3. **Implement user authentication** for production use
|
||||||
|
4. **Set rate limits** in Firebase Functions to prevent abuse
|
||||||
|
5. **Validate all inputs** on both client and server side
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **"Permission denied" errors**
|
||||||
|
- Check your Firestore security rules
|
||||||
|
- Ensure your Firebase project is properly configured
|
||||||
|
|
||||||
|
2. **Email sending fails**
|
||||||
|
- Verify your email credentials
|
||||||
|
- Check Firebase Functions logs: `firebase functions:log`
|
||||||
|
- Ensure "Less secure app access" is disabled and App Password is used
|
||||||
|
|
||||||
|
3. **"Function not found" errors**
|
||||||
|
- Ensure functions are deployed: `firebase deploy --only functions`
|
||||||
|
- Check function names match in your code
|
||||||
|
|
||||||
|
4. **CORS errors**
|
||||||
|
- Functions should handle CORS automatically
|
||||||
|
- Check browser developer console for specific errors
|
||||||
|
|
||||||
|
### Getting Help
|
||||||
|
|
||||||
|
- Check Firebase Functions logs: `firebase functions:log`
|
||||||
|
- View Firestore data in the Firebase console
|
||||||
|
- Check browser developer console for client-side errors
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
1. Build the application:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Deploy to Firebase Hosting:
|
||||||
|
```bash
|
||||||
|
firebase deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `https://your-project-id.web.app`
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Mail Composer
|
||||||
|
|
||||||
|
A modern, feature-rich email composer application built with vanilla JavaScript, HTML, CSS, and Firebase for real email sending capabilities.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### ✉️ Core Email Functionality
|
||||||
|
- **Real email sending** - Powered by Firebase Functions and Nodemailer
|
||||||
|
- **To, CC, BCC fields** - Support for multiple recipients
|
||||||
|
- **Subject line** - Required field validation
|
||||||
|
- **Priority levels** - Normal, High, Low priority options
|
||||||
|
- **Rich text editor** - Bold, italic, underline formatting
|
||||||
|
- **Link insertion** - Add clickable links to your emails
|
||||||
|
|
||||||
|
### 📎 Attachments
|
||||||
|
- **File upload support** - Drag and drop or click to select
|
||||||
|
- **File type validation** - PDF, DOC, DOCX, TXT, JPG, PNG, GIF
|
||||||
|
- **Size limits** - Maximum 10MB per file
|
||||||
|
- **Visual file list** - See attached files with remove options
|
||||||
|
|
||||||
|
### 💾 Draft Management
|
||||||
|
- **Cloud sync** - Drafts stored in Firebase Firestore
|
||||||
|
- **Manual save** - Save drafts manually anytime
|
||||||
|
- **Auto-save** - Automatic draft saving every 30 seconds
|
||||||
|
- **Draft loading** - Load and continue editing saved drafts
|
||||||
|
- **Draft deletion** - Remove unwanted drafts
|
||||||
|
- **Local backup** - Drafts also persist in localStorage
|
||||||
|
|
||||||
|
### 🔥 Firebase Integration
|
||||||
|
- **Real email sending** - Firebase Functions with Nodemailer
|
||||||
|
- **Cloud storage** - Firestore for drafts and email logs
|
||||||
|
- **Error handling** - Comprehensive error reporting
|
||||||
|
- **Email logging** - Track sent emails for analytics
|
||||||
|
- **Multiple providers** - Support for Gmail, SendGrid, Mailgun, etc.
|
||||||
|
|
||||||
|
### 🎨 User Experience
|
||||||
|
- **Responsive design** - Works on desktop and mobile devices
|
||||||
|
- **Modern UI** - Clean, professional interface
|
||||||
|
- **Real-time validation** - Instant feedback on form fields
|
||||||
|
- **Character counter** - Track message length
|
||||||
|
- **Keyboard shortcuts** - Ctrl+B (bold), Ctrl+I (italic), Ctrl+U (underline), Ctrl+S (save)
|
||||||
|
- **Loading states** - Visual feedback during operations
|
||||||
|
|
||||||
|
### 🔒 Data Safety
|
||||||
|
- **Form validation** - Prevents sending incomplete emails
|
||||||
|
- **Unsaved changes warning** - Alerts before leaving page with unsaved content
|
||||||
|
- **Error handling** - Graceful handling of failures
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js (v14 or higher)
|
||||||
|
- npm or yarn
|
||||||
|
- Firebase account (for email sending)
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
1. Clone or download this project
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set up Firebase (for email sending):
|
||||||
|
```bash
|
||||||
|
# Follow the detailed guide in FIREBASE_SETUP.md
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your Firebase configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Open your browser and navigate to the URL shown in the terminal (typically `http://localhost:5173`)
|
||||||
|
|
||||||
|
### Firebase Setup
|
||||||
|
|
||||||
|
For full email sending functionality, you need to set up Firebase. See the detailed [Firebase Setup Guide](./FIREBASE_SETUP.md) for step-by-step instructions.
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
|
Without Firebase setup, the application will work in development mode with simulated email sending for testing the UI and functionality.
|
||||||
|
|
||||||
|
### Building for Production
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The built files will be in the `dist` directory.
|
||||||
|
|
||||||
|
### Preview Production Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Composing an Email
|
||||||
|
|
||||||
|
1. **Fill in recipients** - Enter email addresses in To, CC, or BCC fields (separate multiple emails with commas)
|
||||||
|
2. **Add subject** - Enter a descriptive subject line
|
||||||
|
3. **Compose message** - Use the rich text editor to write your email
|
||||||
|
4. **Format text** - Use toolbar buttons or keyboard shortcuts for formatting
|
||||||
|
5. **Attach files** - Click the attachment area or drag files to attach them
|
||||||
|
6. **Set priority** - Choose email priority level
|
||||||
|
7. **Send or save** - Click Send to send immediately, or Save Draft to save for later
|
||||||
|
|
||||||
|
### Working with Drafts
|
||||||
|
|
||||||
|
- **Auto-save**: Drafts are automatically saved every 30 seconds while composing
|
||||||
|
- **Manual save**: Click "Save Draft" to save immediately
|
||||||
|
- **Load draft**: Click "Load" on any saved draft to continue editing
|
||||||
|
- **Delete draft**: Click "Delete" to remove unwanted drafts
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
- `Ctrl+B` / `Cmd+B` - Bold
|
||||||
|
- `Ctrl+I` / `Cmd+I` - Italic
|
||||||
|
- `Ctrl+U` / `Cmd+U` - Underline
|
||||||
|
- `Ctrl+S` / `Cmd+S` - Save draft
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Technologies Used
|
||||||
|
- **Frontend**: Vanilla JavaScript (ES6+), HTML5, CSS3
|
||||||
|
- **Backend**: Firebase Functions (Node.js)
|
||||||
|
- **Database**: Firebase Firestore
|
||||||
|
- **Email Service**: Nodemailer with Gmail/SMTP support
|
||||||
|
- **Build Tool**: Vite
|
||||||
|
- **Storage**: Firestore + localStorage for draft persistence
|
||||||
|
- **Styling**: CSS Custom Properties (CSS Variables)
|
||||||
|
- **Icons**: Unicode emojis and custom SVG
|
||||||
|
|
||||||
|
### Browser Support
|
||||||
|
- Chrome 60+
|
||||||
|
- Firefox 55+
|
||||||
|
- Safari 12+
|
||||||
|
- Edge 79+
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
├── index.html # Main HTML file
|
||||||
|
├── style.css # Styles and layout
|
||||||
|
├── main.js # Application logic
|
||||||
|
├── firebase-config.js # Firebase configuration
|
||||||
|
├── package.json # Dependencies and scripts
|
||||||
|
├── firebase.json # Firebase project configuration
|
||||||
|
├── firestore.rules # Firestore security rules
|
||||||
|
├── firestore.indexes.json # Firestore indexes
|
||||||
|
├── .env.example # Environment variables template
|
||||||
|
├── FIREBASE_SETUP.md # Firebase setup guide
|
||||||
|
├── functions/ # Firebase Functions
|
||||||
|
│ ├── index.js # Cloud Functions code
|
||||||
|
│ └── package.json # Functions dependencies
|
||||||
|
├── public/
|
||||||
|
│ └── vite.svg # App icon
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
Modify CSS custom properties in `style.css` to change colors, fonts, and layout:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary-color: #3b82f6;
|
||||||
|
--success-color: #10b981;
|
||||||
|
--error-color: #ef4444;
|
||||||
|
/* ... other variables */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Service Integration
|
||||||
|
|
||||||
|
The application now includes Firebase Functions for real email sending. Supported email services:
|
||||||
|
|
||||||
|
#### Gmail (Default)
|
||||||
|
```javascript
|
||||||
|
// Already configured in functions/index.js
|
||||||
|
// Just set your Gmail credentials in Firebase Functions config
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SendGrid
|
||||||
|
```javascript
|
||||||
|
// In functions/index.js, add SendGrid configuration:
|
||||||
|
const sgMail = require('@sendgrid/mail');
|
||||||
|
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mailgun
|
||||||
|
```javascript
|
||||||
|
// In functions/index.js, add Mailgun configuration:
|
||||||
|
const mailgun = require('mailgun-js');
|
||||||
|
const mg = mailgun({
|
||||||
|
apiKey: process.env.MAILGUN_API_KEY,
|
||||||
|
domain: process.env.MAILGUN_DOMAIN
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom SMTP
|
||||||
|
```javascript
|
||||||
|
// Modify the transporter in functions/index.js:
|
||||||
|
const transporter = nodemailer.createTransporter({
|
||||||
|
host: 'your-smtp-host.com',
|
||||||
|
port: 587,
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASSWORD
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Upload Limits
|
||||||
|
Modify file size and type restrictions in the `validateFile()` method:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/pdf',
|
||||||
|
// ... add more types
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is open source and available under the [MIT License](LICENSE).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter any issues or have questions, please:
|
||||||
|
|
||||||
|
1. Check the existing issues in the repository
|
||||||
|
2. Create a new issue with detailed information about the problem
|
||||||
|
3. Include steps to reproduce the issue
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Email templates
|
||||||
|
- [ ] Spell check integration
|
||||||
|
- [ ] Emoji picker
|
||||||
|
- [ ] Scheduled sending
|
||||||
|
- [ ] Email signatures
|
||||||
|
- [ ] Contact management
|
||||||
|
- [ ] Dark mode theme
|
||||||
|
- [ ] Offline support
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
# 📧 Mail Composer with Firebase Integration - Setup Complete!
|
||||||
|
|
||||||
|
## What Has Been Created
|
||||||
|
|
||||||
|
You now have a fully functional mail composer application with Firebase integration for real email sending. Here's what's included:
|
||||||
|
|
||||||
|
### 🎯 Core Features
|
||||||
|
- **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** (Normal, High, Low)
|
||||||
|
- **Draft management** with auto-save and cloud sync
|
||||||
|
- **Real email sending** via Firebase Functions
|
||||||
|
|
||||||
|
### 🔥 Firebase Integration
|
||||||
|
- **Cloud Functions** for secure email sending using Nodemailer
|
||||||
|
- **Firestore Database** for draft storage and email logging
|
||||||
|
- **Multiple email providers** support (Gmail, SendGrid, Mailgun, SMTP)
|
||||||
|
- **Error handling** and fallback to demo mode
|
||||||
|
- **Security rules** for data protection
|
||||||
|
|
||||||
|
### 📁 Project Structure
|
||||||
|
```
|
||||||
|
mail-composer/
|
||||||
|
├── 🌐 Frontend (Vite + Vanilla JS)
|
||||||
|
│ ├── index.html # Main application
|
||||||
|
│ ├── style.css # Modern responsive styling
|
||||||
|
│ ├── main.js # Application logic
|
||||||
|
│ └── firebase-config.js # Firebase configuration
|
||||||
|
├── 🔥 Firebase Integration
|
||||||
|
│ ├── functions/
|
||||||
|
│ │ ├── index.js # Email sending functions
|
||||||
|
│ │ └── package.json # Functions dependencies
|
||||||
|
│ ├── firebase.json # Firebase configuration
|
||||||
|
│ ├── firestore.rules # Database security
|
||||||
|
│ └── firestore.indexes.json # Database indexes
|
||||||
|
├── 📋 Documentation
|
||||||
|
│ ├── README.md # Complete usage guide
|
||||||
|
│ ├── FIREBASE_SETUP.md # Step-by-step Firebase setup
|
||||||
|
│ └── SUMMARY.md # This file
|
||||||
|
└── ⚙️ Configuration
|
||||||
|
├── package.json # Project dependencies
|
||||||
|
└── .env.example # Environment template
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Current Status
|
||||||
|
|
||||||
|
### ✅ Working Features (Demo Mode)
|
||||||
|
- **Email composition** with rich text editor
|
||||||
|
- **File attachments** (up to 10MB)
|
||||||
|
- **Form validation** and error handling
|
||||||
|
- **Draft management** (local storage)
|
||||||
|
- **Responsive design** for mobile and desktop
|
||||||
|
- **Demo email sending** with simulation
|
||||||
|
|
||||||
|
### 🔧 Ready for Firebase Setup
|
||||||
|
- **Firebase Functions** code ready for deployment
|
||||||
|
- **Environment configuration** prepared
|
||||||
|
- **Security rules** defined
|
||||||
|
- **Error handling** for production scenarios
|
||||||
|
|
||||||
|
## 🎯 Next Steps
|
||||||
|
|
||||||
|
### 1. Test the Current Application
|
||||||
|
```bash
|
||||||
|
# The dev server is already running at:
|
||||||
|
http://localhost:5173/
|
||||||
|
|
||||||
|
# Try the demo features:
|
||||||
|
# - Compose an email
|
||||||
|
# - Add attachments
|
||||||
|
# - Save/load drafts
|
||||||
|
# - Send email (demo mode)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Set Up Firebase (Optional - for real email sending)
|
||||||
|
```bash
|
||||||
|
# Follow the detailed guide:
|
||||||
|
cat FIREBASE_SETUP.md
|
||||||
|
|
||||||
|
# Key steps:
|
||||||
|
# 1. Create Firebase project
|
||||||
|
# 2. Enable Firestore and Functions
|
||||||
|
# 3. Configure email service (Gmail/SMTP)
|
||||||
|
# 4. Deploy functions
|
||||||
|
# 5. Update environment variables
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Customize for Your Needs
|
||||||
|
- **Styling**: Modify CSS variables in `style.css`
|
||||||
|
- **Email templates**: Add pre-defined templates
|
||||||
|
- **Authentication**: Add user login/registration
|
||||||
|
- **Contact management**: Add address book functionality
|
||||||
|
|
||||||
|
## 🎨 Customization Options
|
||||||
|
|
||||||
|
### Email Service Providers
|
||||||
|
- **Gmail** (default, requires App Password)
|
||||||
|
- **SendGrid** (commercial service, reliable)
|
||||||
|
- **Mailgun** (developer-friendly API)
|
||||||
|
- **Custom SMTP** (any email provider)
|
||||||
|
|
||||||
|
### Styling Themes
|
||||||
|
```css
|
||||||
|
/* Modify CSS variables for theming */
|
||||||
|
:root {
|
||||||
|
--primary-color: #3b82f6; /* Blue theme */
|
||||||
|
--success-color: #10b981; /* Green accents */
|
||||||
|
--error-color: #ef4444; /* Red for errors */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Features You Can Add
|
||||||
|
- 📧 Email templates library
|
||||||
|
- 👥 Contact management
|
||||||
|
- 📅 Scheduled sending
|
||||||
|
- 🌙 Dark mode theme
|
||||||
|
- 🔍 Email search and filtering
|
||||||
|
- 📊 Analytics dashboard
|
||||||
|
- 📱 Mobile app (PWA)
|
||||||
|
|
||||||
|
## 🔒 Security Features
|
||||||
|
|
||||||
|
- **Input validation** on both client and server
|
||||||
|
- **File type restrictions** for attachments
|
||||||
|
- **Size limits** to prevent abuse
|
||||||
|
- **CORS protection** in Firebase Functions
|
||||||
|
- **Firestore security rules** for data access
|
||||||
|
- **Environment variables** for sensitive config
|
||||||
|
|
||||||
|
## 📞 Support & Documentation
|
||||||
|
|
||||||
|
- **Complete setup guide**: See `FIREBASE_SETUP.md`
|
||||||
|
- **Usage instructions**: See `README.md`
|
||||||
|
- **Troubleshooting**: Check Firebase Console logs
|
||||||
|
- **Browser console**: For client-side debugging
|
||||||
|
|
||||||
|
## 🎉 Enjoy Your Mail Composer!
|
||||||
|
|
||||||
|
Your mail composer is ready to use! Start with the demo mode to test all features, then follow the Firebase setup guide when you're ready to send real emails.
|
||||||
|
|
||||||
|
**Demo Mode Features Available Now:**
|
||||||
|
- ✅ Full UI functionality
|
||||||
|
- ✅ Draft management (local)
|
||||||
|
- ✅ File attachments
|
||||||
|
- ✅ Form validation
|
||||||
|
- ✅ Rich text editing
|
||||||
|
- ✅ Responsive design
|
||||||
|
|
||||||
|
**Production Features After Firebase Setup:**
|
||||||
|
- 🔥 Real email sending
|
||||||
|
- ☁️ Cloud draft sync
|
||||||
|
- 📊 Email logging & analytics
|
||||||
|
- 🔒 Enhanced security
|
||||||
|
- 📈 Scalability
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// EmailJS Configuration for immediate email sending
|
||||||
|
import emailjs from '@emailjs/browser';
|
||||||
|
|
||||||
|
// EmailJS configuration
|
||||||
|
// You'll need to get these from https://www.emailjs.com/
|
||||||
|
const EMAILJS_CONFIG = {
|
||||||
|
SERVICE_ID: 'service_ec76mbk',
|
||||||
|
TEMPLATE_ID: 'template_nlh9jpe',
|
||||||
|
PUBLIC_KEY: '_Jp47GztsNMnAlijT'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize EmailJS
|
||||||
|
export const initEmailJS = () => {
|
||||||
|
if (EMAILJS_CONFIG.PUBLIC_KEY && EMAILJS_CONFIG.PUBLIC_KEY !== 'your_public_key') {
|
||||||
|
emailjs.init(EMAILJS_CONFIG.PUBLIC_KEY);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send email using EmailJS
|
||||||
|
export const sendEmailViaEmailJS = async (emailData) => {
|
||||||
|
try {
|
||||||
|
// Template parameters matching your EmailJS template exactly
|
||||||
|
const templateParams = {
|
||||||
|
to_email: emailData.to[0], // Single recipient for now
|
||||||
|
subject: emailData.subject,
|
||||||
|
message: emailData.message
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Sending with EmailJS:', {
|
||||||
|
serviceId: EMAILJS_CONFIG.SERVICE_ID,
|
||||||
|
templateId: EMAILJS_CONFIG.TEMPLATE_ID,
|
||||||
|
params: templateParams
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await emailjs.send(
|
||||||
|
EMAILJS_CONFIG.SERVICE_ID,
|
||||||
|
EMAILJS_CONFIG.TEMPLATE_ID,
|
||||||
|
templateParams,
|
||||||
|
EMAILJS_CONFIG.PUBLIC_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('EmailJS Success:', result);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('EmailJS Error Details:', error);
|
||||||
|
throw new Error(`EmailJS Error: ${error.text || error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if EmailJS is configured
|
||||||
|
export const isEmailJSConfigured = () => {
|
||||||
|
return EMAILJS_CONFIG.SERVICE_ID !== 'your_service_id' &&
|
||||||
|
EMAILJS_CONFIG.TEMPLATE_ID !== 'your_template_id' &&
|
||||||
|
EMAILJS_CONFIG.PUBLIC_KEY !== 'your_public_key';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test EmailJS configuration
|
||||||
|
export const testEmailJS = async () => {
|
||||||
|
try {
|
||||||
|
const testParams = {
|
||||||
|
to_email: 'test@example.com',
|
||||||
|
subject: 'Test Email from Mail Composer',
|
||||||
|
message: 'This is a test message to verify EmailJS is working correctly.'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Testing EmailJS with:', {
|
||||||
|
service: EMAILJS_CONFIG.SERVICE_ID,
|
||||||
|
template: EMAILJS_CONFIG.TEMPLATE_ID,
|
||||||
|
publicKey: EMAILJS_CONFIG.PUBLIC_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await emailjs.send(
|
||||||
|
EMAILJS_CONFIG.SERVICE_ID,
|
||||||
|
EMAILJS_CONFIG.TEMPLATE_ID,
|
||||||
|
testParams,
|
||||||
|
EMAILJS_CONFIG.PUBLIC_KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, result };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('EmailJS Test Failed:', error);
|
||||||
|
return { success: false, error: error.text || error.message };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Firebase configuration
|
||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getFirestore } from 'firebase/firestore';
|
||||||
|
import { getFunctions, httpsCallable } from 'firebase/functions';
|
||||||
|
|
||||||
|
// Your Firebase config object
|
||||||
|
// These values come from environment variables
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || "your-api-key",
|
||||||
|
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN || "your-project.firebaseapp.com",
|
||||||
|
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID || "your-project-id",
|
||||||
|
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET || "your-project.appspot.com",
|
||||||
|
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID || "123456789",
|
||||||
|
appId: import.meta.env.VITE_FIREBASE_APP_ID || "your-app-id"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Firebase
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
export const db = getFirestore(app);
|
||||||
|
export const functions = getFunctions(app);
|
||||||
|
|
||||||
|
// Email sending function
|
||||||
|
export const sendEmail = httpsCallable(functions, 'sendEmail');
|
||||||
|
|
||||||
|
// Test email configuration
|
||||||
|
export const testEmailConfig = httpsCallable(functions, 'testEmailConfig');
|
||||||
|
|
||||||
|
// Get email logs (for admin)
|
||||||
|
export const getEmailLogs = httpsCallable(functions, 'getEmailLogs');
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "your-project-id"
|
||||||
|
},
|
||||||
|
"targets": {},
|
||||||
|
"etags": {},
|
||||||
|
"hosting": {
|
||||||
|
"public": "dist",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"source": "functions",
|
||||||
|
"codebase": "default",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git",
|
||||||
|
"firebase-debug.log",
|
||||||
|
"firebase-debug.*.log"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"firestore": {
|
||||||
|
"rules": "firestore.rules",
|
||||||
|
"indexes": "firestore.indexes.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"indexes": [],
|
||||||
|
"fieldOverrides": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
rules_version = '2';
|
||||||
|
|
||||||
|
service cloud.firestore {
|
||||||
|
match /databases/{database}/documents {
|
||||||
|
// Allow read/write to drafts collection
|
||||||
|
match /drafts/{document} {
|
||||||
|
allow read, write: if true; // You can add authentication rules here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow read/write to sent emails for tracking
|
||||||
|
match /sentEmails/{document} {
|
||||||
|
allow read, write: if true; // You can add authentication rules here
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow read access to email logs for admin purposes
|
||||||
|
match /emailLogs/{document} {
|
||||||
|
allow read: if true; // You can add admin authentication here
|
||||||
|
allow write: if false; // Only functions should write to this collection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
const {onRequest, onCall} = require("firebase-functions/v2/https");
|
||||||
|
const {logger} = require("firebase-functions");
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const cors = require('cors')({origin: true});
|
||||||
|
|
||||||
|
// Initialize Firebase Admin
|
||||||
|
admin.initializeApp();
|
||||||
|
|
||||||
|
// Configure your email service (Gmail example)
|
||||||
|
// You'll need to enable "Less secure app access" or use App Passwords
|
||||||
|
const transporter = nodemailer.createTransporter({
|
||||||
|
service: 'gmail',
|
||||||
|
auth: {
|
||||||
|
user: process.env.GMAIL_USER, // Set in Firebase Functions config
|
||||||
|
pass: process.env.GMAIL_PASSWORD // Set in Firebase Functions config
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alternative configuration for other email services
|
||||||
|
/*
|
||||||
|
const transporter = nodemailer.createTransporter({
|
||||||
|
host: 'smtp.your-email-provider.com',
|
||||||
|
port: 587,
|
||||||
|
secure: false,
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASSWORD
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Send Email Cloud Function
|
||||||
|
exports.sendEmail = onCall(async (request) => {
|
||||||
|
const data = request.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validate required fields
|
||||||
|
if (!data.to || !data.subject || !data.message) {
|
||||||
|
throw new Error('Missing required fields: to, subject, or message');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email addresses
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
const validateEmails = (emails) => {
|
||||||
|
return emails.every(email => emailRegex.test(email.trim()));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!validateEmails(data.to)) {
|
||||||
|
throw new Error('Invalid email addresses in "to" field');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.cc && !validateEmails(data.cc)) {
|
||||||
|
throw new Error('Invalid email addresses in "cc" field');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.bcc && !validateEmails(data.bcc)) {
|
||||||
|
throw new Error('Invalid email addresses in "bcc" field');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare email options
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.GMAIL_USER || 'your-email@gmail.com',
|
||||||
|
to: data.to.join(', '),
|
||||||
|
subject: data.subject,
|
||||||
|
html: data.message,
|
||||||
|
priority: data.priority || 'normal'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add CC and BCC if provided
|
||||||
|
if (data.cc && data.cc.length > 0) {
|
||||||
|
mailOptions.cc = data.cc.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.bcc && data.bcc.length > 0) {
|
||||||
|
mailOptions.bcc = data.bcc.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set priority header
|
||||||
|
if (data.priority === 'high') {
|
||||||
|
mailOptions.priority = 'high';
|
||||||
|
mailOptions.headers = { 'X-Priority': '1' };
|
||||||
|
} else if (data.priority === 'low') {
|
||||||
|
mailOptions.priority = 'low';
|
||||||
|
mailOptions.headers = { 'X-Priority': '5' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the email
|
||||||
|
const info = await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
logger.info('Email sent successfully:', info.messageId);
|
||||||
|
|
||||||
|
// Log to Firestore for tracking
|
||||||
|
await admin.firestore().collection('emailLogs').add({
|
||||||
|
to: data.to,
|
||||||
|
cc: data.cc || [],
|
||||||
|
bcc: data.bcc || [],
|
||||||
|
subject: data.subject,
|
||||||
|
messageId: info.messageId,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
status: 'sent'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageId: info.messageId,
|
||||||
|
message: 'Email sent successfully'
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error sending email:', error);
|
||||||
|
|
||||||
|
// Log failed attempt
|
||||||
|
await admin.firestore().collection('emailLogs').add({
|
||||||
|
to: data.to,
|
||||||
|
subject: data.subject,
|
||||||
|
error: error.message,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
status: 'failed'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get Email Logs (for admin purposes)
|
||||||
|
exports.getEmailLogs = onCall(async (request) => {
|
||||||
|
try {
|
||||||
|
const snapshot = await admin.firestore()
|
||||||
|
.collection('emailLogs')
|
||||||
|
.orderBy('timestamp', 'desc')
|
||||||
|
.limit(50)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
snapshot.forEach(doc => {
|
||||||
|
logs.push({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, logs };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting email logs:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
exports.healthCheck = onRequest(async (req, res) => {
|
||||||
|
cors(req, res, () => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'Mail Composer Functions'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test email configuration
|
||||||
|
exports.testEmailConfig = onCall(async (request) => {
|
||||||
|
try {
|
||||||
|
// Verify transporter configuration
|
||||||
|
await transporter.verify();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Email configuration is valid'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Email configuration test failed:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "functions",
|
||||||
|
"description": "Cloud Functions for Firebase",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "firebase emulators:start --only functions",
|
||||||
|
"shell": "firebase functions:shell",
|
||||||
|
"start": "npm run shell",
|
||||||
|
"deploy": "firebase deploy --only functions",
|
||||||
|
"logs": "firebase functions:log"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"firebase-admin": "^12.1.0",
|
||||||
|
"firebase-functions": "^5.0.0",
|
||||||
|
"nodemailer": "^6.9.0",
|
||||||
|
"cors": "^2.8.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"firebase-functions-test": "^3.1.0"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Mail Composer</title>
|
||||||
|
<link rel="stylesheet" href="./style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header class="header">
|
||||||
|
<h1>📧 Mail Composer</h1>
|
||||||
|
<p>Compose and send emails easily</p>
|
||||||
|
<div id="emailServiceStatus" class="service-status">
|
||||||
|
<span class="status-indicator demo">🔄 Demo Mode</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-container">
|
||||||
|
<form id="mailForm" class="mail-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="to">To:</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="to"
|
||||||
|
name="to"
|
||||||
|
placeholder="recipient@example.com"
|
||||||
|
required
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
<small class="help-text">Separate multiple emails with commas</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cc">CC:</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="cc"
|
||||||
|
name="cc"
|
||||||
|
placeholder="cc@example.com (optional)"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bcc">BCC:</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="bcc"
|
||||||
|
name="bcc"
|
||||||
|
placeholder="bcc@example.com (optional)"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subject">Subject:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="subject"
|
||||||
|
name="subject"
|
||||||
|
placeholder="Enter email subject"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="priority">Priority:</label>
|
||||||
|
<select id="priority" name="priority">
|
||||||
|
<option value="normal">Normal</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message">Message:</label>
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button type="button" id="boldBtn" class="toolbar-btn" title="Bold">
|
||||||
|
<strong>B</strong>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="italicBtn" class="toolbar-btn" title="Italic">
|
||||||
|
<em>I</em>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="underlineBtn" class="toolbar-btn" title="Underline">
|
||||||
|
<u>U</u>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="linkBtn" class="toolbar-btn" title="Insert Link">
|
||||||
|
🔗
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="message"
|
||||||
|
class="message-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
data-placeholder="Type your message here..."
|
||||||
|
></div>
|
||||||
|
<div class="char-counter">
|
||||||
|
<span id="charCount">0</span> characters
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="attachments">Attachments:</label>
|
||||||
|
<div class="file-input-wrapper">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="attachments"
|
||||||
|
name="attachments"
|
||||||
|
multiple
|
||||||
|
accept=".pdf,.doc,.docx,.txt,.jpg,.png,.gif"
|
||||||
|
/>
|
||||||
|
<label for="attachments" class="file-input-label">
|
||||||
|
📎 Choose Files
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="attachmentList" class="attachment-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" id="sendBtn">
|
||||||
|
<span class="btn-text">📤 Send Email</span>
|
||||||
|
<span class="btn-loading" style="display: none;">⏳ Sending...</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="saveDraftBtn">
|
||||||
|
💾 Save Draft
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="clearBtn">
|
||||||
|
🗑️ Clear All
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="testEmailJSBtn" style="display: none;">
|
||||||
|
🧪 Test EmailJS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="messageStatus" class="message-status"></div>
|
||||||
|
|
||||||
|
<!-- Drafts Section -->
|
||||||
|
<section class="drafts-section">
|
||||||
|
<h2>📝 Saved Drafts</h2>
|
||||||
|
<div id="draftsList" class="drafts-list">
|
||||||
|
<p class="no-drafts">No drafts saved yet</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,676 @@
|
|||||||
|
// 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, '"')})">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?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Generated
+1997
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "mail-composer",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emailjs/browser": "^4.4.1",
|
||||||
|
"emailjs-com": "^3.2.0",
|
||||||
|
"firebase": "^12.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||||
|
<polyline points="22,6 12,13 2,6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1,480 @@
|
|||||||
|
:root {
|
||||||
|
--primary-color: #3b82f6;
|
||||||
|
--primary-hover: #2563eb;
|
||||||
|
--secondary-color: #6b7280;
|
||||||
|
--success-color: #10b981;
|
||||||
|
--error-color: #ef4444;
|
||||||
|
--warning-color: #f59e0b;
|
||||||
|
--background: #f8fafc;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--text-primary: #1f2937;
|
||||||
|
--text-secondary: #6b7280;
|
||||||
|
--border-color: #d1d5db;
|
||||||
|
--border-radius: 8px;
|
||||||
|
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: var(--background);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-status {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.demo {
|
||||||
|
background: rgba(245, 158, 11, 0.2);
|
||||||
|
color: #f59e0b;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.emailjs {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
color: #10b981;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.firebase {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--background);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-editor {
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-editor:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-editor:empty:before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-counter {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-wrapper input[type="file"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--background);
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-label:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgb(59 130 246 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-list {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--background);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-remove {
|
||||||
|
background: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.loading .btn-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.loading .btn-loading {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
font-weight: 500;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.success {
|
||||||
|
background: rgb(16 185 129 / 0.1);
|
||||||
|
color: var(--success-color);
|
||||||
|
border: 1px solid rgb(16 185 129 / 0.2);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.error {
|
||||||
|
background: rgb(239 68 68 / 0.1);
|
||||||
|
color: var(--error-color);
|
||||||
|
border: 1px solid rgb(239 68 68 / 0.2);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-status.warning {
|
||||||
|
background: rgb(245 158 11 / 0.1);
|
||||||
|
color: var(--warning-color);
|
||||||
|
border: 1px solid rgb(245 158 11 / 0.2);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drafts-section {
|
||||||
|
margin-top: 3rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drafts-section h2 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drafts-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-item {
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--background);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-item:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-subject {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-preview {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-actions button {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-load {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-delete {
|
||||||
|
background: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-drafts {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#app {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for better UX */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-form {
|
||||||
|
animation: fadeIn 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-item {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user