Files
CyberSim-OS/src/js/apps/inlook.js
T

418 lines
17 KiB
JavaScript

/**
* CyberSim OS - Inlook Email Application
*/
export class InlookApp {
constructor({ windowManager, eventBus, notifications, scenario, onNavigateUrl, onOpenDoc, onFileDownloaded }) {
this.wm = windowManager;
this.eventBus = eventBus;
this.notifications = notifications;
this.scenario = scenario;
this.onNavigateUrl = onNavigateUrl;
this.onOpenDoc = onOpenDoc;
this.onFileDownloaded = onFileDownloaded;
this.currentFolder = 'inbox';
this.activeEmailId = null;
this.emails = JSON.parse(JSON.stringify(scenario.emails));
}
getIconSvg() {
return `<svg viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`;
}
launch() {
const win = this.wm.createWindow({
id: 'inlook',
title: 'Inlook Mail - NexaCore Workplace',
iconSvg: this.getIconSvg(),
width: 860,
height: 560,
bodyContent: this.renderShell()
});
this.bindEvents(win.bodyElement);
this.selectFirstEmail();
this.eventBus.emit('APP_OPENED', { target: 'inlook' });
}
renderShell() {
return `
<div class="inlook-container">
<div class="inlook-sidebar">
<button class="cs-btn cs-btn-primary cs-btn-sm" style="margin-bottom:8px;" id="inlook-compose-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
New Message
</button>
<div class="inlook-folder-item active" data-folder="inbox">
<span>📥 Inbox</span>
<span class="cs-badge cs-badge-info" id="inlook-unread-count">0</span>
</div>
<div class="inlook-folder-item" data-folder="sent">
<span>📤 Sent</span>
</div>
<div class="inlook-folder-item" data-folder="trash">
<span>🗑️ Trash</span>
</div>
</div>
<div class="inlook-list-pane" id="inlook-email-list"></div>
<div class="inlook-reading-pane" id="inlook-reading-pane">
<div style="padding:40px; text-align:center; color:#94a3b8; margin:auto;">
Select an email message to view its contents.
</div>
</div>
</div>
`;
}
bindEvents(container) {
container.querySelectorAll('.inlook-folder-item').forEach(el => {
el.addEventListener('click', () => {
container.querySelectorAll('.inlook-folder-item').forEach(f => f.classList.remove('active'));
el.classList.add('active');
this.currentFolder = el.dataset.folder;
this.renderEmailList();
});
});
const composeBtn = container.querySelector('#inlook-compose-btn');
if (composeBtn) {
composeBtn.addEventListener('click', () => {
this.notifications.show({
title: 'Inlook Mail',
body: 'Corporate policy restricts unassigned outgoing mail during initial shift orientation.',
type: 'info'
});
});
}
this.renderEmailList();
}
renderEmailList() {
const listEl = document.getElementById('inlook-email-list');
if (!listEl) return;
const filtered = this.emails.filter(e => e.folder === this.currentFolder);
const unread = this.emails.filter(e => e.folder === 'inbox' && e.unread).length;
const badge = document.getElementById('inlook-unread-count');
if (badge) badge.innerText = unread;
if (filtered.length === 0) {
listEl.innerHTML = `<div style="padding:20px; text-align:center; color:#94a3b8; font-size:12px;">No messages in ${this.currentFolder}</div>`;
return;
}
listEl.innerHTML = filtered.map(email => `
<div class="inlook-list-item ${email.unread ? 'unread' : ''} ${email.id === this.activeEmailId ? 'active' : ''}" data-email-id="${email.id}">
<div class="inlook-item-header">
<span class="inlook-item-sender">${email.sender.split('(')[0]}</span>
<span class="inlook-item-date">${email.date}</span>
</div>
<div class="inlook-item-subject">${email.subject}</div>
<div class="inlook-item-snippet">${email.body.replace(/<[^>]*>?/gm, '').substring(0, 48)}...</div>
</div>
`).join('');
listEl.querySelectorAll('.inlook-list-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.emailId;
this.openEmail(id);
});
});
}
selectFirstEmail() {
const inboxEmails = this.emails.filter(e => e.folder === 'inbox');
if (inboxEmails.length > 0) {
this.openEmail(inboxEmails[0].id);
}
}
openEmail(id) {
const email = this.emails.find(e => e.id === id);
if (!email) return;
this.activeEmailId = id;
email.unread = false;
this.renderEmailList();
this.eventBus.emit('EMAIL_OPENED', {
target: id,
details: { subject: email.subject, rfcSender: email.rfcSender, isThreat: !!email.isThreat }
});
const readingPane = document.getElementById('inlook-reading-pane');
if (!readingPane) return;
readingPane.innerHTML = `
<div class="inlook-toolbar">
<button class="cs-btn cs-btn-sm" id="btn-inlook-reply">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg> Reply
</button>
<button class="cs-btn cs-btn-danger cs-btn-sm" id="btn-inlook-report">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M3.6 9h16.8L12 21 3.6 9z"/></svg> Report Suspicious
</button>
<button class="cs-btn cs-btn-sm" id="btn-inlook-delete">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg> Delete
</button>
</div>
<div class="inlook-msg-header">
<div class="inlook-msg-subject">${email.subject}</div>
<div class="inlook-sender-row">
<div class="inlook-sender-box">
<div class="inlook-sender-avatar">${email.sender.charAt(0)}</div>
<div class="inlook-sender-details">
<div class="sender-name">${email.sender}</div>
<div class="sender-email">${email.rfcSender}</div>
</div>
<button class="inlook-inspect-btn" id="btn-inspect-header">Inspect Header</button>
</div>
<div style="font-size:11px; color:#94a3b8;">${email.date}</div>
</div>
${email.attachments && email.attachments.length > 0 ? `
<div class="inlook-attachments">
<span style="font-size:11px; font-weight:600; color:#64748b;">Attachments (${email.attachments.length}):</span>
${email.attachments.map(att => `
<div class="inlook-attachment-chip" data-att-name="${att.name}" data-att-size="${att.size}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
<span>${att.name}</span>
<span style="color:#94a3b8; font-size:10px;">(${att.size})</span>
</div>
`).join('')}
</div>
` : ''}
</div>
<div class="inlook-msg-body">
${email.body.replace(/\n/g, '<br>')}
</div>
`;
this.bindEmailActions(email);
}
bindEmailActions(email) {
const inspectBtn = document.getElementById('btn-inspect-header');
if (inspectBtn) {
inspectBtn.addEventListener('click', () => {
this.eventBus.emit('EMAIL_INSPECTED_SENDER', {
target: email.id,
details: { displaySender: email.sender, rfcSender: email.rfcSender }
});
const isDomainMismatch = email.rfcSender.includes('nexac0re-portal.com') || email.rfcSender.includes('apex-global-supplies.net');
alert(`--- Inlook RFC Header Inspector ---\n\nDisplay Name: ${email.sender}\nEnvelope RFC From: <${email.rfcSender}>\nAuthentication-Results: spf=pass (domain: ${email.rfcSender.split('@')[1]})\nReturn-Path: <bounce@${email.rfcSender.split('@')[1]}>\n\n${isDomainMismatch ? '⚠️ Notice: Envelope domain differs from standard corporate (@nexacore.internal).' : '✓ Envelope domain matches internal enterprise domain.'}`);
});
}
const links = document.querySelectorAll('.inlook-msg-link');
links.forEach(link => {
const actualUrl = link.dataset.url;
const displayText = link.dataset.display || link.innerText;
link.title = `Simulated Target: ${actualUrl}`;
link.addEventListener('mouseenter', () => {
this.eventBus.emit('EMAIL_LINK_HOVERED', {
target: email.id,
details: { displayText, actualUrl }
});
});
link.addEventListener('click', (e) => {
e.preventDefault();
this.eventBus.emit('EMAIL_LINK_CLICKED', {
target: email.id,
details: { displayText, actualUrl, isPhishing: actualUrl.includes('nexac0re-portal.com') }
});
if (this.onNavigateUrl) {
this.onNavigateUrl(actualUrl);
}
});
});
const attChips = document.querySelectorAll('.inlook-attachment-chip');
attChips.forEach(chip => {
chip.addEventListener('click', () => {
const attName = chip.dataset.attName;
const attSize = chip.dataset.attSize;
this.eventBus.emit('EMAIL_ATTACHMENT_OPENED', {
target: email.id,
details: { attachmentName: attName, attachmentSize: attSize }
});
if (this.onFileDownloaded) {
this.onFileDownloaded({
id: `down_${Date.now()}`,
name: attName,
type: 'archive',
folder: 'Downloads',
size: attSize,
date: new Date().toISOString().split('T')[0]
});
}
this.notifications.show({
title: 'Download Complete',
body: `File "${attName}" saved to Downloads folder.`,
type: 'warning'
});
});
});
const reportBtn = document.getElementById('btn-inlook-report');
if (reportBtn) {
reportBtn.addEventListener('click', () => {
this.openReportDialog(email);
});
}
const replyBtn = document.getElementById('btn-inlook-reply');
if (replyBtn) {
replyBtn.addEventListener('click', () => {
this.openReplyDialog(email);
});
}
const deleteBtn = document.getElementById('btn-inlook-delete');
if (deleteBtn) {
deleteBtn.addEventListener('click', () => {
email.folder = 'trash';
this.renderEmailList();
this.eventBus.emit('EMAIL_DELETED', {
target: email.id,
details: { subject: email.subject, wasThreat: !!email.isThreat }
});
this.selectFirstEmail();
this.notifications.show({
title: 'Inlook Mail',
body: 'Message moved to Trash.',
type: 'info'
});
});
}
}
openReportDialog(email) {
const modalOverlay = document.createElement('div');
modalOverlay.className = 'cs-modal-overlay';
modalOverlay.innerHTML = `
<div class="cs-modal">
<div class="cs-modal-header">
<div class="cs-modal-title">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M3.6 9h16.8L12 21 3.6 9z"/></svg>
Report Suspicious Message to SOC
</div>
<button class="cs-btn-win close" id="btn-close-modal">&times;</button>
</div>
<div class="cs-modal-body">
<p style="margin-bottom:12px;">You are about to report the following email to the NexaCore Security Operations Center:</p>
<div style="background:#f1f5f9; padding:10px; border-radius:4px; font-size:12px; margin-bottom:14px;">
<strong>Subject:</strong> ${email.subject}<br>
<strong>Sender:</strong> ${email.rfcSender}
</div>
<div class="cs-form-group">
<label class="cs-form-label">Select Primary Threat Reason:</label>
<select class="cs-select" id="report-reason">
<option value="spoofed_sender">Spoofed or lookalike sender domain</option>
<option value="deceptive_link">Deceptive / mismatched link destination</option>
<option value="suspicious_attachment">Unsolicited or executable attachment</option>
<option value="credential_harvest">Credential theft / artificial urgency</option>
<option value="general_spam">General unsolicited spam</option>
</select>
</div>
<div class="cs-form-group">
<label class="cs-form-label">Investigative Notes (Optional):</label>
<textarea class="cs-textarea" id="report-notes" rows="2" placeholder="e.g. Lookalike domain nexac0re-portal.com detected"></textarea>
</div>
</div>
<div class="cs-modal-footer">
<button class="cs-btn" id="btn-cancel-report">Cancel</button>
<button class="cs-btn cs-btn-danger" id="btn-submit-report">Submit Incident Report</button>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
const close = () => modalOverlay.remove();
modalOverlay.querySelector('#btn-close-modal').addEventListener('click', close);
modalOverlay.querySelector('#btn-cancel-report').addEventListener('click', close);
modalOverlay.querySelector('#btn-submit-report').addEventListener('click', () => {
const reason = modalOverlay.querySelector('#report-reason').value;
const notes = modalOverlay.querySelector('#report-notes').value;
this.eventBus.emit('EMAIL_REPORTED', {
target: email.id,
details: { reason, notes, isThreat: !!email.isThreat, threatId: email.threatId || null }
});
email.folder = 'trash';
this.renderEmailList();
this.selectFirstEmail();
close();
this.notifications.show({
title: 'Security Center Report Acknowledged',
body: `Incident report for "${email.subject.substring(0, 30)}..." received by SOC.`,
type: 'success'
});
});
}
openReplyDialog(email) {
const modalOverlay = document.createElement('div');
modalOverlay.className = 'cs-modal-overlay';
modalOverlay.innerHTML = `
<div class="cs-modal">
<div class="cs-modal-header">
<div class="cs-modal-title">Reply to: ${email.sender}</div>
<button class="cs-btn-win close" id="btn-close-reply">&times;</button>
</div>
<div class="cs-modal-body">
<div class="cs-form-group">
<label class="cs-form-label">To:</label>
<input type="text" class="cs-input" value="${email.rfcSender}" readonly>
</div>
<div class="cs-form-group">
<label class="cs-form-label">Subject:</label>
<input type="text" class="cs-input" value="Re: ${email.subject}" readonly>
</div>
<div class="cs-form-group">
<label class="cs-form-label">Response Message:</label>
<textarea class="cs-textarea" id="reply-text" rows="4" placeholder="Type your workplace response here..."></textarea>
</div>
</div>
<div class="cs-modal-footer">
<button class="cs-btn" id="btn-cancel-reply">Cancel</button>
<button class="cs-btn cs-btn-primary" id="btn-send-reply">Send Reply</button>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
const close = () => modalOverlay.remove();
modalOverlay.querySelector('#btn-close-reply').addEventListener('click', close);
modalOverlay.querySelector('#btn-cancel-reply').addEventListener('click', close);
modalOverlay.querySelector('#btn-send-reply').addEventListener('click', () => {
const text = modalOverlay.querySelector('#reply-text').value;
this.eventBus.emit('EMAIL_REPLIED', {
target: email.id,
details: { responseText: text, recipient: email.rfcSender }
});
close();
this.notifications.show({
title: 'Inlook Mail',
body: `Reply sent to ${email.sender.split('(')[0]}.`,
type: 'success'
});
});
}
}