new action: attach the message as PDF and file it
For senders whose mail body is the receipt (e.g. GoCardless payouts) there is no attachment to file. The new action "Als PDF anhängen + nach <Ordner>" renders the mail like a print from a mail client (header block From/To/Date/Subject, then the content) and replaces it in the target folder with: original content + PDF + the original mail byte-for-byte as Original.eml, so the unaltered original is retained (GoBD). - Rendering with WeasyPrint. Images are fetched by us and inlined as data: URIs; WeasyPrint itself may only read data:. Fetching is restricted to http(s), public addresses (private, loopback, link-local and PDF_BLOCKED_NETWORKS, default 172.0.0.0/12, are refused; redirects are checked hop by hop), with time and size limits. cid: images come from the MIME parts. - The replacement is APPENDed with the original flags and INTERNALDATE, verified in the target via its new Message-ID, and only then is the original deleted. Already converted mails are only moved. - "PDF-Vorschau" in the rule dialog renders the newest matching mail read-only and opens it in the browser. - List/log show 📄 targets; warnings for PDF + move/delete and for PDF + "Hat Anhang = Ja"; log folder search covers PDF targets. - Docker image gets Pango and fonts (~316 MB total). Tested with the real GoCardless mail (3 remote images, rendered both locally and inside the built image) and a simulated IMAP server for success, rejected APPEND, missing copy and already converted mails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,10 @@
|
||||
<section class="dlg-section test-section">
|
||||
<div class="test-head">
|
||||
<h4>Regel testen</h4>
|
||||
<button type="button" class="outline small" id="test-btn" onclick="runTest()">Gegen Postfach testen</button>
|
||||
<span>
|
||||
<button type="button" class="outline small" id="pdf-preview-btn" onclick="runPdfPreview()" hidden title="PDF der neuesten passenden Mail erzeugen und anzeigen — es wird nichts verändert">PDF-Vorschau</button>
|
||||
<button type="button" class="outline small" id="test-btn" onclick="runTest()">Gegen Postfach testen</button>
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted small-text">Prüft die aktuelle (ungespeicherte) Regel nur lesend gegen die neuesten passenden Mails im Quellordner. Es wird nichts verschoben.</p>
|
||||
<div id="test-result"></div>
|
||||
@@ -251,6 +254,7 @@ const DATE_MATCH_TYPES = [
|
||||
const ALL_MATCH_TYPES = [...TEXT_MATCH_TYPES, ...DATE_MATCH_TYPES];
|
||||
const ACTION_TYPES = [
|
||||
{value: 'move', label: 'Verschieben nach', needsParam: true, needsFolder: true, paramLabel: 'Zielordner'},
|
||||
{value: 'pdf', label: 'Als PDF anhängen + nach', needsParam: true, needsFolder: true, paramLabel: 'Zielordner'},
|
||||
{value: 'forward', label: 'Weiterleiten an', needsParam: true, needsFolder: false, paramLabel: 'E-Mail-Adresse'},
|
||||
{value: 'delete', label: 'Löschen (in Papierkorb)', needsParam: false, needsFolder: false},
|
||||
{value: 'mark_read', label: 'Als gelesen markieren', needsParam: false, needsFolder: false},
|
||||
@@ -324,8 +328,15 @@ function ruleWarnings(r) {
|
||||
}
|
||||
for (const a of acts) {
|
||||
if (a.action_type === 'move' && !a.parameter) w.push('Verschieben ohne Zielordner.');
|
||||
if (a.action_type === 'pdf' && !a.parameter) w.push('„Als PDF anhängen“ ohne Zielordner.');
|
||||
if (a.action_type === 'forward' && !a.parameter) w.push('Weiterleiten ohne Zieladresse.');
|
||||
if (a.action_type === 'move' && a.parameter && a.parameter === r.source_folder) w.push('Zielordner ist gleich dem Quellordner.');
|
||||
if ((a.action_type === 'move' || a.action_type === 'pdf') && a.parameter && a.parameter === r.source_folder) w.push('Zielordner ist gleich dem Quellordner.');
|
||||
}
|
||||
if (acts.some(a => a.action_type === 'pdf')) {
|
||||
if (acts.some(a => a.action_type === 'move' || a.action_type === 'delete'))
|
||||
w.push('„Als PDF anhängen“ legt die Mail bereits im Zielordner ab — ein zusätzliches Verschieben/Löschen ist nicht nötig und würde scheitern.');
|
||||
if (conds.some(c => c.field === 'has_attachment' && c.value === 'true' && !c.negate))
|
||||
w.push('„Hat Anhang = Ja“ passt nicht zu „Als PDF anhängen“: die PDF-Aktion ist für Mails gedacht, bei denen die Nachricht selbst der Beleg ist.');
|
||||
}
|
||||
return w;
|
||||
}
|
||||
@@ -407,6 +418,7 @@ function condChip(c) {
|
||||
function targetText(f) {
|
||||
return (f.actions || []).map(a => {
|
||||
if (a.action_type === 'move') return '📁 ' + (a.parameter || '?').replace(/^INBOX[./]/, '');
|
||||
if (a.action_type === 'pdf') return '📄 ' + (a.parameter || '?').replace(/^INBOX[./]/, '');
|
||||
if (a.action_type === 'forward') return '↪ ' + (a.parameter || '?');
|
||||
if (a.action_type === 'delete') return '🗑 Papierkorb';
|
||||
if (a.action_type === 'mark_read') return '✓ gelesen';
|
||||
@@ -694,6 +706,7 @@ function collectFormData() {
|
||||
|
||||
function refreshWarnings() {
|
||||
const data = collectFormData();
|
||||
document.getElementById('pdf-preview-btn').hidden = !data.actions.some(a => a.action_type === 'pdf');
|
||||
const w = ruleWarnings(data);
|
||||
// Erkenntnisse aus dem letzten Test ergänzen
|
||||
if (lastTest) {
|
||||
@@ -738,6 +751,44 @@ async function saveFilter() {
|
||||
|
||||
// ---------- Test ----------
|
||||
|
||||
function hasPdfAction() {
|
||||
return collectActions().some(a => a.action_type === 'pdf');
|
||||
}
|
||||
|
||||
async function runPdfPreview() {
|
||||
const data = collectFormData();
|
||||
const out = document.getElementById('test-result');
|
||||
const btn = document.getElementById('pdf-preview-btn');
|
||||
if (!data.conditions.length) { out.innerHTML = '<p class="muted">Erst mindestens eine Bedingung anlegen.</p>'; return; }
|
||||
// Fenster sofort öffnen, sonst blockt der Browser es als Popup
|
||||
const win = window.open('', '_blank');
|
||||
btn.setAttribute('aria-busy', 'true');
|
||||
btn.disabled = true;
|
||||
out.innerHTML = '<p class="muted small-text">PDF wird erzeugt (Bilder werden geladen)…</p>';
|
||||
try {
|
||||
const resp = await fetch('/api/filters/pdf-preview', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({account_id: data.account_id, source_folder: data.source_folder, conditions: data.conditions, limit: 40}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const d = await readJson(resp);
|
||||
if (win) win.close();
|
||||
out.innerHTML = `<p class="reason">${esc(typeof d.detail === 'string' ? d.detail : JSON.stringify(d.detail))}</p>`;
|
||||
return;
|
||||
}
|
||||
const url = URL.createObjectURL(await resp.blob());
|
||||
if (win) win.location = url; else window.open(url, '_blank');
|
||||
out.innerHTML = `<p class="muted small-text">PDF-Vorschau geöffnet · Bilder: ${esc(resp.headers.get('X-Preview-Images') || '')}</p>`;
|
||||
} catch (e) {
|
||||
if (win) win.close();
|
||||
out.innerHTML = `<p class="reason">Netzwerk-Fehler: ${esc(e.message)}</p>`;
|
||||
} finally {
|
||||
btn.removeAttribute('aria-busy');
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
const data = collectFormData();
|
||||
const out = document.getElementById('test-result');
|
||||
|
||||
@@ -339,6 +339,7 @@ function emailOf(from) {
|
||||
function targetOf(log) {
|
||||
// Bei Aktionen steht der Zielordner in details, sonst Quellordner
|
||||
if (log.action_type === 'move' && log.details) return {text: '📁 ' + log.details.replace(/^INBOX[./]/, ''), filter: log.details};
|
||||
if (log.action_type === 'pdf' && log.details) return {text: '📄 ' + log.details.replace(/^INBOX[./]/, ''), filter: log.details};
|
||||
if (log.action_type === 'forward' && log.details) return {text: '↪ ' + log.details, filter: null};
|
||||
if (log.action_type === 'delete') return {text: '🗑 Papierkorb', filter: null};
|
||||
if (log.folder) return {text: log.folder, filter: log.folder};
|
||||
@@ -348,7 +349,7 @@ function targetOf(log) {
|
||||
function shortMessage(log) {
|
||||
const m = log.message || '';
|
||||
if (log.action_type && m.startsWith('Aktion ausgeführt')) {
|
||||
return {move: 'Verschoben', forward: 'Weitergeleitet', delete: 'Gelöscht', mark_read: 'Als gelesen markiert'}[log.action_type] || m;
|
||||
return {move: 'Verschoben', pdf: 'Als PDF angehängt & abgelegt', forward: 'Weitergeleitet', delete: 'Gelöscht', mark_read: 'Als gelesen markiert'}[log.action_type] || m;
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user