trash-cleaner
Version:
Finds and deletes trash email in the mailbox
192 lines (173 loc) • 6.93 kB
text/typescript
import fs from 'fs';
import path from 'path';
import { ProgressReporter } from './progress-reporter.js';
import { Email } from '../client/email-client.js';
const DEFAULT_OUTPUT_FILE = 'trash-cleaner-report.html';
/**
* A progress reporter that generates an HTML report file.
*/
class HtmlProgressReporter extends ProgressReporter {
private _outputPath: string;
private _trashEmails: Email[] = [];
private _unreadEmailCount: number = 0;
private _dryRun: boolean = false;
/**
* Creates an instance of HtmlProgressReporter.
*/
constructor(outputPath: string = DEFAULT_OUTPUT_FILE) {
super();
this._outputPath = outputPath;
}
/**
* An event that fires when cleaning has started.
*/
onStart(dryRun: boolean): void {
this._dryRun = dryRun;
this._trashEmails = [];
this._unreadEmailCount = 0;
}
/**
* An event that fires when unread emails are retrieved.
*/
onUnreadEmailsRetrieved(emails: Email[]): void {
this._unreadEmailCount = emails.length;
}
/**
* An event that fires when trash emails are identified.
*/
onTrashEmailsIdentified(emails: Email[]): void {
this._trashEmails = emails;
}
/**
* An event that fires when cleaning has stopped.
*/
onStop(): void {
const html = this._generateHtml();
fs.writeFileSync(this._outputPath, html, 'utf8');
const resolvedPath = path.resolve(this._outputPath);
console.log(`HTML report written to: ${resolvedPath}`);
}
/**
* Generates the full HTML report.
*/
private _generateHtml(): string {
const timestamp = new Date().toLocaleString();
const actionCounts = this._getActionCounts();
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trash Cleaner Report</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; background: #f5f5f5; }
.container { max-width: 900px; margin: 0 auto; background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
h1 { color: #333; border-bottom: 2px solid #eee; padding-bottom: 0.5rem; }
.summary { display: flex; gap: 1rem; margin: 1rem 0; flex-wrap: wrap; }
.stat { background: #f8f9fa; padding: 1rem; border-radius: 6px; min-width: 150px; }
.stat .number { font-size: 2rem; font-weight: bold; color: #333; }
.stat .label { color: #666; font-size: 0.9rem; }
.action-delete { border-left: 4px solid #dc3545; }
.action-archive { border-left: 4px solid #ffc107; }
.action-mark-as-read { border-left: 4px solid #0d6efd; }
.dry-run-badge { background: #fff3cd; color: #856404; padding: 0.25rem 0.75rem; border-radius: 4px; display: inline-block; margin-bottom: 1rem; }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
.action-badge { padding: 0.2rem 0.5rem; border-radius: 3px; font-size: 0.8rem; font-weight: 500; }
.badge-delete { background: #f8d7da; color: #721c24; }
.badge-archive { background: #fff3cd; color: #856404; }
.badge-mark-as-read { background: #cce5ff; color: #004085; }
.timestamp { color: #999; font-size: 0.85rem; }
.empty { color: #666; font-style: italic; padding: 2rem; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h1>🗑️ Trash Cleaner Report</h1>
<p class="timestamp">Generated: ${this._escapeHtml(timestamp)}</p>
${this._dryRun ? '<span class="dry-run-badge">⚠️ Dry-run mode — no actions were performed</span>' : ''}
<div class="summary">
<div class="stat">
<div class="number">${this._unreadEmailCount}</div>
<div class="label">Unread emails</div>
</div>
<div class="stat">
<div class="number">${this._trashEmails.length}</div>
<div class="label">Trash identified</div>
</div>
${this._renderActionStats(actionCounts)}
</div>
${this._trashEmails.length > 0 ? this._renderEmailTable() : '<p class="empty">No trash emails found.</p>'}
</div>
</body>
</html>`;
}
/**
* Gets action counts from trash emails.
*/
private _getActionCounts(): Record<string, number> {
const counts: Record<string, number> = {};
for (const email of this._trashEmails) {
const action = email._action || 'delete';
counts[action] = (counts[action] || 0) + 1;
}
return counts;
}
/**
* Renders action stat cards.
*/
private _renderActionStats(actionCounts: Record<string, number>): string {
return Object.entries(actionCounts).map(([action, count]) =>
`<div class="stat action-${action}">
<div class="number">${count}</div>
<div class="label">${this._actionLabel(action)}</div>
</div>`
).join('\n ');
}
/**
* Renders the email details table.
*/
private _renderEmailTable(): string {
const rows = this._trashEmails.map(email => {
const action = email._action || 'delete';
const rule = email._rule ? this._escapeHtml(email._rule) : '';
return `<tr>
<td><span class="action-badge badge-${action}">${action}</span></td>
<td>${rule}</td>
<td>${this._escapeHtml(email.from)}</td>
<td>${this._escapeHtml(email.subject)}</td>
<td>${this._escapeHtml(email.labels.join(', '))}</td>
</tr>`;
}).join('\n');
return `<table>
<thead>
<tr><th>Action</th><th>Rule</th><th>From</th><th>Subject</th><th>Labels</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>`;
}
/**
* Returns a human-readable action label.
*/
private _actionLabel(action: string): string {
switch (action) {
case 'delete': return 'Deleted';
case 'archive': return 'Archived';
case 'mark-as-read': return 'Marked as read';
default: return action;
}
}
/**
* Escapes HTML special characters.
*/
private _escapeHtml(str: string): string {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
}
export { HtmlProgressReporter };