Initial commit: IMAP Mail Filter Service
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ "Konto bearbeiten" if account else "Neues Konto" }} — IMAP Mail Filter{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{{ "Konto bearbeiten" if account else "Neues Konto" }}</h1>
|
||||
|
||||
<form id="account-form">
|
||||
<fieldset>
|
||||
<legend>IMAP-Einstellungen</legend>
|
||||
<label>
|
||||
Name
|
||||
<input type="text" name="name" value="{{ account.name if account else '' }}" required placeholder="z.B. Arbeit, Privat">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label>
|
||||
IMAP-Server
|
||||
<input type="text" name="imap_host" value="{{ account.imap_host if account else '' }}" required placeholder="imap.example.com">
|
||||
</label>
|
||||
<label>
|
||||
Port
|
||||
<input type="number" name="imap_port" value="{{ account.imap_port if account else 993 }}" required>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<label>
|
||||
Benutzername
|
||||
<input type="text" name="username" value="{{ account.username if account else '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Passwort
|
||||
<input type="password" name="password" value="" placeholder="{{ '(unverändert)' if account else '' }}">
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" name="use_ssl" {{ 'checked' if (not account or account.use_ssl) else '' }}>
|
||||
SSL verwenden
|
||||
</label>
|
||||
<label>
|
||||
Polling-Intervall (Sekunden)
|
||||
<input type="number" name="poll_interval_seconds" value="{{ account.poll_interval_seconds if account else 120 }}" min="30">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="enabled" {{ 'checked' if (not account or account.enabled) else '' }}>
|
||||
Konto aktiv
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>SMTP-Einstellungen (optional, für Weiterleitung)</legend>
|
||||
<div class="grid">
|
||||
<label>
|
||||
SMTP-Server
|
||||
<input type="text" name="smtp_host" value="{{ account.smtp_host if account else '' }}" placeholder="smtp.example.com">
|
||||
</label>
|
||||
<label>
|
||||
SMTP-Port
|
||||
<input type="number" name="smtp_port" value="{{ account.smtp_port if account else 465 }}">
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<label>
|
||||
SMTP-Benutzer
|
||||
<input type="text" name="smtp_username" value="{{ account.smtp_username if account else '' }}">
|
||||
</label>
|
||||
<label>
|
||||
SMTP-Passwort
|
||||
<input type="password" name="smtp_password" value="" placeholder="{{ '(unverändert)' if account else '' }}">
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div role="group">
|
||||
<button type="submit">{{ "Speichern" if account else "Erstellen" }}</button>
|
||||
<a href="/accounts" role="button" class="outline">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.getElementById('account-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const data = {
|
||||
name: form.name.value,
|
||||
imap_host: form.imap_host.value,
|
||||
imap_port: parseInt(form.imap_port.value),
|
||||
use_ssl: form.use_ssl.checked,
|
||||
username: form.username.value,
|
||||
poll_interval_seconds: parseInt(form.poll_interval_seconds.value),
|
||||
enabled: form.enabled.checked,
|
||||
};
|
||||
if (form.password.value) data.password = form.password.value;
|
||||
if (form.smtp_host.value) {
|
||||
data.smtp_host = form.smtp_host.value;
|
||||
data.smtp_port = parseInt(form.smtp_port.value);
|
||||
data.smtp_username = form.smtp_username.value;
|
||||
if (form.smtp_password.value) data.smtp_password = form.smtp_password.value;
|
||||
}
|
||||
|
||||
{% if account %}
|
||||
const resp = await fetch('/api/accounts/{{ account.id }}', {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
{% else %}
|
||||
if (!data.password) { alert('Passwort ist erforderlich'); return; }
|
||||
const resp = await fetch('/api/accounts/', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
if (resp.ok) {
|
||||
window.location.href = '/accounts';
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
alert('Fehler: ' + JSON.stringify(err));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,51 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Konten — IMAP Mail Filter{% endblock %}
|
||||
{% block content %}
|
||||
<h1>IMAP-Konten</h1>
|
||||
|
||||
<a href="/accounts/new" role="button">Neues Konto</a>
|
||||
|
||||
{% if accounts %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Server</th>
|
||||
<th>Benutzer</th>
|
||||
<th>Status</th>
|
||||
<th>Letzter Poll</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for acc in accounts %}
|
||||
<tr>
|
||||
<td>{{ acc.name }}</td>
|
||||
<td>{{ acc.imap_host }}:{{ acc.imap_port }}</td>
|
||||
<td>{{ acc.username }}</td>
|
||||
<td>{{ "Aktiv" if acc.enabled else "Deaktiviert" }}</td>
|
||||
<td>{{ acc.last_poll_at or "Noch nie" }}</td>
|
||||
<td>
|
||||
<div role="group">
|
||||
<a href="/accounts/{{ acc.id }}/edit" role="button" class="outline small">Bearbeiten</a>
|
||||
<button class="outline small contrast" onclick="deleteAccount({{ acc.id }}, '{{ acc.name }}')">Löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>Noch keine Konten vorhanden.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function deleteAccount(id, name) {
|
||||
if (!confirm(`Konto "${name}" wirklich löschen? Alle Filterregeln werden ebenfalls gelöscht.`)) return;
|
||||
await fetch(`/api/accounts/${id}`, {method: 'DELETE'});
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}IMAP Mail Filter{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container-fluid">
|
||||
<ul>
|
||||
<li><strong>IMAP Mail Filter</strong></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/">Dashboard</a></li>
|
||||
<li><a href="/accounts">Konten</a></li>
|
||||
<li><a href="/filters">Filter</a></li>
|
||||
<li><a href="/yaml">YAML</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{% if flash_message %}
|
||||
<article role="alert">{{ flash_message }}</article>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer class="container">
|
||||
<small>IMAP Mail Filter Service</small>
|
||||
</footer>
|
||||
<script src="/static/app.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — IMAP Mail Filter{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
{% if accounts %}
|
||||
<div class="grid">
|
||||
{% for acc in accounts %}
|
||||
<article>
|
||||
<header>
|
||||
<hgroup>
|
||||
<h3>{{ acc.name }}</h3>
|
||||
<p>{{ acc.username }}@{{ acc.imap_host }}</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
<p>
|
||||
<strong>Status:</strong>
|
||||
{% if acc.enabled %}
|
||||
<ins>Aktiv</ins>
|
||||
{% else %}
|
||||
<del>Deaktiviert</del>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>Polling:</strong> alle {{ acc.poll_interval_seconds }}s</p>
|
||||
<p><strong>Letzter Poll:</strong> {{ acc.last_poll_at or "Noch nie" }}</p>
|
||||
<p><strong>Filterregeln:</strong> {{ acc.filter_rule_count }}</p>
|
||||
<footer>
|
||||
<div role="group">
|
||||
<button class="outline" onclick="pollNow({{ acc.id }})">Jetzt prüfen</button>
|
||||
<button class="outline" onclick="testConnection({{ acc.id }})">Verbindungstest</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<article>
|
||||
<p>Noch keine Konten eingerichtet. <a href="/accounts/new">Konto hinzufügen</a></p>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<div id="status-message" style="display:none"></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function pollNow(accountId) {
|
||||
const msg = document.getElementById('status-message');
|
||||
msg.style.display = 'block';
|
||||
msg.textContent = 'Polling läuft...';
|
||||
msg.setAttribute('role', 'status');
|
||||
try {
|
||||
const resp = await fetch(`/api/accounts/${accountId}/poll-now`, {method: 'POST'});
|
||||
const data = await resp.json();
|
||||
msg.textContent = data.message;
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} catch(e) {
|
||||
msg.textContent = 'Fehler: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function testConnection(accountId) {
|
||||
const msg = document.getElementById('status-message');
|
||||
msg.style.display = 'block';
|
||||
msg.textContent = 'Teste Verbindung...';
|
||||
try {
|
||||
const resp = await fetch(`/api/accounts/${accountId}/test`, {method: 'POST'});
|
||||
const data = await resp.json();
|
||||
msg.textContent = data.message;
|
||||
} catch(e) {
|
||||
msg.textContent = 'Fehler: ' + e.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,283 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Filterregeln — IMAP Mail Filter{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Filterregeln</h1>
|
||||
|
||||
<label for="account-select">Konto auswählen:</label>
|
||||
<select id="account-select" onchange="loadFilters()">
|
||||
<option value="">— Konto wählen —</option>
|
||||
{% for acc in accounts %}
|
||||
<option value="{{ acc.id }}" {{ 'selected' if selected_account_id == acc.id else '' }}>{{ acc.name }} ({{ acc.username }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div id="filters-container">
|
||||
<p>Bitte ein Konto auswählen.</p>
|
||||
</div>
|
||||
|
||||
<dialog id="filter-dialog">
|
||||
<article style="min-width: 60vw;">
|
||||
<header>
|
||||
<button aria-label="Close" rel="prev" onclick="document.getElementById('filter-dialog').close()"></button>
|
||||
<h3 id="dialog-title">Neue Filterregel</h3>
|
||||
</header>
|
||||
<form id="filter-form">
|
||||
<input type="hidden" id="filter-id" value="">
|
||||
<label>
|
||||
Name
|
||||
<input type="text" id="filter-name" required placeholder="z.B. Newsletter sortieren">
|
||||
</label>
|
||||
<div class="grid">
|
||||
<label>
|
||||
Priorität
|
||||
<input type="number" id="filter-priority" value="100">
|
||||
</label>
|
||||
<label>
|
||||
Quellordner
|
||||
<input type="text" id="filter-source-folder" value="INBOX">
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<input type="checkbox" id="filter-stop-processing">
|
||||
Nach Treffer keine weiteren Regeln anwenden
|
||||
</label>
|
||||
|
||||
<h4>Bedingungen <small>(alle müssen zutreffen)</small></h4>
|
||||
<div id="conditions-list"></div>
|
||||
<button type="button" class="outline small" onclick="addCondition()">+ Bedingung</button>
|
||||
|
||||
<h4>Aktionen</h4>
|
||||
<div id="actions-list"></div>
|
||||
<button type="button" class="outline small" onclick="addAction()">+ Aktion</button>
|
||||
|
||||
<footer>
|
||||
<div role="group">
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" class="outline" onclick="document.getElementById('filter-dialog').close()">Abbrechen</button>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</article>
|
||||
</dialog>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const FIELDS = [
|
||||
{value: 'from', label: 'Von'},
|
||||
{value: 'to', label: 'An'},
|
||||
{value: 'subject', label: 'Betreff'},
|
||||
{value: 'body', label: 'Text'},
|
||||
{value: 'has_attachment', label: 'Hat Anhang'},
|
||||
];
|
||||
const MATCH_TYPES = [
|
||||
{value: 'contains', label: 'enthält'},
|
||||
{value: 'regex', label: 'Regex'},
|
||||
{value: 'exact', label: 'exakt'},
|
||||
];
|
||||
const ACTION_TYPES = [
|
||||
{value: 'move', label: 'Verschieben in Ordner', needsParam: true, paramLabel: 'Zielordner'},
|
||||
{value: 'forward', label: 'Weiterleiten an', needsParam: true, paramLabel: 'E-Mail-Adresse'},
|
||||
{value: 'delete', label: 'Löschen', needsParam: false},
|
||||
{value: 'mark_read', label: 'Als gelesen markieren', needsParam: false},
|
||||
];
|
||||
|
||||
async function loadFilters() {
|
||||
const accountId = document.getElementById('account-select').value;
|
||||
const container = document.getElementById('filters-container');
|
||||
if (!accountId) {
|
||||
container.innerHTML = '<p>Bitte ein Konto auswählen.</p>';
|
||||
return;
|
||||
}
|
||||
const resp = await fetch(`/api/filters/account/${accountId}`);
|
||||
const filters = await resp.json();
|
||||
if (filters.length === 0) {
|
||||
container.innerHTML = `
|
||||
<p>Keine Filterregeln für dieses Konto.</p>
|
||||
<button onclick="openNewFilter()">Neue Regel erstellen</button>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
let html = '<button onclick="openNewFilter()" style="margin-bottom:1rem">Neue Regel</button><table><thead><tr><th>Prio</th><th>Name</th><th>Ordner</th><th>Bedingungen</th><th>Aktionen</th><th></th></tr></thead><tbody>';
|
||||
for (const f of filters) {
|
||||
const conds = f.conditions.map(c => {
|
||||
const fieldLabel = FIELDS.find(x => x.value === c.field)?.label || c.field;
|
||||
const matchLabel = MATCH_TYPES.find(x => x.value === c.match_type)?.label || c.match_type;
|
||||
return `${c.negate ? 'NICHT ' : ''}${fieldLabel} ${matchLabel} "${c.value}"`;
|
||||
}).join('<br>');
|
||||
const acts = f.actions.map(a => {
|
||||
const at = ACTION_TYPES.find(x => x.value === a.action_type);
|
||||
return `${at?.label || a.action_type}${a.parameter ? ': ' + a.parameter : ''}`;
|
||||
}).join('<br>');
|
||||
html += `<tr>
|
||||
<td>${f.priority}</td>
|
||||
<td>${f.name}${f.stop_processing ? ' <small>(stop)</small>' : ''}</td>
|
||||
<td>${f.source_folder}</td>
|
||||
<td><small>${conds}</small></td>
|
||||
<td><small>${acts}</small></td>
|
||||
<td>
|
||||
<div role="group">
|
||||
<button class="outline small" onclick='editFilter(${JSON.stringify(f)})'>Bearbeiten</button>
|
||||
<button class="outline small contrast" onclick="deleteFilter(${f.id})">Löschen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function addCondition(data = null) {
|
||||
const list = document.getElementById('conditions-list');
|
||||
const idx = list.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'grid';
|
||||
div.style.marginBottom = '0.5rem';
|
||||
div.innerHTML = `
|
||||
<select name="cond_field_${idx}">
|
||||
${FIELDS.map(f => `<option value="${f.value}" ${data?.field === f.value ? 'selected' : ''}>${f.label}</option>`).join('')}
|
||||
</select>
|
||||
<select name="cond_match_${idx}">
|
||||
${MATCH_TYPES.map(m => `<option value="${m.value}" ${data?.match_type === m.value ? 'selected' : ''}>${m.label}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" name="cond_value_${idx}" value="${data?.value || ''}" placeholder="Wert" required>
|
||||
<label style="white-space:nowrap"><input type="checkbox" name="cond_negate_${idx}" ${data?.negate ? 'checked' : ''}> NOT</label>
|
||||
<button type="button" class="outline small contrast" onclick="this.parentElement.remove()">X</button>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
}
|
||||
|
||||
function addAction(data = null) {
|
||||
const list = document.getElementById('actions-list');
|
||||
const idx = list.children.length;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'grid';
|
||||
div.style.marginBottom = '0.5rem';
|
||||
const selectedType = ACTION_TYPES.find(a => a.value === data?.action_type);
|
||||
div.innerHTML = `
|
||||
<select name="act_type_${idx}" onchange="toggleActionParam(this)">
|
||||
${ACTION_TYPES.map(a => `<option value="${a.value}" data-needs-param="${a.needsParam}" data-param-label="${a.paramLabel || ''}" ${data?.action_type === a.value ? 'selected' : ''}>${a.label}</option>`).join('')}
|
||||
</select>
|
||||
<input type="text" name="act_param_${idx}" value="${data?.parameter || ''}" placeholder="${selectedType?.paramLabel || 'Parameter'}" ${selectedType?.needsParam === false && !data?.parameter ? 'style=display:none' : ''}>
|
||||
<button type="button" class="outline small contrast" onclick="this.parentElement.remove()">X</button>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
}
|
||||
|
||||
function toggleActionParam(select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
const input = select.parentElement.querySelector('input[type=text]');
|
||||
if (opt.dataset.needsParam === 'true') {
|
||||
input.style.display = '';
|
||||
input.placeholder = opt.dataset.paramLabel;
|
||||
} else {
|
||||
input.style.display = 'none';
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function openNewFilter() {
|
||||
document.getElementById('dialog-title').textContent = 'Neue Filterregel';
|
||||
document.getElementById('filter-id').value = '';
|
||||
document.getElementById('filter-name').value = '';
|
||||
document.getElementById('filter-priority').value = '100';
|
||||
document.getElementById('filter-source-folder').value = 'INBOX';
|
||||
document.getElementById('filter-stop-processing').checked = false;
|
||||
document.getElementById('conditions-list').innerHTML = '';
|
||||
document.getElementById('actions-list').innerHTML = '';
|
||||
addCondition();
|
||||
addAction();
|
||||
document.getElementById('filter-dialog').showModal();
|
||||
}
|
||||
|
||||
function editFilter(f) {
|
||||
document.getElementById('dialog-title').textContent = 'Regel bearbeiten';
|
||||
document.getElementById('filter-id').value = f.id;
|
||||
document.getElementById('filter-name').value = f.name;
|
||||
document.getElementById('filter-priority').value = f.priority;
|
||||
document.getElementById('filter-source-folder').value = f.source_folder;
|
||||
document.getElementById('filter-stop-processing').checked = f.stop_processing;
|
||||
document.getElementById('conditions-list').innerHTML = '';
|
||||
document.getElementById('actions-list').innerHTML = '';
|
||||
for (const c of f.conditions) addCondition(c);
|
||||
for (const a of f.actions) addAction(a);
|
||||
document.getElementById('filter-dialog').showModal();
|
||||
}
|
||||
|
||||
function collectFormData() {
|
||||
const accountId = document.getElementById('account-select').value;
|
||||
const conditions = [];
|
||||
const condRows = document.getElementById('conditions-list').children;
|
||||
for (let i = 0; i < condRows.length; i++) {
|
||||
const row = condRows[i];
|
||||
const selects = row.querySelectorAll('select');
|
||||
const input = row.querySelector('input[type=text]');
|
||||
const checkbox = row.querySelector('input[type=checkbox]');
|
||||
conditions.push({
|
||||
field: selects[0].value,
|
||||
match_type: selects[1].value,
|
||||
value: input.value,
|
||||
negate: checkbox.checked,
|
||||
});
|
||||
}
|
||||
const actions = [];
|
||||
const actRows = document.getElementById('actions-list').children;
|
||||
for (let i = 0; i < actRows.length; i++) {
|
||||
const row = actRows[i];
|
||||
const select = row.querySelector('select');
|
||||
const input = row.querySelector('input[type=text]');
|
||||
actions.push({
|
||||
action_type: select.value,
|
||||
parameter: input.value || null,
|
||||
});
|
||||
}
|
||||
return {
|
||||
account_id: parseInt(accountId),
|
||||
name: document.getElementById('filter-name').value,
|
||||
priority: parseInt(document.getElementById('filter-priority').value),
|
||||
source_folder: document.getElementById('filter-source-folder').value,
|
||||
stop_processing: document.getElementById('filter-stop-processing').checked,
|
||||
enabled: true,
|
||||
conditions,
|
||||
actions,
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('filter-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const data = collectFormData();
|
||||
const filterId = document.getElementById('filter-id').value;
|
||||
|
||||
let resp;
|
||||
if (filterId) {
|
||||
resp = await fetch(`/api/filters/${filterId}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
} else {
|
||||
resp = await fetch('/api/filters/', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
if (resp.ok) {
|
||||
document.getElementById('filter-dialog').close();
|
||||
loadFilters();
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
alert('Fehler: ' + JSON.stringify(err));
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteFilter(id) {
|
||||
if (!confirm('Filterregel wirklich löschen?')) return;
|
||||
await fetch(`/api/filters/${id}`, {method: 'DELETE'});
|
||||
loadFilters();
|
||||
}
|
||||
|
||||
// Auto-load if account is pre-selected
|
||||
if (document.getElementById('account-select').value) loadFilters();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}YAML — IMAP Mail Filter{% endblock %}
|
||||
{% block content %}
|
||||
<h1>YAML Import / Export</h1>
|
||||
|
||||
<div class="grid">
|
||||
<article>
|
||||
<header><h3>Export</h3></header>
|
||||
<p>Aktuelle Konfiguration als YAML-Datei herunterladen.</p>
|
||||
<button onclick="exportYaml()">YAML exportieren</button>
|
||||
<pre id="yaml-preview" style="max-height: 400px; overflow-y: auto; display: none;"></pre>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<header><h3>Import</h3></header>
|
||||
<p>YAML-Datei hochladen um Konten und Filterregeln zu importieren.</p>
|
||||
<form id="import-form">
|
||||
<input type="file" name="file" accept=".yaml,.yml" required>
|
||||
<button type="submit">Importieren</button>
|
||||
</form>
|
||||
<div id="import-result" style="display:none"></div>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
async function exportYaml() {
|
||||
const resp = await fetch('/api/yaml/export');
|
||||
const text = await resp.text();
|
||||
const pre = document.getElementById('yaml-preview');
|
||||
pre.textContent = text;
|
||||
pre.style.display = 'block';
|
||||
|
||||
// Also trigger download
|
||||
const blob = new Blob([text], {type: 'text/yaml'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'filters.yaml';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
document.getElementById('import-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const resp = await fetch('/api/yaml/import', {method: 'POST', body: formData});
|
||||
const result = await resp.json();
|
||||
const div = document.getElementById('import-result');
|
||||
div.style.display = 'block';
|
||||
if (result.error) {
|
||||
div.innerHTML = `<article role="alert">Fehler: ${result.error}</article>`;
|
||||
} else {
|
||||
div.innerHTML = `<article>
|
||||
Import erfolgreich!<br>
|
||||
Konten erstellt: ${result.accounts_created}<br>
|
||||
Konten aktualisiert: ${result.accounts_updated}<br>
|
||||
Regeln erstellt: ${result.rules_created}
|
||||
</article>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user