feat: Persist outbound emails, fix @UPDATE_TEAM_MEMBER parser, add per-entry delete

- Add sent_emails table to DB for persistent outbox logging
- send_email() now writes every outgoing mail (incl. errors) to sent_emails
- parse_agent_commands() passes agent_key/task_id as triggered_by metadata
- Fix @UPDATE_TEAM_MEMBER parser: now matches Identifier/TelegramID/Role/etc.
  format from system prompt (was expecting Email/Field/Value — never matched)
- update_team_member() called correctly via **kwargs (was positional args bug)
- Set Piotr telegram_id=1578034974 directly in DB
- email_log.html: two-tab UI (Inbox Journal + Outbox), click-to-expand body
- emails.html: per-message delete button in inbox list
- New routes: DELETE inbox (IMAP expunge), journal entry, sent entry
This commit is contained in:
eric 2026-02-21 18:43:21 +00:00
parent 99df910497
commit f6ad727bf0
3 changed files with 340 additions and 165 deletions

244
app.py
View file

@ -290,6 +290,20 @@ def init_journal():
)
""")
# Outbox-Tabelle für ausgehende Emails
con.execute("""
CREATE TABLE IF NOT EXISTS sent_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
to_address TEXT NOT NULL,
subject TEXT,
body TEXT,
sent_at TEXT NOT NULL,
triggered_by TEXT,
task_id INTEGER,
status TEXT DEFAULT 'sent'
)
""")
# App-Settings Tabelle für globale Einstellungen
con.execute("""
CREATE TABLE IF NOT EXISTS app_settings (
@ -954,13 +968,13 @@ def respond_to_message(message_id, response):
return True
return False
def parse_agent_commands(agent_key, response_text):
def parse_agent_commands(agent_key, response_text, task_id=None):
"""Parst Agent-Antwort nach Orchestrator-Kommandos und führt sie aus."""
import re
# ASK_ORCHESTRATOR: Agent stellt Frage an Orchestrator
ask_requests = re.findall(
r'@ASK_ORCHESTRATOR\s*\nQuestion:\s*([^\n]+)\s*\nContext:\s*([^@]+)@END',
r'@ASK_ORCHESTRATOR\s*\nQuestion:\s*([^\n]+)\s*\nContext:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -986,7 +1000,7 @@ Die Antwort wird an {agent_key} zurückgegeben.""",
# CREATE_SUBTASK: Agent möchte Subtask erstellen
subtask_requests = re.findall(
r'@CREATE_SUBTASK\s*\nTask:\s*([^\n]+)\s*\nRequirements:\s*([^@]+)@END',
r'@CREATE_SUBTASK\s*\nTask:\s*([^\n]+)\s*\nRequirements:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -1003,7 +1017,7 @@ Die Antwort wird an {agent_key} zurückgegeben.""",
# SUGGEST_AGENT: Agent schlägt neuen Agent vor
suggest_requests = re.findall(
r'@SUGGEST_AGENT\s*\nRole:\s*([^\n]+)\s*\nSkills:\s*([^\n]+)\s*\nReason:\s*([^@]+)@END',
r'@SUGGEST_AGENT\s*\nRole:\s*([^\n]+)\s*\nSkills:\s*([^\n]+)\s*\nReason:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -1033,7 +1047,7 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
# READ_KNOWLEDGE: Agent möchte Wissensdatenbank durchsuchen
read_kb_requests = re.findall(
r'@READ_KNOWLEDGE\s*\nTopic:\s*([^@]+)@END',
r'@READ_KNOWLEDGE\s*\nTopic:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -1062,7 +1076,7 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
# SEND_EMAIL: Orchestrator sendet Email an Team-Member
send_email_requests = re.findall(
r'@SEND_EMAIL\s*\nTo:\s*([^\n]+)\s*\nSubject:\s*([^\n]+)\s*\nBody:\s*([^@]+)@END',
r'@SEND_EMAIL\s*\nTo:\s*([^\n]+)\s*\nSubject:\s*([^\n]+)\s*\nBody:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -1072,7 +1086,8 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
body_clean = body.strip()
# Versuche Email zu senden
success, message = send_email(to_clean, subject_clean, body_clean)
success, message = send_email(to_clean, subject_clean, body_clean,
triggered_by=f'agent:{agent_key}', task_id=task_id)
if success:
logger.info(f"[AgentCmd] Email gesendet an {to_clean}: {subject_clean}")
else:
@ -1080,7 +1095,7 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
# SEND_TELEGRAM: Orchestrator sendet Telegram-Nachricht
send_telegram_requests = re.findall(
r'@SEND_TELEGRAM\s*\nTo:\s*([^\n]+)\s*\nMessage:\s*([^@]+)@END',
r'@SEND_TELEGRAM\s*\nTo:\s*([^\n]+)\s*\nMessage:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -1116,7 +1131,7 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
# ADD_TEAM_MEMBER: Füge neues Team-Mitglied hinzu
add_member_requests = re.findall(
r'@ADD_TEAM_MEMBER\s*\nName:\s*([^\n]+)\s*\nEmail:\s*([^\n]+)\s*\nRole:\s*([^\n]+)\s*\nResponsibilities:\s*([^@]+)@END',
r'@ADD_TEAM_MEMBER\s*\nName:\s*([^\n]+)\s*\nEmail:\s*([^\n]+)\s*\nRole:\s*([^\n]+)\s*\nResponsibilities:\s*(.*?)@END',
response_text,
re.DOTALL
)
@ -1133,50 +1148,41 @@ Bitte entscheide ob dieser Agent erstellt werden soll.""",
logger.warning(f"[AgentCmd] Team-Member konnte nicht hinzugefügt werden: {name_clean}")
# UPDATE_TEAM_MEMBER: Aktualisiere Team-Mitglied
update_member_requests = re.findall(
r'@UPDATE_TEAM_MEMBER\s*\nEmail:\s*([^\n]+)\s*\nField:\s*([^\n]+)\s*\nValue:\s*([^@]+)@END',
response_text,
re.DOTALL
)
for email, field, value in update_member_requests:
email_clean = email.strip()
field_clean = field.strip().lower()
value_clean = value.strip()
# Hole aktuelles Team-Member
con = sqlite3.connect(EMAIL_JOURNAL_DB)
member = con.execute(
"SELECT name, role, email, responsibilities FROM team_members WHERE LOWER(email) = ?",
(email_clean.lower(),)
).fetchone()
if member:
# Update je nach Field
updates = {
'name': member[0],
'role': member[1],
'email': member[2],
'responsibilities': member[3]
}
if field_clean in updates:
updates[field_clean] = value_clean
success = update_team_member(
email_clean,
updates['name'],
updates['role'],
updates['responsibilities']
)
if success:
logger.info(f"[AgentCmd] Team-Member aktualisiert: {email_clean} - {field_clean}")
else:
logger.error(f"[AgentCmd] Update fehlgeschlagen für {email_clean}")
else:
logger.warning(f"[AgentCmd] Unbekanntes Field: {field_clean}")
# Format: @UPDATE_TEAM_MEMBER\nIdentifier: <email oder name>\n[Feld: Wert]\n...\n@END
ALLOWED_UPDATE_FIELDS = {
'name': 'name',
'role': 'role',
'responsibilities': 'responsibilities',
'email': 'email',
'telegramid': 'telegram_id',
'telegram_id': 'telegram_id',
'phone': 'phone',
}
for block in re.findall(r'@UPDATE_TEAM_MEMBER\s*\n(.*?)@END', response_text, re.DOTALL):
lines = [l.strip() for l in block.strip().splitlines() if l.strip()]
identifier = None
kwargs = {}
for line in lines:
if ':' not in line:
continue
key, _, val = line.partition(':')
key_norm = key.strip().lower().replace(' ', '_')
val_clean = val.strip()
if key_norm == 'identifier':
identifier = val_clean
elif key_norm in ALLOWED_UPDATE_FIELDS:
kwargs[ALLOWED_UPDATE_FIELDS[key_norm]] = val_clean
if not identifier:
logger.warning("[AgentCmd] @UPDATE_TEAM_MEMBER ohne Identifier ignoriert")
continue
if not kwargs:
logger.warning(f"[AgentCmd] @UPDATE_TEAM_MEMBER für '{identifier}' ohne Felder ignoriert")
continue
success = update_team_member(identifier, **kwargs)
if success:
logger.info(f"[AgentCmd] Team-Member aktualisiert: {identifier} - {list(kwargs.keys())}")
else:
logger.warning(f"[AgentCmd] Team-Member nicht gefunden: {email_clean}")
con.close()
logger.error(f"[AgentCmd] Update fehlgeschlagen für '{identifier}'")
def create_new_agent(agent_key, role, skills):
"""Erstellt dynamisch einen neuen Agenten."""
@ -1373,8 +1379,9 @@ def get_email_body(email_id):
return f'Fehler beim Abrufen der Email: {str(e)}'
def send_email(to_address, subject, body):
"""Sendet eine Email via SMTP"""
def send_email(to_address, subject, body, triggered_by='manual', task_id=None):
"""Sendet eine Email via SMTP und persistiert sie in der Outbox."""
now = datetime.now().isoformat()
try:
if not EMAIL_CONFIG['email_address'] or not EMAIL_CONFIG['email_password']:
return False, 'Email-Konfiguration erforderlich'
@ -1396,9 +1403,29 @@ def send_email(to_address, subject, body):
server.login(EMAIL_CONFIG['email_address'], EMAIL_CONFIG['email_password'])
server.send_message(msg)
server.quit()
# Ausgehende Email persistieren
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.execute(
"INSERT INTO sent_emails (to_address, subject, body, sent_at, triggered_by, task_id, status) VALUES (?,?,?,?,?,?,'sent')",
(to_address, subject, body, now, triggered_by, task_id)
)
con.commit()
con.close()
return True, 'Email erfolgreich versendet'
except Exception as e:
# Fehlgeschlagene Versuche auch loggen
try:
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.execute(
"INSERT INTO sent_emails (to_address, subject, body, sent_at, triggered_by, task_id, status) VALUES (?,?,?,?,?,?,'error')",
(to_address, subject, body, now, triggered_by, task_id)
)
con.commit()
con.close()
except Exception:
pass
return False, f'Fehler beim Versenden: {str(e)}'
def is_whitelisted(sender_address):
@ -1547,33 +1574,24 @@ async def telegram_message_handler(update: Update, context: ContextTypes.DEFAULT
def send_telegram_message(chat_id: int, message: str):
"""Sendet eine Nachricht an einen Telegram-Chat."""
global telegram_app
if not telegram_app or not TELEGRAM_CONFIG['bot_token']:
logging.warning("[Telegram] Cannot send message: Bot not configured")
"""Sendet eine Nachricht an einen Telegram-Chat via direktem HTTP-Request."""
token = TELEGRAM_CONFIG.get('bot_token')
if not token:
logging.warning("[Telegram] Cannot send message: Bot token not configured")
return False
try:
import asyncio
# In die laufende Event-Loop des Telegram-Threads senden (thread-safe)
loop = telegram_app.updater.application.updater._network_loop if hasattr(telegram_app, 'updater') else None
if loop is None:
# Fallback: Loop des Telegram-Threads direkt holen
loop = telegram_app.loop if hasattr(telegram_app, 'loop') else None
if loop and loop.is_running():
future = asyncio.run_coroutine_threadsafe(
telegram_app.bot.send_message(chat_id=chat_id, text=message),
loop
)
future.result(timeout=30)
import requests as req
r = req.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={'chat_id': chat_id, 'text': message},
timeout=15
)
if r.ok:
logging.info(f"[Telegram] Nachricht an {chat_id} gesendet.")
return True
else:
# Letzter Fallback: eigene Loop (kein Telegram-Bot läuft)
loop = asyncio.new_event_loop()
loop.run_until_complete(telegram_app.bot.send_message(chat_id=chat_id, text=message))
loop.close()
return True
logging.error(f"[Telegram] API Fehler: {r.status_code} {r.text}")
return False
except Exception as e:
logging.error(f"[Telegram] Error sending message: {e}")
return False
@ -2012,6 +2030,12 @@ def process_beat_tasks():
task['response'] = response
logger.info("[TaskBeat] Task #%d abgeschlossen.", task['id'])
# Agent-Kommandos parsen (@SEND_EMAIL, @UPDATE_TEAM_MEMBER, etc.)
try:
parse_agent_commands('orchestrator', response, task_id=task['id'])
except Exception as e:
logger.error("[TaskBeat] parse_agent_commands Fehler: %s", str(e))
# Telegram-Antwort senden
if task.get('type') == 'telegram' and task.get('telegram_chat_id'):
try:
@ -3006,13 +3030,69 @@ def view_email(email_id):
return {'content': body}
@app.route('/emails/inbox/<imap_uid>/delete', methods=['POST'])
@login_required
def delete_inbox_email(imap_uid):
"""Löscht eine einzelne Email aus dem IMAP-Posteingang (verschiebt in Trash)."""
if not (EMAIL_CONFIG['email_address'] and EMAIL_CONFIG['email_password']):
flash('Email-Konfiguration erforderlich', 'danger')
return redirect(url_for('emails'))
try:
mail = imaplib.IMAP4_SSL(EMAIL_CONFIG['imap_server'], EMAIL_CONFIG['imap_port'])
mail.login(EMAIL_CONFIG['email_address'], EMAIL_CONFIG['email_password'])
mail.select('INBOX')
mail.store(imap_uid, '+FLAGS', '\\Deleted')
mail.expunge()
mail.close()
mail.logout()
flash('Email aus Posteingang gelöscht.', 'success')
except Exception as e:
flash(f'Fehler beim Löschen: {e}', 'danger')
return redirect(url_for('emails'))
@app.route('/emails/journal/<path:message_id>/delete', methods=['POST'])
@login_required
def delete_journal_entry(message_id):
"""Löscht einen einzelnen Journal-Eintrag (eingehende Emails)."""
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.execute("DELETE FROM email_journal WHERE message_id = ?", (message_id,))
con.commit()
con.close()
flash('Journal-Eintrag gelöscht.', 'success')
return redirect(url_for('email_log_view'))
@app.route('/emails/sent/<int:sent_id>/delete', methods=['POST'])
@login_required
def delete_sent_email(sent_id):
"""Löscht einen einzelnen Outbox-Eintrag."""
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.execute("DELETE FROM sent_emails WHERE id = ?", (sent_id,))
con.commit()
con.close()
flash('Gesendete Email aus Log gelöscht.', 'success')
return redirect(url_for('email_log_view'))
@app.route('/email-log')
@login_required
def email_log_view():
"""Zeigt das Email-Verarbeitungs-Log."""
with email_log_lock:
log_entries = list(reversed(email_log)) # Neueste zuerst
return render_template('email_log.html', agents=AGENTS, log_entries=log_entries)
"""Zeigt Inbox-Journal und Outbox-Log."""
# Eingehende: aus DB
con = sqlite3.connect(EMAIL_JOURNAL_DB)
con.row_factory = sqlite3.Row
journal_rows = con.execute(
"SELECT * FROM email_journal ORDER BY received_at DESC"
).fetchall()
# Ausgehende: aus DB
sent_rows = con.execute(
"SELECT * FROM sent_emails ORDER BY sent_at DESC"
).fetchall()
con.close()
return render_template('email_log.html', agents=AGENTS,
journal_rows=journal_rows,
sent_rows=sent_rows)
@app.route('/settings', methods=['GET', 'POST'])

View file

@ -4,87 +4,177 @@
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between">
<div>
<h1>Email-Verarbeitungs-Log</h1>
<p>Automatisch verarbeitete Emails und Antworten</p>
</div>
<div class="d-flex gap-2 align-items-center">
<span class="badge bg-secondary" style="font-size:.85rem;">{{ log_entries|length }} Einträge</span>
<button class="btn btn-secondary btn-sm" onclick="location.reload()">Aktualisieren</button>
<h1>Email-Log</h1>
<p>Posteingang-Journal und gesendete Emails</p>
</div>
<button class="btn btn-secondary btn-sm" onclick="location.reload()">Aktualisieren</button>
</div>
<div class="mb-3" style="font-size:.8rem;color:var(--text-muted);">
<span class="status-replied me-3">✓ replied</span> — Auto-Reply versendet &nbsp;|&nbsp;
<span class="status-skipped me-3">— skipped</span> — Nicht auf Whitelist &nbsp;|&nbsp;
<span class="status-error">✗ error</span> — Fehler beim Versenden
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="emailLogTabs">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-inbox">
Posteingang / Journal
<span class="badge bg-secondary ms-1">{{ journal_rows|length }}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-sent">
Gesendet
<span class="badge bg-primary ms-1">{{ sent_rows|length }}</span>
</a>
</li>
</ul>
{% if not log_entries %}
<div class="card">
<div class="card-body text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📭</p>
<p>Noch keine Emails verarbeitet.<br><small>Der Poller prüft alle 2 Minuten den Posteingang.</small></p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover log-table mb-0">
<thead>
<tr>
<th>Zeitstempel</th>
<th>Von</th>
<th>Betreff</th>
<th>Agent</th>
<th>Status</th>
<th>Antwort-Vorschau</th>
</tr>
</thead>
<tbody>
{% for entry in log_entries %}
<tr>
<td style="white-space:nowrap;"><small style="color:var(--text-muted);">{{ entry.timestamp }}</small></td>
<td><small>{{ entry.from }}</small></td>
<td>{{ entry.subject }}</td>
<td>
{% if entry.agent %}
<span class="badge bg-primary badge-agent">{{ entry.agent }}</span>
{% else %}
<span style="color:var(--text-muted);"></span>
{% endif %}
</td>
<td>
{% if entry.status == 'replied' or entry.status == 'completed' %}
<span class="status-replied">✓ replied</span>
{% elif entry.status == 'skipped' %}
<span class="status-skipped">— skipped</span>
{% elif entry.status == 'error' %}
<span class="status-error">✗ error</span>
{% elif entry.status == 'queued' %}
<span style="color:var(--warning);">⏳ queued</span>
{% else %}
<span style="color:var(--text-muted);">{{ entry.status }}</span>
{% endif %}
</td>
<td>
{% if entry.response_preview %}
<div class="response-preview">{{ entry.response_preview }}</div>
{% else %}
<small style="color:var(--text-muted);"></small>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="tab-content">
<!-- ── TAB: Posteingang / Journal ── -->
<div class="tab-pane fade show active" id="tab-inbox">
{% if not journal_rows %}
<div class="card">
<div class="card-body text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📭</p>
<p>Keine Journal-Einträge vorhanden.</p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover log-table mb-0">
<thead>
<tr>
<th>Empfangen</th>
<th>Von</th>
<th>Betreff</th>
<th>Agent</th>
<th>Status</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
{% for row in journal_rows %}
<tr>
<td style="white-space:nowrap;">
<small style="color:var(--text-muted);">{{ row.received_at[:16] if row.received_at else '—' }}</small>
</td>
<td><small>{{ row.sender or '—' }}</small></td>
<td>{{ row.subject or '—' }}</td>
<td>
{% if row.agent_key %}
<span class="badge bg-primary badge-agent">{{ row.agent_key }}</span>
{% else %}
<span style="color:var(--text-muted);"></span>
{% endif %}
</td>
<td>
{% if row.status == 'completed' %}
<span class="status-replied">✓ completed</span>
{% elif row.status == 'skipped' %}
<span class="status-skipped">— skipped</span>
{% elif row.status == 'error' %}
<span class="status-error">✗ error</span>
{% elif row.status == 'queued' %}
<span style="color:var(--warning);">⏳ queued</span>
{% else %}
<span style="color:var(--text-muted);">{{ row.status }}</span>
{% endif %}
</td>
<td>
<form method="POST" action="/emails/journal/{{ row.message_id | urlencode }}/delete"
onsubmit="return confirm('Journal-Eintrag löschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen"></button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="mt-3" style="font-size:.78rem;color:var(--text-muted);">
Whitelist: <strong>eric.fischer@signtime.media</strong>, <strong>p.dyderski@live.at</strong>,
<strong>georg.tschare@gmail.com</strong>, <strong>*@diversityball.at</strong> · Max. 50 Einträge
</div>
<!-- ── TAB: Gesendet ── -->
<div class="tab-pane fade" id="tab-sent">
{% if not sent_rows %}
<div class="card">
<div class="card-body text-center py-5" style="color:var(--text-muted);">
<p style="font-size:2rem;">📤</p>
<p>Noch keine gesendeten Emails im Log.<br>
<small>Ab jetzt werden alle ausgehenden Emails hier gespeichert.</small>
</p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover log-table mb-0">
<thead>
<tr>
<th>Gesendet</th>
<th>An</th>
<th>Betreff</th>
<th>Ausgelöst von</th>
<th>Status</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
{% for row in sent_rows %}
<tr style="cursor:pointer;" onclick="toggleBody({{ row.id }})">
<td style="white-space:nowrap;">
<small style="color:var(--text-muted);">{{ row.sent_at[:16] if row.sent_at else '—' }}</small>
</td>
<td><small>{{ row.to_address or '—' }}</small></td>
<td>{{ row.subject or '—' }}</td>
<td>
<small style="color:var(--text-muted);">
{{ row.triggered_by or 'manual' }}
{% if row.task_id %}<span class="badge bg-secondary ms-1">Task #{{ row.task_id }}</span>{% endif %}
</small>
</td>
<td>
{% if row.status == 'sent' %}
<span class="status-replied">✓ gesendet</span>
{% elif row.status == 'error' %}
<span class="status-error">✗ Fehler</span>
{% else %}
<span style="color:var(--text-muted);">{{ row.status }}</span>
{% endif %}
</td>
<td onclick="event.stopPropagation()">
<form method="POST" action="/emails/sent/{{ row.id }}/delete"
onsubmit="return confirm('Eintrag löschen?')">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen"></button>
</form>
</td>
</tr>
<!-- Expandable body row -->
<tr id="body-{{ row.id }}" style="display:none;">
<td colspan="6" style="background:var(--bg-elevated);padding:1rem;">
<pre style="white-space:pre-wrap;font-size:.82rem;margin:0;color:var(--text-primary);">{{ row.body or '(kein Inhalt)' }}</pre>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div><!-- /.tab-content -->
{% endblock %}
{% block scripts %}
<script>
function toggleBody(id) {
const row = document.getElementById('body-' + id);
if (row) row.style.display = row.style.display === 'none' ? '' : 'none';
}
</script>
{% endblock %}

View file

@ -60,14 +60,19 @@
{% else %}
<ul class="list-group list-group-flush">
{% for mail in emails %}
<li class="list-group-item list-group-item-action"
style="cursor:pointer;" onclick="viewEmail('{{ mail.id }}', '{{ mail.subject|e }}', '{{ mail.from|e }}')">
<div class="d-flex justify-content-between align-items-start">
<strong style="font-size:.875rem;">{{ mail.subject }}</strong>
<small style="color:var(--text-muted);white-space:nowrap;margin-left:.5rem;">{{ mail.date[:10] }}</small>
<li class="list-group-item list-group-item-action d-flex align-items-start gap-2" style="cursor:pointer;">
<div class="flex-grow-1" onclick="viewEmail('{{ mail.id }}', '{{ mail.subject|e }}', '{{ mail.from|e }}')">
<div class="d-flex justify-content-between align-items-start">
<strong style="font-size:.875rem;">{{ mail.subject }}</strong>
<small style="color:var(--text-muted);white-space:nowrap;margin-left:.5rem;">{{ mail.date[:10] }}</small>
</div>
<div style="font-size:.8rem;color:var(--text-muted);">{{ mail.from }}</div>
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.2rem;">{{ mail.preview }}</div>
</div>
<div style="font-size:.8rem;color:var(--text-muted);">{{ mail.from }}</div>
<div style="font-size:.78rem;color:var(--text-muted);margin-top:.2rem;">{{ mail.preview }}</div>
<form method="POST" action="/emails/inbox/{{ mail.id }}/delete"
onsubmit="return confirm('Email aus Posteingang löschen?')" style="flex-shrink:0;">
<button type="submit" class="btn btn-sm btn-outline-danger" title="Löschen"></button>
</form>
</li>
{% endfor %}
</ul>