freealberta/influence/app/public/js/email-composer.js

407 lines
16 KiB
JavaScript

// Email Composer Module
class EmailComposer {
constructor() {
this.modal = document.getElementById('email-modal');
this.previewModal = document.getElementById('email-preview-modal');
this.form = document.getElementById('email-form');
this.closeBtn = document.getElementById('close-modal');
this.closePreviewBtn = document.getElementById('close-preview-modal');
this.cancelBtn = document.getElementById('cancel-email');
this.cancelPreviewBtn = document.getElementById('cancel-preview');
this.editBtn = document.getElementById('edit-email');
this.confirmSendBtn = document.getElementById('confirm-send');
this.messageTextarea = document.getElementById('email-message');
this.charCounter = document.querySelector('.char-counter');
this.currentRecipient = null;
this.currentEmailData = null;
this.lastPreviewTime = 0; // Track last preview request time
this.init();
}
init() {
// Modal controls
this.closeBtn.addEventListener('click', () => this.closeModal());
this.closePreviewBtn.addEventListener('click', () => this.closePreviewModal());
this.cancelBtn.addEventListener('click', () => this.closeModal());
this.cancelPreviewBtn.addEventListener('click', () => this.closePreviewModal());
this.editBtn.addEventListener('click', () => this.editEmail());
this.confirmSendBtn.addEventListener('click', () => this.confirmSend());
// Click outside modal to close
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) this.closeModal();
});
this.previewModal.addEventListener('click', (e) => {
if (e.target === this.previewModal) this.closePreviewModal();
});
// Form handling - now shows preview instead of sending directly
this.form.addEventListener('submit', (e) => this.handlePreview(e));
// Character counter
this.messageTextarea.addEventListener('input', () => this.updateCharCounter());
// Add event listener to sender name field to update subject dynamically
const senderNameField = document.getElementById('sender-name');
const subjectField = document.getElementById('email-subject');
const postalCodeField = document.getElementById('sender-postal-code');
senderNameField.addEventListener('input', () => {
const senderName = senderNameField.value.trim() || 'your constituent';
const postalCode = postalCodeField.value.trim();
if (postalCode) {
subjectField.value = `Message from ${senderName} from ${postalCode}`;
}
});
// Escape key to close modals
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (this.previewModal.style.display === 'block') {
this.closePreviewModal();
} else if (this.modal.style.display === 'block') {
this.closeModal();
}
}
});
}
openModal(recipient) {
this.currentRecipient = recipient;
// Populate recipient info
document.getElementById('recipient-email').value = recipient.email;
document.getElementById('recipient-info').innerHTML = `
<strong>${recipient.name}</strong><br>
${recipient.office}<br>
${recipient.district}<br>
<em>${recipient.email}</em>
`;
// Set postal code from current lookup
const postalCode = window.postalLookup ? window.postalLookup.currentPostalCode : '';
document.getElementById('sender-postal-code').value = postalCode;
// Clear form fields
document.getElementById('sender-name').value = '';
document.getElementById('sender-email').value = '';
document.getElementById('email-subject').value = '';
document.getElementById('email-message').value = '';
// Set default subject
document.getElementById('email-subject').value = `Message from your constituent from ${postalCode}`;
this.updateCharCounter();
this.modal.style.display = 'block';
// Focus on first input
document.getElementById('sender-name').focus();
}
closeModal() {
this.modal.style.display = 'none';
// Only clear data if we're not showing preview (user is canceling)
if (this.previewModal.style.display !== 'block') {
this.currentRecipient = null;
this.currentEmailData = null;
}
}
closePreviewModal() {
this.previewModal.style.display = 'none';
// Clear email data when closing preview (user canceling)
this.currentRecipient = null;
this.currentEmailData = null;
}
editEmail() {
// Close preview modal and return to compose modal without clearing data
this.previewModal.style.display = 'none';
this.modal.style.display = 'block';
}
updateCharCounter() {
const maxLength = 5000;
const currentLength = this.messageTextarea.value.length;
const remaining = maxLength - currentLength;
this.charCounter.textContent = `${remaining} characters remaining`;
if (remaining < 100) {
this.charCounter.style.color = '#dc3545'; // Red
} else if (remaining < 500) {
this.charCounter.style.color = '#ffc107'; // Yellow
} else {
this.charCounter.style.color = '#666'; // Default
}
}
validateForm() {
const errors = [];
const senderName = document.getElementById('sender-name').value.trim();
const senderEmail = document.getElementById('sender-email').value.trim();
const subject = document.getElementById('email-subject').value.trim();
const message = document.getElementById('email-message').value.trim();
if (!senderName) {
errors.push('Your name is required');
}
if (!senderEmail) {
errors.push('Your email is required');
} else if (!this.validateEmail(senderEmail)) {
errors.push('Please enter a valid email address');
}
if (!subject) {
errors.push('Subject is required');
}
if (!message) {
errors.push('Message is required');
} else if (message.length < 10) {
errors.push('Message must be at least 10 characters long');
}
// Check for suspicious content
if (this.containsSuspiciousContent(message) || this.containsSuspiciousContent(subject)) {
errors.push('Your message contains content that may not be appropriate');
}
return errors;
}
validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
containsSuspiciousContent(text) {
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i,
/<iframe/i,
/<object/i,
/<embed/i
];
return suspiciousPatterns.some(pattern => pattern.test(text));
}
async handlePreview(e) {
e.preventDefault();
// Prevent duplicate calls within 2 seconds
const currentTime = Date.now();
if (currentTime - this.lastPreviewTime < 2000) {
console.log('Preview request blocked - too soon since last request');
return;
}
this.lastPreviewTime = currentTime;
const errors = this.validateForm();
if (errors.length > 0) {
window.messageDisplay.show(errors.join('<br>'), 'error');
return;
}
const submitButton = this.form.querySelector('button[type="submit"]');
const originalText = submitButton.textContent;
try {
submitButton.disabled = true;
submitButton.textContent = 'Loading Preview...';
this.currentEmailData = {
recipientEmail: document.getElementById('recipient-email').value,
senderName: document.getElementById('sender-name').value.trim(),
senderEmail: document.getElementById('sender-email').value.trim(),
subject: document.getElementById('email-subject').value.trim(),
message: document.getElementById('email-message').value.trim(),
postalCode: document.getElementById('sender-postal-code').value,
recipientName: this.currentRecipient ? this.currentRecipient.name : null
};
const preview = await window.apiClient.previewEmail(this.currentEmailData);
if (preview.success) {
this.showPreview(preview.preview);
this.previewModal.style.display = 'block'; // Show preview modal first
this.closeModal(); // Close the compose modal
} else {
throw new Error(preview.message || 'Failed to generate preview');
}
} catch (error) {
console.error('Email preview failed:', error);
window.messageDisplay.show(`Failed to generate preview: ${error.message}`, 'error');
} finally {
submitButton.disabled = false;
submitButton.textContent = originalText;
}
}
showPreview(preview) {
// Populate preview modal with email details
document.getElementById('preview-recipient').textContent = preview.to;
document.getElementById('preview-sender').textContent = `${this.currentEmailData.senderName} <${this.currentEmailData.senderEmail}>`;
document.getElementById('preview-subject').textContent = preview.subject;
// Show the HTML preview content using iframe for complete isolation
const previewContent = document.getElementById('preview-content');
if (preview.html) {
// Use iframe to completely isolate the email HTML from the parent page
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.minHeight = '400px';
iframe.style.border = '1px solid #dee2e6';
iframe.style.borderRadius = '6px';
iframe.style.backgroundColor = '#ffffff';
iframe.sandbox = 'allow-same-origin'; // Safe sandbox settings
// Clear previous content and add iframe
previewContent.innerHTML = '';
previewContent.appendChild(iframe);
// Write the HTML content to the iframe
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
iframeDoc.open();
iframeDoc.write(preview.html);
iframeDoc.close();
// Auto-resize iframe to content height
iframe.onload = () => {
try {
const body = iframe.contentDocument.body;
const html = iframe.contentDocument.documentElement;
const height = Math.max(
body.scrollHeight,
body.offsetHeight,
html.clientHeight,
html.scrollHeight,
html.offsetHeight
);
iframe.style.height = Math.min(height + 20, 600) + 'px'; // Max height of 600px
} catch (e) {
// Fallback height if auto-resize fails
iframe.style.height = '400px';
}
};
} else if (preview.text) {
previewContent.innerHTML = `<pre style="white-space: pre-wrap; font-family: inherit; padding: 20px; background-color: #f8f9fa; border-radius: 6px; border: 1px solid #dee2e6;">${this.escapeHtml(preview.text)}</pre>`;
} else {
previewContent.innerHTML = '<p style="padding: 20px; text-align: center; color: #666;">No preview content available</p>';
}
}
// Escape HTML to prevent injection when showing text content
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async confirmSend() {
if (!this.currentEmailData) {
window.messageDisplay.show('No email data to send', 'error');
return;
}
const confirmButton = this.confirmSendBtn;
const originalText = confirmButton.textContent;
try {
confirmButton.disabled = true;
confirmButton.textContent = 'Sending...';
const result = await window.apiClient.sendEmail(this.currentEmailData);
if (result.success) {
window.messageDisplay.show('Email sent successfully! Your representative will receive your message.', 'success');
this.closePreviewModal();
this.currentEmailData = null;
} else {
throw new Error(result.message || 'Failed to send email');
}
} catch (error) {
console.error('Email send failed:', error);
// Handle rate limit errors specifically
if (error.status === 429 && error.data && error.data.rateLimitType === 'per-recipient') {
const retryMinutes = Math.ceil((error.data.retryAfter || 300) / 60);
window.messageDisplay.show(
`Rate limit reached: You can only send one email per representative every 5 minutes. Please wait ${retryMinutes} more minutes before sending another email to this representative. You can still send emails to other representatives.`,
'warning'
);
} else if (error.status === 429) {
// General rate limit
window.messageDisplay.show(
`Too many emails sent. Please wait before sending more emails.`,
'warning'
);
} else {
window.messageDisplay.show(`Failed to send email: ${error.message}`, 'error');
}
} finally {
confirmButton.disabled = false;
confirmButton.textContent = originalText;
}
}
// Helper method to get template messages
getTemplateMessage(type) {
const templates = {
general: `Dear {{name}},
I am writing as your constituent from {{postalCode}} to express my views on an important matter.
[Please write your message here]
I would appreciate your response on this issue and would like to know your position.
Thank you for your time and service to our community.
Sincerely,
{{senderName}}`,
concern: `Dear {{name}},
I am writing to express my concern about [specific issue] as your constituent from {{postalCode}}.
[Describe your concern and its impact]
I urge you to [specific action you want them to take].
Thank you for considering my views on this important matter.
Best regards,
{{senderName}}`,
support: `Dear {{name}},
I am writing to express my support for [specific issue/bill/policy] as your constituent from {{postalCode}}.
[Explain why you support this and its importance]
I encourage you to continue supporting this initiative.
Thank you for your leadership on this matter.
Respectfully,
{{senderName}}`
};
return templates[type] || templates.general;
}
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.emailComposer = new EmailComposer();
});