generated from Labyricorn/labyricorn-project-template
418 lines
17 KiB
JavaScript
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">×</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">×</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'
|
|
});
|
|
});
|
|
}
|
|
}
|