feat: ARIA kann E-Mails lesen und senden
IMAP/SMTP mit SSL/TLS, STARTTLS oder unverschluesselt. Senden mit Text, HTML, Anhaengen und eingebetteten Bildern (cid:), Antworten mit In-Reply-To. Lesen, Suchen, Ordner anlegen (auch Unterordner), Mails verschieben. Tools: email_status/send/list/read/folders/folder_create/move. Whitelist als harte Grenze im Sendepfad, nicht im Prompt: alles, was nicht drinsteht, ist gesperrt, leer = nirgendwohin. Jeder Empfaenger zaehlt (An, Cc, Bcc), ein fremder blockiert die ganze Mail. Genau eine Schreibweise: name@domain, '*' nur als *@domain, *@*.domain oder *@*. '*hacker-net.de' liest sich, als passte es auf 'evilhacker-net.de' — solche Kurzformen lehnt das Speichern mit Korrekturvorschlag ab. Verglichen wird die Domain als Ganzes. Zugangsdaten sieht ARIA nie: - liegen im Brain unter /data. NICHT in /shared — das ist in ARIAs Container eingehaengt, /data nicht. - keine Schnittstelle gibt das Passwort zurueck, nur password_set. - Brain-Export laesst email_secret.json + email_admin.key aus (GNU- und busybox-tar geprueft, der Diagnostic-Container ist Alpine). - Anhangspfade nach realpath geprueft: ein Symlink in /shared/uploads auf die Geheimnis-Datei wuerde sonst im Brain aufgeloest und verschickt. - Konfiguration nur mit Admin-Schluessel (X-Aria-Admin, liegt in /data). Die Diagnostic haengt ihn an, weist aber Anfragen aus ARIAs Container ab — sonst koennte eine eingehende Mail sie per curl die Whitelist oeffnen lassen. Grenze: ARIAs Container hat SSH auf die VM. Gegen einen gezielten Einbruch schuetzt das nicht, nur dagegen, dass Zugangsdaten von selbst auftauchen. Eingehende Mails sind fuer ARIA als Daten markiert, nicht als Anweisungen. Diagnostic: Einstellungen → Integrationen → ARIA E-Mail, plus "📬 Postfach oeffnen" — ARIAs ganzes Postfach in eigenem Fenster, Unterordner eingerueckt, Posteingang + Gesendet oben. Nur lesend, markiert nichts als gelesen. Mail-HTML in einem Rahmen ohne Skripte und ohne Nachladen von aussen (kein Tracking-Pixel); eingebettete Bilder kommen als data: mit. Jede gesendete Mail landet als Kopie in "Gesendet" — fehlt der Ordner, wird er angelegt, statt die Kopie still zu verlieren. Tests: 65 Einheitstests (Whitelist inkl. evilhacker-net.de, MIME-Aufbau, Header-Injection, Symlink-Ausbruch, Geheimnis nie in Ausgaben) + 50 gegen einen echten Mailserver (GreenMail): TLS-Login, Senden mit Anhang + Bild, Flags, Suche mit Umlaut, Gesendet-Kopie, In-Reply-To, Ordner mit Umlaut, MOVE und der Fallback ohne MOVE (der keine fremden geloeschten Mails mitreisst). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1108,6 +1108,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ARIA E-Mail -->
|
||||
<div class="settings-section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||
<h2 style="margin:0;">📧 ARIA E-Mail</h2>
|
||||
<button class="btn" onclick="openMailbox()" title="ARIAs Postfach in einem eigenen Fenster — nur lesen, markiert nichts als gelesen" style="padding:4px 12px;font-size:11px;">📬 Postfach öffnen</button>
|
||||
</div>
|
||||
<div style="font-size:11px;color:#8888AA;margin-bottom:8px;line-height:1.55;">
|
||||
ARIA kann Mails lesen und senden — Text, HTML, Anhänge, eingebettete Bilder.
|
||||
<strong style="color:#E0E0F0;">Die Zugangsdaten sieht sie nie:</strong> gespeichert im
|
||||
Brain unter <code>/data</code> (nicht in ihrem Container), keine Schnittstelle gibt das
|
||||
Passwort zurück, der Brain-Export lässt es aus. Ändern kann diese Einstellungen nur die
|
||||
Diagnostic — Anfragen aus ARIAs Container werden abgewiesen.
|
||||
</div>
|
||||
<div class="card" style="max-width:780px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Absender-Name<input type="text" id="mail-from-name" placeholder="ARIA" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Absender-Adresse<input type="text" id="mail-from-address" placeholder="aria@hacker-net.de" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:12px;color:#E0E0F0;font-weight:600;">Posteingang (IMAP)</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1.4fr;gap:10px;margin-top:4px;">
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Server<input type="text" id="mail-imap-host" placeholder="imap.beispiel.de" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Port<input type="number" id="mail-imap-port" placeholder="993" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Verschlüsselung<select id="mail-imap-encryption" onchange="mailEncChanged('mail-imap-encryption')" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;"><option value="ssl">SSL/TLS</option><option value="starttls">STARTTLS</option><option value="none">keine (unverschlüsselt)</option></select></label>
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:12px;color:#E0E0F0;font-weight:600;">Postausgang (SMTP)</div>
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1.4fr;gap:10px;margin-top:4px;">
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Server<input type="text" id="mail-smtp-host" placeholder="smtp.beispiel.de" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Port<input type="number" id="mail-smtp-port" placeholder="465" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Verschlüsselung<select id="mail-smtp-encryption" onchange="mailEncChanged('mail-smtp-encryption')" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;"><option value="ssl">SSL/TLS</option><option value="starttls">STARTTLS</option><option value="none">keine (unverschlüsselt)</option></select></label>
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:12px;color:#E0E0F0;font-weight:600;">Anmeldung</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:4px;">
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Benutzername<input type="text" id="mail-username" placeholder="meist die Mail-Adresse" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">Passwort<input type="password" id="mail-password" placeholder="" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
</div>
|
||||
<details style="margin-top:8px;font-size:11px;color:#8888AA;">
|
||||
<summary style="cursor:pointer;">SMTP mit abweichender Anmeldung (selten, z.B. Relay)</summary>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px;">
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">SMTP-Benutzername<input type="text" id="mail-smtp-username" placeholder="leer = wie oben" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
<label style="display:flex;flex-direction:column;gap:3px;font-size:11px;color:#8888AA;">SMTP-Passwort<input type="password" id="mail-smtp-password" placeholder="" autocomplete="off" style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;"></label>
|
||||
</div>
|
||||
</details>
|
||||
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;font-size:11px;color:#8888AA;">
|
||||
<label style="cursor:pointer;"><input type="checkbox" id="mail-verify-tls" checked> Zertifikat prüfen</label>
|
||||
<label style="cursor:pointer;" title="Eine Kopie jeder Mail, die ARIA sendet, landet in deinem Ordner „Gesendet“ — dann siehst du im Postfach, was sie verschickt hat"><input type="checkbox" id="mail-save-sent" checked> Kopie in „Gesendet“ ablegen</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:14px;font-size:12px;color:#E0E0F0;font-weight:600;">Whitelist — nur hierhin darf ARIA senden</div>
|
||||
<div style="font-size:11px;color:#8888AA;margin:4px 0 6px;line-height:1.5;">
|
||||
Eine Zeile pro Eintrag. Alles, was nicht hier steht, ist gesperrt —
|
||||
<strong style="color:#FF9F0A;">leer = ARIA kann an niemanden senden.</strong>
|
||||
Gilt für jeden Empfänger (An, Cc, Bcc); ein einziger fremder blockiert die ganze Mail.
|
||||
<br>
|
||||
<code>stefan@hacker-net.de</code> genau diese Adresse ·
|
||||
<code>*@hacker-net.de</code> alle Adressen der Domain ·
|
||||
<code>*@*.hacker-net.de</code> alle Subdomains ·
|
||||
<code>*@*</code> alles
|
||||
</div>
|
||||
<textarea id="mail-whitelist" rows="5" placeholder="*@hacker-net.de mama@gmx.de"
|
||||
style="background:#0A0A14;color:#E0E0F0;border:1px solid #2A2A3E;padding:6px 8px;border-radius:4px;font-family:inherit;font-size:12px;width:100%;box-sizing:border-box;font-family:monospace;resize:vertical;"></textarea>
|
||||
|
||||
<div style="margin-top:14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<button class="btn" onclick="saveMailConfig()" style="padding:6px 14px;font-size:12px;">Speichern</button>
|
||||
<button class="btn secondary" onclick="testMailConfig()" title="Meldet sich an beiden Servern an — sendet nichts" style="padding:6px 14px;font-size:12px;">🔌 Verbindung testen</button>
|
||||
<span id="mail-status" style="color:#8888AA;font-size:11px;white-space:pre-wrap;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FLUX Bildgenerierung -->
|
||||
<div class="settings-section">
|
||||
<h2>FLUX Bildgenerierung</h2>
|
||||
@@ -5003,6 +5073,7 @@
|
||||
loadOnboardingQR();
|
||||
loadOAuthServices();
|
||||
loadJellyfinConfig();
|
||||
loadMailConfig();
|
||||
refreshVoiceIdStatus();
|
||||
send({ action: 'get_attachment_limit' });
|
||||
send({ action: 'get_privacy_flags' });
|
||||
@@ -5275,6 +5346,106 @@
|
||||
send({ action: 'host_forget', hostId });
|
||||
}
|
||||
|
||||
// ── ARIA E-Mail ───────────────────────────────────────────────────
|
||||
// Konfiguration ueber /api/email-admin/* — die Diagnostic haengt dort den
|
||||
// Admin-Schluessel an (siehe server.js). Passwoerter kommen NIE zurueck,
|
||||
// nur password_set; ein leeres Passwortfeld heisst "unveraendert".
|
||||
const MAIL_PORTS = { imap: { ssl: 993, starttls: 143, none: 143 },
|
||||
smtp: { ssl: 465, starttls: 587, none: 25 } };
|
||||
|
||||
function _mailStatus(text, color) {
|
||||
const el = document.getElementById('mail-status');
|
||||
if (el) { el.textContent = text; el.style.color = color || '#8888AA'; }
|
||||
}
|
||||
|
||||
// Port beim Wechsel der Verschluesselung mitziehen — aber nur, wenn noch
|
||||
// der Standard-Port der alten Wahl drinsteht. Ein bewusst eigener Port
|
||||
// bleibt stehen.
|
||||
function mailEncChanged(id) {
|
||||
const proto = id.includes('imap') ? 'imap' : 'smtp';
|
||||
const sel = document.getElementById(id);
|
||||
const port = document.getElementById(`mail-${proto}-port`);
|
||||
const prev = sel.dataset.prev || sel.value;
|
||||
const std = Object.values(MAIL_PORTS[proto]).map(String);
|
||||
if (!port.value || std.includes(port.value) && port.value === String(MAIL_PORTS[proto][prev])) {
|
||||
port.value = MAIL_PORTS[proto][sel.value];
|
||||
}
|
||||
sel.dataset.prev = sel.value;
|
||||
}
|
||||
|
||||
async function loadMailConfig() {
|
||||
try {
|
||||
const r = await fetch('/api/email-admin/config');
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.detail || ('HTTP ' + r.status));
|
||||
const set = (id, v) => { const el = document.getElementById(id); if (el) el.value = v ?? ''; };
|
||||
set('mail-from-name', d.from_name); set('mail-from-address', d.from_address);
|
||||
set('mail-imap-host', d.imap_host); set('mail-imap-port', d.imap_port);
|
||||
set('mail-imap-encryption', d.imap_encryption);
|
||||
set('mail-smtp-host', d.smtp_host); set('mail-smtp-port', d.smtp_port);
|
||||
set('mail-smtp-encryption', d.smtp_encryption);
|
||||
set('mail-username', d.username); set('mail-smtp-username', d.smtp_username);
|
||||
set('mail-password', ''); set('mail-smtp-password', '');
|
||||
document.getElementById('mail-password').placeholder = d.password_set ? '•••••• gesetzt — leer lassen = unverändert' : 'nicht gesetzt';
|
||||
document.getElementById('mail-smtp-password').placeholder = d.smtp_password_set ? '•••••• gesetzt' : 'leer = wie oben';
|
||||
['mail-imap-encryption', 'mail-smtp-encryption'].forEach(id => {
|
||||
const el = document.getElementById(id); if (el) el.dataset.prev = el.value; });
|
||||
document.getElementById('mail-verify-tls').checked = d.verify_tls !== false;
|
||||
document.getElementById('mail-save-sent').checked = d.save_sent !== false;
|
||||
set('mail-whitelist', (d.whitelist || []).join('\n'));
|
||||
_mailStatus((d.whitelist || []).length ? '' : 'Whitelist leer — ARIA kann an niemanden senden.', '#FF9F0A');
|
||||
} catch (e) { _mailStatus('Laden fehlgeschlagen: ' + e.message, '#FF6B6B'); }
|
||||
}
|
||||
|
||||
async function saveMailConfig() {
|
||||
const v = id => (document.getElementById(id)?.value || '').trim();
|
||||
const body = {
|
||||
from_name: v('mail-from-name'), from_address: v('mail-from-address'),
|
||||
imap_host: v('mail-imap-host'), imap_port: Number(v('mail-imap-port') || 993),
|
||||
imap_encryption: v('mail-imap-encryption'),
|
||||
smtp_host: v('mail-smtp-host'), smtp_port: Number(v('mail-smtp-port') || 465),
|
||||
smtp_encryption: v('mail-smtp-encryption'),
|
||||
username: v('mail-username'), smtp_username: v('mail-smtp-username'),
|
||||
verify_tls: document.getElementById('mail-verify-tls').checked,
|
||||
save_sent: document.getElementById('mail-save-sent').checked,
|
||||
whitelist: v('mail-whitelist').split(/\n+/).map(x => x.trim()).filter(Boolean),
|
||||
};
|
||||
// Passwoerter nur mitschicken, wenn etwas eingetippt wurde.
|
||||
const pw = document.getElementById('mail-password').value;
|
||||
const spw = document.getElementById('mail-smtp-password').value;
|
||||
if (pw) body.password = pw;
|
||||
if (spw) body.smtp_password = spw;
|
||||
if (body.imap_encryption === 'none' || body.smtp_encryption === 'none') {
|
||||
if (!confirm('Ohne Verschlüsselung gehen Passwort und Mails im Klartext durchs Netz. Trotzdem speichern?')) return;
|
||||
}
|
||||
_mailStatus('Speichere …');
|
||||
try {
|
||||
const r = await fetch('/api/email-admin/config', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.detail || ('HTTP ' + r.status));
|
||||
await loadMailConfig();
|
||||
_mailStatus('✓ Gespeichert' + ((d.whitelist || []).length ? '' : ' — Whitelist leer, ARIA kann an niemanden senden.'),
|
||||
(d.whitelist || []).length ? '#34C759' : '#FF9F0A');
|
||||
} catch (e) { _mailStatus('✗ ' + e.message, '#FF6B6B'); }
|
||||
}
|
||||
|
||||
async function testMailConfig() {
|
||||
_mailStatus('Teste Anmeldung an beiden Servern …');
|
||||
try {
|
||||
const r = await fetch('/api/email-admin/test', { method: 'POST' });
|
||||
const d = await r.json();
|
||||
if (!r.ok) throw new Error(d.detail || ('HTTP ' + r.status));
|
||||
const line = (n, x) => x && x.ok ? `✓ ${n} ok` : `✗ ${n}: ${(x && x.error) || '?'}`;
|
||||
const ok = d.imap?.ok && d.smtp?.ok;
|
||||
_mailStatus(line('IMAP', d.imap) + '\n' + line('SMTP', d.smtp), ok ? '#34C759' : '#FF6B6B');
|
||||
} catch (e) { _mailStatus('✗ ' + e.message, '#FF6B6B'); }
|
||||
}
|
||||
|
||||
function openMailbox() {
|
||||
window.open('/email.html', 'aria-mailbox', 'width=1200,height=800');
|
||||
}
|
||||
|
||||
// ── Jellyfin-Server (Medien-Integration) ─────────────────────────
|
||||
let jellyfinServers = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user