botserver/src/email/ui.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

614 lines
29 KiB
Rust

use axum::{
extract::{Path, State},
response::Html,
routing::get,
Router,
};
use std::sync::Arc;
use uuid::Uuid;
use crate::core::shared::state::AppState;
pub async fn handle_email_inbox_page(State(_state): State<Arc<AppState>>) -> Html<String> {
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Inbox</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; height: 100vh; display: flex; }
.sidebar { width: 240px; background: white; border-right: 1px solid #e0e0e0; padding: 16px; flex-shrink: 0; }
.sidebar h2 { font-size: 20px; margin-bottom: 20px; padding: 8px; }
.compose-btn { width: 100%; padding: 14px 20px; background: #0066cc; color: white; border: none; border-radius: 24px; font-size: 14px; font-weight: 500; cursor: pointer; margin-bottom: 20px; }
.compose-btn:hover { background: #0052a3; }
.nav-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-radius: 8px; cursor: pointer; color: #333; text-decoration: none; margin-bottom: 4px; }
.nav-item:hover { background: #f5f5f5; }
.nav-item.active { background: #e8f4ff; color: #0066cc; font-weight: 500; }
.nav-item .count { margin-left: auto; background: #e0e0e0; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
.nav-item.active .count { background: #0066cc; color: white; }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.toolbar { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: white; border-bottom: 1px solid #e0e0e0; }
.search-box { flex: 1; padding: 10px 16px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; }
.toolbar-btn { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; }
.email-list { flex: 1; overflow-y: auto; background: white; }
.email-item { display: flex; align-items: center; padding: 16px 20px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.15s; }
.email-item:hover { background: #f8f9fa; }
.email-item.unread { background: #f0f7ff; }
.email-item.unread:hover { background: #e8f4ff; }
.email-item.selected { background: #e3f2fd; }
.email-checkbox { margin-right: 16px; }
.email-star { margin-right: 12px; color: #ddd; cursor: pointer; font-size: 18px; }
.email-star.starred { color: #ffc107; }
.email-sender { width: 200px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.email-item.unread .email-sender { font-weight: 600; }
.email-content { flex: 1; display: flex; gap: 8px; overflow: hidden; }
.email-subject { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.email-item.unread .email-subject { font-weight: 600; }
.email-preview { color: #666; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.email-date { width: 100px; text-align: right; font-size: 13px; color: #666; flex-shrink: 0; }
.email-item.unread .email-date { font-weight: 500; color: #333; }
.empty-state { text-align: center; padding: 80px 24px; color: #666; }
.empty-state h3 { margin-bottom: 8px; color: #1a1a1a; }
.pagination { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; background: white; border-top: 1px solid #e0e0e0; font-size: 13px; color: #666; }
</style>
</head>
<body>
<div class="sidebar">
<h2>Mail</h2>
<button class="compose-btn" onclick="composeMail()">✏️ Compose</button>
<a href="/suite/email" class="nav-item active">
<span>📥</span> Inbox <span class="count" id="inboxCount">0</span>
</a>
<a href="/suite/email/starred" class="nav-item">
<span>⭐</span> Starred
</a>
<a href="/suite/email/sent" class="nav-item">
<span>📤</span> Sent
</a>
<a href="/suite/email/drafts" class="nav-item">
<span>📝</span> Drafts <span class="count" id="draftsCount">0</span>
</a>
<a href="/suite/email/archive" class="nav-item">
<span>📁</span> Archive
</a>
<a href="/suite/email/spam" class="nav-item">
<span>🚫</span> Spam
</a>
<a href="/suite/email/trash" class="nav-item">
<span>🗑️</span> Trash
</a>
</div>
<div class="main-content">
<div class="toolbar">
<input type="checkbox" id="selectAll" onclick="toggleSelectAll()">
<button class="toolbar-btn" onclick="archiveSelected()" title="Archive">📁</button>
<button class="toolbar-btn" onclick="deleteSelected()" title="Delete">🗑️</button>
<button class="toolbar-btn" onclick="markAsRead()" title="Mark as read">✉️</button>
<input type="text" class="search-box" placeholder="Search emails..." id="searchInput" oninput="searchEmails()">
<button class="toolbar-btn" onclick="refreshInbox()">🔄</button>
</div>
<div class="email-list" id="emailList">
<div class="empty-state">
<h3>Your inbox is empty</h3>
<p>Emails you receive will appear here</p>
</div>
</div>
<div class="pagination">
<span id="paginationInfo">0 emails</span>
<div>
<button class="toolbar-btn" onclick="prevPage()" id="prevBtn" disabled>← Prev</button>
<button class="toolbar-btn" onclick="nextPage()" id="nextBtn" disabled>Next →</button>
</div>
</div>
</div>
<script>
let emails = [];
let selectedEmails = new Set();
let currentPage = 1;
const pageSize = 50;
async function loadEmails() {
try {
const response = await fetch('/api/email/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folder: currentFolder })
});
const data = await response.json();
emails = data.emails || data || [];
renderEmails();
updateCounts();
} catch (e) {
console.error('Failed to load emails:', e);
}
}
function renderEmails() {
const list = document.getElementById('emailList');
if (!emails || emails.length === 0) {
list.innerHTML = '<div class="empty-state"><h3>Your inbox is empty</h3><p>Emails you receive will appear here</p></div>';
return;
}
list.innerHTML = emails.map(e => `
<div class="email-item ${e.is_read ? '' : 'unread'} ${selectedEmails.has(e.id) ? 'selected' : ''}" onclick="openEmail('${e.id}')">
<input type="checkbox" class="email-checkbox" ${selectedEmails.has(e.id) ? 'checked' : ''} onclick="event.stopPropagation(); toggleSelect('${e.id}')">
<span class="email-star ${e.is_starred ? 'starred' : ''}" onclick="event.stopPropagation(); toggleStar('${e.id}')">${e.is_starred ? '★' : '☆'}</span>
<div class="email-sender">${e.from_name || e.from_address}</div>
<div class="email-content">
<span class="email-subject">${e.subject || '(No subject)'}</span>
<span class="email-preview"> - ${e.preview || e.body_text || ''}</span>
</div>
<div class="email-date">${formatDate(e.received_at || e.created_at)}</div>
</div>
`).join('');
document.getElementById('paginationInfo').textContent = `${emails.length} emails`;
}
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
if (diff < 86400000) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
if (diff < 604800000) {
return date.toLocaleDateString([], { weekday: 'short' });
}
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
function updateCounts() {
const unread = emails.filter(e => !e.is_read).length;
document.getElementById('inboxCount').textContent = unread || '';
}
function openEmail(id) {
window.location = `/suite/email/${id}`;
}
function composeMail() {
window.location = '/suite/email/compose';
}
function toggleSelect(id) {
if (selectedEmails.has(id)) {
selectedEmails.delete(id);
} else {
selectedEmails.add(id);
}
renderEmails();
}
function toggleSelectAll() {
const selectAll = document.getElementById('selectAll').checked;
if (selectAll) {
emails.forEach(e => selectedEmails.add(e.id));
} else {
selectedEmails.clear();
}
renderEmails();
}
async function toggleStar(id) {
const email = emails.find(e => e.id === id);
if (email) {
email.is_starred = !email.is_starred;
await fetch(`/api/email/messages/${id}/star`, { method: 'POST' });
renderEmails();
}
}
async function archiveSelected() {
if (selectedEmails.size === 0) return;
for (const id of selectedEmails) {
await fetch(`/api/email/messages/${id}/archive`, { method: 'POST' });
}
selectedEmails.clear();
loadEmails();
}
async function deleteSelected() {
if (selectedEmails.size === 0) return;
if (!confirm(`Delete ${selectedEmails.size} email(s)?`)) return;
for (const id of selectedEmails) {
await fetch(`/api/email/messages/${id}`, { method: 'DELETE' });
}
selectedEmails.clear();
loadEmails();
}
async function markAsRead() {
if (selectedEmails.size === 0) return;
for (const id of selectedEmails) {
await fetch(`/api/email/messages/${id}/read`, { method: 'POST' });
}
loadEmails();
}
function searchEmails() {
const query = document.getElementById('searchInput').value.toLowerCase();
const filtered = emails.filter(e =>
(e.subject && e.subject.toLowerCase().includes(query)) ||
(e.from_name && e.from_name.toLowerCase().includes(query)) ||
(e.from_address && e.from_address.toLowerCase().includes(query)) ||
(e.body_text && e.body_text.toLowerCase().includes(query))
);
renderFilteredEmails(filtered);
}
function renderFilteredEmails(filtered) {
const list = document.getElementById('emailList');
if (!filtered || filtered.length === 0) {
list.innerHTML = '<div class="empty-state"><h3>No emails found</h3><p>Try a different search term</p></div>';
return;
}
list.innerHTML = filtered.map(e => `
<div class="email-item ${e.is_read ? '' : 'unread'}" onclick="openEmail('${e.id}')">
<input type="checkbox" class="email-checkbox" onclick="event.stopPropagation(); toggleSelect('${e.id}')">
<span class="email-star ${e.is_starred ? 'starred' : ''}" onclick="event.stopPropagation(); toggleStar('${e.id}')">${e.is_starred ? '★' : '☆'}</span>
<div class="email-sender">${e.from_name || e.from_address}</div>
<div class="email-content">
<span class="email-subject">${e.subject || '(No subject)'}</span>
<span class="email-preview"> - ${e.preview || ''}</span>
</div>
<div class="email-date">${formatDate(e.received_at)}</div>
</div>
`).join('');
}
function refreshInbox() {
loadEmails();
}
loadEmails();
</script>
</body>
</html>"#;
Html(html.to_string())
}
pub async fn handle_email_detail_page(
State(_state): State<Arc<AppState>>,
Path(email_id): Path<Uuid>,
) -> Html<String> {
let html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }}
.container {{ max-width: 900px; margin: 0 auto; padding: 24px; }}
.back-link {{ color: #0066cc; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; margin-bottom: 16px; }}
.email-card {{ background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }}
.email-header {{ padding: 24px; border-bottom: 1px solid #e0e0e0; }}
.email-subject {{ font-size: 24px; font-weight: 600; margin-bottom: 16px; }}
.email-meta {{ display: flex; align-items: flex-start; gap: 16px; }}
.sender-avatar {{ width: 48px; height: 48px; border-radius: 50%; background: #0066cc; color: white; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 500; flex-shrink: 0; }}
.sender-info {{ flex: 1; }}
.sender-name {{ font-weight: 600; font-size: 16px; }}
.sender-email {{ color: #666; font-size: 14px; }}
.email-date {{ color: #666; font-size: 14px; }}
.email-recipients {{ margin-top: 8px; font-size: 13px; color: #666; }}
.email-actions {{ display: flex; gap: 8px; }}
.action-btn {{ padding: 8px 16px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; font-size: 14px; }}
.action-btn:hover {{ background: #f5f5f5; }}
.action-btn.primary {{ background: #0066cc; color: white; border-color: #0066cc; }}
.action-btn.primary:hover {{ background: #0052a3; }}
.email-body {{ padding: 24px; line-height: 1.7; font-size: 15px; }}
.email-body p {{ margin-bottom: 16px; }}
.attachments {{ padding: 16px 24px; background: #f9f9f9; border-top: 1px solid #e0e0e0; }}
.attachments-title {{ font-weight: 600; margin-bottom: 12px; font-size: 14px; }}
.attachment {{ display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; background: white; border: 1px solid #ddd; border-radius: 6px; margin-right: 8px; margin-bottom: 8px; cursor: pointer; }}
.attachment:hover {{ background: #f5f5f5; }}
</style>
</head>
<body>
<div class="container">
<a href="/suite/email" class="back-link">← Back to Inbox</a>
<div class="email-card">
<div class="email-header">
<h1 class="email-subject" id="emailSubject">Loading...</h1>
<div class="email-meta">
<div class="sender-avatar" id="senderAvatar">?</div>
<div class="sender-info">
<div class="sender-name" id="senderName">Loading...</div>
<div class="sender-email" id="senderEmail"></div>
<div class="email-recipients" id="recipients"></div>
</div>
<div style="text-align: right;">
<div class="email-date" id="emailDate"></div>
<div class="email-actions" style="margin-top: 12px;">
<button class="action-btn primary" onclick="replyEmail()">↩️ Reply</button>
<button class="action-btn" onclick="forwardEmail()">↪️ Forward</button>
<button class="action-btn" onclick="deleteEmail()">🗑️ Delete</button>
</div>
</div>
</div>
</div>
<div class="email-body" id="emailBody">
<p>Loading email content...</p>
</div>
<div class="attachments" id="attachments" style="display: none;">
<div class="attachments-title">📎 Attachments</div>
<div id="attachmentsList"></div>
</div>
</div>
</div>
<script>
const emailId = '{email_id}';
async function loadEmail() {{
try {{
const response = await fetch(`/api/email/messages/${{emailId}}`);
const email = await response.json();
if (email) {{
document.getElementById('emailSubject').textContent = email.subject || '(No subject)';
document.getElementById('senderName').textContent = email.from_name || email.from_address;
document.getElementById('senderEmail').textContent = email.from_address ? `<${{email.from_address}}>` : '';
document.getElementById('senderAvatar').textContent = (email.from_name || email.from_address || '?')[0].toUpperCase();
document.getElementById('emailDate').textContent = email.received_at ? new Date(email.received_at).toLocaleString() : '';
if (email.to_addresses && email.to_addresses.length) {{
document.getElementById('recipients').textContent = `To: ${{email.to_addresses.join(', ')}}`;
}}
const body = email.body_html || email.body_text || 'No content';
document.getElementById('emailBody').innerHTML = email.body_html ? body : `<p>${{body.replace(/\\n/g, '</p><p>')}}</p>`;
if (email.attachments && email.attachments.length) {{
document.getElementById('attachments').style.display = 'block';
document.getElementById('attachmentsList').innerHTML = email.attachments.map(a => `
<div class="attachment" onclick="downloadAttachment('${{a.id}}')">
📄 ${{a.filename}} (${{formatSize(a.size)}})
</div>
`).join('');
}}
if (!email.is_read) {{
fetch(`/api/email/messages/${{emailId}}/read`, {{ method: 'POST' }});
}}
}}
}} catch (e) {{
console.error('Failed to load email:', e);
document.getElementById('emailBody').innerHTML = '<p>Failed to load email content</p>';
}}
}}
function formatSize(bytes) {{
if (!bytes) return '0 B';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
}}
function replyEmail() {{
window.location = `/suite/email/compose?reply=${{emailId}}`;
}}
function forwardEmail() {{
window.location = `/suite/email/compose?forward=${{emailId}}`;
}}
async function deleteEmail() {{
if (!confirm('Move this email to trash?')) return;
try {{
await fetch(`/api/email/messages/${{emailId}}`, {{ method: 'DELETE' }});
window.location = '/suite/email';
}} catch (e) {{
alert('Failed to delete email');
}}
}}
function downloadAttachment(attachmentId) {{
window.open(`/api/email/attachments/${{attachmentId}}/download`, '_blank');
}}
loadEmail();
</script>
</body>
</html>"#
);
Html(html)
}
pub async fn handle_email_compose_page(State(_state): State<Arc<AppState>>) -> Html<String> {
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compose Email</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 900px; margin: 0 auto; padding: 24px; }
.back-link { color: #0066cc; text-decoration: none; display: inline-flex; align-items: center; gap: 8px; margin-bottom: 16px; }
.compose-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }
.compose-header { padding: 16px 24px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
.compose-header h1 { font-size: 20px; }
.compose-form { padding: 0; }
.form-row { display: flex; align-items: center; border-bottom: 1px solid #e0e0e0; }
.form-label { width: 80px; padding: 12px 24px; font-weight: 500; color: #666; flex-shrink: 0; }
.form-input { flex: 1; padding: 12px 16px; border: none; font-size: 14px; outline: none; }
.form-input:focus { background: #f8fafc; }
.body-editor { padding: 24px; min-height: 400px; }
.body-editor textarea { width: 100%; min-height: 350px; border: none; font-size: 15px; line-height: 1.6; resize: vertical; outline: none; }
.compose-footer { padding: 16px 24px; border-top: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; background: #f9f9f9; }
.btn { padding: 10px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #0066cc; color: white; }
.btn-primary:hover { background: #0052a3; }
.btn-secondary { background: white; border: 1px solid #ddd; color: #333; }
.btn-secondary:hover { background: #f5f5f5; }
.footer-actions { display: flex; gap: 12px; }
.attachment-btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; background: white; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; }
.attachment-btn:hover { background: #f5f5f5; }
.attachments-list { padding: 0 24px 16px; display: flex; flex-wrap: wrap; gap: 8px; }
.attachment-item { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; background: #f5f5f5; border-radius: 4px; font-size: 13px; }
.attachment-remove { cursor: pointer; color: #999; }
.attachment-remove:hover { color: #c62828; }
</style>
</head>
<body>
<div class="container">
<a href="/suite/email" class="back-link">← Back to Inbox</a>
<div class="compose-card">
<div class="compose-header">
<h1>New Message</h1>
</div>
<form id="composeForm" class="compose-form">
<div class="form-row">
<label class="form-label">To</label>
<input type="text" class="form-input" id="toField" placeholder="Recipients" required>
</div>
<div class="form-row">
<label class="form-label">Cc</label>
<input type="text" class="form-input" id="ccField" placeholder="Cc recipients">
</div>
<div class="form-row">
<label class="form-label">Subject</label>
<input type="text" class="form-input" id="subjectField" placeholder="Subject">
</div>
<div class="attachments-list" id="attachmentsList"></div>
<div class="body-editor">
<textarea id="bodyField" placeholder="Write your message..."></textarea>
</div>
<div class="compose-footer">
<div>
<label class="attachment-btn">
📎 Attach files
<input type="file" id="attachmentInput" multiple style="display: none;">
</label>
</div>
<div class="footer-actions">
<button type="button" class="btn btn-secondary" onclick="saveDraft()">Save Draft</button>
<button type="button" class="btn btn-secondary" onclick="discardDraft()">Discard</button>
<button type="submit" class="btn btn-primary">Send</button>
</div>
</div>
</form>
</div>
</div>
<script>
let attachments = [];
document.getElementById('attachmentInput').addEventListener('change', (e) => {
for (const file of e.target.files) {
attachments.push(file);
}
renderAttachments();
});
function renderAttachments() {
const list = document.getElementById('attachmentsList');
list.innerHTML = attachments.map((f, i) => `
<div class="attachment-item">
📄 ${f.name}
<span class="attachment-remove" onclick="removeAttachment(${i})">✕</span>
</div>
`).join('');
}
function removeAttachment(index) {
attachments.splice(index, 1);
renderAttachments();
}
async function saveDraft() {
const data = {
to_addresses: document.getElementById('toField').value.split(',').map(e => e.trim()).filter(e => e),
cc_addresses: document.getElementById('ccField').value.split(',').map(e => e.trim()).filter(e => e),
subject: document.getElementById('subjectField').value,
body_text: document.getElementById('bodyField').value,
is_draft: true
};
try {
await fetch('/api/email/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
window.location = '/suite/email/drafts';
} catch (e) {
alert('Failed to save draft');
}
}
function discardDraft() {
if (document.getElementById('bodyField').value && !confirm('Discard this draft?')) return;
window.location = '/suite/email';
}
document.getElementById('composeForm').addEventListener('submit', async (e) => {
e.preventDefault();
const toField = document.getElementById('toField').value;
if (!toField.trim()) {
alert('Please enter at least one recipient');
return;
}
const data = {
to_addresses: toField.split(',').map(e => e.trim()).filter(e => e),
cc_addresses: document.getElementById('ccField').value.split(',').map(e => e.trim()).filter(e => e),
subject: document.getElementById('subjectField').value,
body_text: document.getElementById('bodyField').value,
is_draft: false
};
try {
await fetch('/api/email/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
window.location = '/suite/email';
} catch (e) {
alert('Failed to send email');
}
});
const params = new URLSearchParams(window.location.search);
if (params.get('reply') || params.get('forward')) {
loadReplyData(params.get('reply') || params.get('forward'), !!params.get('forward'));
}
async function loadReplyData(emailId, isForward) {
try {
const response = await fetch(`/api/email/messages/${emailId}`);
const email = await response.json();
if (email) {
if (!isForward) {
document.getElementById('toField').value = email.from_address || '';
document.getElementById('subjectField').value = 'Re: ' + (email.subject || '');
} else {
document.getElementById('subjectField').value = 'Fwd: ' + (email.subject || '');
}
const quote = `\n\n--- Original Message ---\nFrom: ${email.from_address}\nDate: ${new Date(email.received_at).toLocaleString()}\nSubject: ${email.subject}\n\n${email.body_text || ''}`;
document.getElementById('bodyField').value = quote;
}
} catch (e) {
console.error('Failed to load reply data:', e);
}
}
</script>
</body>
</html>"#;
Html(html.to_string())
}
pub fn configure_email_ui_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/suite/email", get(handle_email_inbox_page))
.route("/suite/email/compose", get(handle_email_compose_page))
.route("/suite/email/:id", get(handle_email_detail_page))
}