586 lines
16 KiB
Markdown
586 lines
16 KiB
Markdown
# Mail-Adler Erweiterte Features
|
|
|
|
## 1. Datenbank: SQLite vs. MariaDB
|
|
|
|
### Analyse für ~3000 Mails
|
|
|
|
| Kriterium | SQLite | MariaDB |
|
|
|-----------|--------|---------|
|
|
| **Größe** | 1 Datei | Server-basiert |
|
|
| **3000 Mails Größe** | ~500MB-1GB | ~100-200MB |
|
|
| **Backup** | ✅ Einfach (Datei kopieren) | ⚠️ MySQL-Dumps nötig |
|
|
| **Verschlüsselung** | ⚠️ SQLCipher (extra) | ✅ TLS ready |
|
|
| **Aufbewahrungsfristen** | ✅ Einfach (SQL Trigger) | ✅ Einfach (SQL Trigger) |
|
|
| **Performance** | ✅ Gut für lokal | ⚠️ Remote-Latenz |
|
|
| **Installation** | ✅ Qt-built-in | ⚠️ Server setup |
|
|
| **Deployment** | ✅ Mit App | ❌ Extern |
|
|
|
|
### EMPFEHLUNG: **SQLite + SQLCipher** (Phase B)
|
|
- Lokal = privat + schnell
|
|
- Einfaches Backup (Datei)
|
|
- Encryption eingebaut
|
|
- Keine Server-Abhängigkeit
|
|
|
|
**MariaDB später (Phase E+)** wenn Multi-User/Sync nötig
|
|
|
|
---
|
|
|
|
## 2. Gesetzliche Aufbewahrungsfristen + Auto-Löschung
|
|
|
|
### Implementierung
|
|
|
|
```python
|
|
# src/storage/retention_policy.py
|
|
|
|
class RetentionPolicy:
|
|
def __init__(self):
|
|
self.policies = {
|
|
# Deutschland/EU (DSGVO)
|
|
"GDPR": {
|
|
"email": 7 * 365, # 7 Jahre (falls geschäftsrelevant)
|
|
"attachment": 7 * 365,
|
|
"deleted_email": 30, # Gelöschte 30 Tage
|
|
"spam": 30, # Spam 30 Tage
|
|
},
|
|
# Schweiz (StG)
|
|
"CHE": {
|
|
"email": 7 * 365,
|
|
"attachment": 7 * 365,
|
|
"deleted_email": 30,
|
|
"spam": 30,
|
|
},
|
|
# USA (verschiedene Staaten)
|
|
"USA": {
|
|
"email": 5 * 365, # 5 Jahre
|
|
"attachment": 5 * 365,
|
|
"deleted_email": 30,
|
|
"spam": 30,
|
|
},
|
|
}
|
|
|
|
def schedule_auto_delete(self):
|
|
"""
|
|
Daily Job: Lösche alte Emails/Anhänge
|
|
"""
|
|
scheduler.add_job(
|
|
self.delete_old_emails,
|
|
'cron',
|
|
hour=3, # 03:00 nachts
|
|
minute=0
|
|
)
|
|
|
|
def delete_old_emails(self):
|
|
"""
|
|
Lösche Emails älter als retention_days
|
|
Speichere vorher Hash zum Audit
|
|
"""
|
|
region = Settings.retentionRegion() # GDPR, CHE, USA
|
|
policy = self.policies[region]
|
|
|
|
# Email löschen
|
|
cutoff_date = datetime.now() - timedelta(days=policy["email"])
|
|
old_emails = db.query(
|
|
"SELECT id, subject, date FROM emails WHERE date < ? AND folder != 'trash'",
|
|
cutoff_date
|
|
)
|
|
|
|
for email in old_emails:
|
|
# Audit Log speichern (bevor löschen)
|
|
audit_log.record({
|
|
"email_id": email["id"],
|
|
"subject": email["subject"],
|
|
"deleted_at": datetime.now(),
|
|
"reason": "retention_policy_auto_delete"
|
|
})
|
|
|
|
# Anhänge löschen
|
|
db.delete_attachments(email["id"])
|
|
|
|
# Email löschen
|
|
db.delete_email(email["id"])
|
|
|
|
log.info(f"Deleted {len(old_emails)} old emails")
|
|
|
|
# Spam löschen (schneller)
|
|
spam_cutoff = datetime.now() - timedelta(days=policy["spam"])
|
|
db.delete_emails(f"folder = 'spam' AND date < ?", spam_cutoff)
|
|
```
|
|
|
|
### UI: Aufbewahrungsrichtlinie einstellen
|
|
|
|
```
|
|
Einstellungen → Datenschutz
|
|
|
|
Aufbewahrungsrichtlinie:
|
|
├─ Land/Region: [GDPR - Deutschland/EU] ▼
|
|
│ └─ Emails: 7 Jahre
|
|
│ └─ Gelöschte: 30 Tage
|
|
│ └─ Spam: 30 Tage
|
|
│
|
|
├─ Auto-Löschung:
|
|
│ ☑ Aktiviert (täglich 03:00)
|
|
│ ☑ Audit-Log speichern
|
|
│
|
|
└─ Info: "Vollständige Compliance mit DSGVO"
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Anhänge: Lazy-Load (Nur bei Anklick herunterladen)
|
|
|
|
### Architektur
|
|
|
|
```cpp
|
|
// src/attachment/AttachmentManager.h/cpp
|
|
class AttachmentManager {
|
|
private:
|
|
struct AttachmentMetadata {
|
|
QString id;
|
|
QString filename;
|
|
QString mime_type;
|
|
int size; // Bytes
|
|
bool downloaded; // false = noch nicht heruntergeladen
|
|
QString local_path; // "" wenn nicht downloaded
|
|
};
|
|
|
|
public:
|
|
// Zeige Anhang-Preview (nur Metadaten)
|
|
QVector<AttachmentMetadata> getAttachmentsMetadata(QString emailId) {
|
|
// Keine Daten herunterladen, nur Info:
|
|
// Größe, Name, Typ anzeigen
|
|
}
|
|
|
|
// Download on Click
|
|
void downloadAttachment(QString attachmentId) {
|
|
// Erst wenn User klickt:
|
|
// 1. Download vom Server
|
|
// 2. In ~/.local/share/mail-adler/attachments/cache/
|
|
// 3. Beim Anklick öffnen
|
|
}
|
|
|
|
// Auto-Cleanup (nach Öffnen)
|
|
void autoCleanupOldAttachments() {
|
|
// Nach 7 Tagen gelöschte Attachments aus Cache löschen
|
|
// Originale bleiben im Email-Archive
|
|
}
|
|
};
|
|
```
|
|
|
|
### UI: Anhang-Anzeige
|
|
|
|
```
|
|
Email von alice@gmx.de
|
|
|
|
Subject: Dokumente für Projekt
|
|
|
|
Anhänge (3):
|
|
├─ 📄 Vertrag.pdf (2.3 MB) [⬇️ Download] [🔗 Öffnen]
|
|
├─ 📊 Budget.xlsx (1.2 MB) [⬇️ Download] [🔗 Öffnen]
|
|
└─ 🖼️ Logo.png (845 KB) [⬇️ Download] [🔗 Öffnen]
|
|
|
|
(Nur Name + Größe angezeigt, Download erst auf Klick)
|
|
```
|
|
|
|
---
|
|
|
|
## 4. UI: Ungelesene-Zähler + Navigation
|
|
|
|
### Ordner-Panel mit Zähler
|
|
|
|
```
|
|
📧 Eingang (23) ← 23 ungelesene
|
|
├─ 📂 Arbeit (8)
|
|
├─ 📂 Privat (5)
|
|
├─ 📂 Spam (10)
|
|
└─ 🗑️ Papierkorb (2)
|
|
|
|
[Gesendet]
|
|
[Entwürfe]
|
|
[Archiv]
|
|
```
|
|
|
|
### Click auf "(23)" → Erste ungelesen
|
|
|
|
```cpp
|
|
// src/ui/FolderPanel.h/cpp
|
|
void FolderPanel::onUnreadCountClicked(QString folder) {
|
|
// 1. Hole erste ungelesene Email
|
|
Email firstUnread = db.query(
|
|
"SELECT * FROM emails WHERE folder = ? AND unread = 1 ORDER BY date DESC LIMIT 1",
|
|
folder
|
|
);
|
|
|
|
// 2. Springe zu dieser Email
|
|
emit navigateToEmail(firstUnread.id);
|
|
|
|
// 3. Markiere als gelesen
|
|
email.markAsRead();
|
|
|
|
// 4. (Optional) Nächste ungelesen
|
|
// Wenn User Taste drückt (z.B. 'n' für next unread)
|
|
}
|
|
|
|
void MailListView::onKeyPressed(Qt::Key key) {
|
|
if (key == Qt::Key_N) { // 'n' = next unread
|
|
Email next = getNextUnreadInFolder();
|
|
if (next.id()) {
|
|
navigateToEmail(next.id);
|
|
next.markAsRead();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Tastatur-Shortcuts für Ungelesene
|
|
|
|
```
|
|
n → Nächste ungelesen
|
|
p → Vorherige ungelesen
|
|
u → Markiere als ungelesen
|
|
f → Markiere als gelesen
|
|
|
|
Beispiel:
|
|
User klickt auf (23) → Erste ungelesen wird angezeigt
|
|
User drückt 'n' → Nächste ungelesen
|
|
User drückt 'n' → Nächste ungelesen
|
|
... etc
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Ungelesene-Handling: Spam & Blockierte
|
|
|
|
### Spam-Check
|
|
|
|
```python
|
|
# src/email/UnreadHandler.py
|
|
class UnreadHandler:
|
|
def categorizeUnread(self, email):
|
|
"""
|
|
Prüfe: Ist ungelesene Email in Spam?
|
|
Ist ungelesene Email blockiert?
|
|
"""
|
|
|
|
# 1. Spam-Check
|
|
if email.folder == "spam":
|
|
return {
|
|
"unread": True,
|
|
"spam": True,
|
|
"blocked": False,
|
|
"action": "Nicht zählen in normalem Ungelesen"
|
|
}
|
|
|
|
# 2. Blockiert-Check
|
|
sender = email.from_address
|
|
if db.isBlocked(sender):
|
|
return {
|
|
"unread": True,
|
|
"spam": False,
|
|
"blocked": True,
|
|
"action": "Nicht zählen in normalem Ungelesen"
|
|
}
|
|
|
|
# 3. Normal
|
|
return {
|
|
"unread": True,
|
|
"spam": False,
|
|
"blocked": False,
|
|
"action": "Zähle in (23)"
|
|
}
|
|
```
|
|
|
|
### UI: Separate Zähler
|
|
|
|
```
|
|
📧 Eingang (23 normal) [🚫 5 blocked] [🚫 8 spam]
|
|
└─ 23 = nur legitim ungelesen
|
|
└─ 5 = blockierte Absender
|
|
└─ 8 = Spam
|
|
|
|
Wenn User auf (23) klickt:
|
|
→ Erste legitim ungelesen
|
|
|
|
Wenn User auf [🚫 5] klickt:
|
|
→ Erste blockierte (aber nicht vorschalten)
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Serienbriefe: Massenmails mit Vorlagen
|
|
|
|
### Implementierung
|
|
|
|
```cpp
|
|
// src/mail/MailMerge.h/cpp
|
|
class MailMerge {
|
|
public:
|
|
struct Template {
|
|
QString id;
|
|
QString name;
|
|
QString subject; // Mit {{var}} Platzhaltern
|
|
QString body; // Mit {{var}} Platzhaltern
|
|
QStringList variables; // ["name", "email", "company"]
|
|
};
|
|
|
|
void createSeriesEmail(Template tmpl, QVector<QMap<QString, QString>> data) {
|
|
"""
|
|
Erstelle Massen-Email aus Vorlage + Daten
|
|
|
|
data = [
|
|
{"name": "Alice", "email": "alice@...", "company": "ABC Ltd"},
|
|
{"name": "Bob", "email": "bob@...", "company": "XYZ Corp"},
|
|
]
|
|
|
|
Beispiel Vorlage:
|
|
Subject: Hallo {{name}}!
|
|
Body: Lieber {{name}},
|
|
{{company}} hat sich für unsere Services interessiert...
|
|
"""
|
|
|
|
for (auto &row : data) {
|
|
// 1. Ersetze {{var}} durch Wert
|
|
QString subject = tmpl.subject;
|
|
QString body = tmpl.body;
|
|
|
|
for (auto &[var, value] : row) {
|
|
subject.replace("{{" + var + "}}", value);
|
|
body.replace("{{" + var + "}}", value);
|
|
}
|
|
|
|
// 2. Erstelle Email
|
|
Email email;
|
|
email.to = row["email"];
|
|
email.subject = subject;
|
|
email.body = body;
|
|
email.delayed = true; // Verzögerter Versand
|
|
|
|
// 3. Speichern
|
|
m_pendingEmails.push_back(email);
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### UI: Serienbriefe Dialog
|
|
|
|
```
|
|
┌──────────────────────────────────┐
|
|
│ Serienbriefe │
|
|
├──────────────────────────────────┤
|
|
│ Vorlage: [Kundenangebot] ▼ │
|
|
│ │
|
|
│ Empfänger-Liste (CSV): │
|
|
│ [Durchsuchen...] │
|
|
│ ✓ header row (name, email, co) │
|
|
│ │
|
|
│ Preview: │
|
|
│ ┌────────────────────────────────┐
|
|
│ │Subject: Hallo Alice! │
|
|
│ │ │
|
|
│ │Lieber Alice, ABC Ltd hat... │
|
|
│ └────────────────────────────────┘
|
|
│ │
|
|
│ Versand: │
|
|
│ ☑ Verzögerter Versand │
|
|
│ └─ Nach Email: [1] Minute │
|
|
│ │
|
|
│ [Preview] [Senden] [Abbrechen] │
|
|
└──────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Verzögertes Versenden (Scheduled Send)
|
|
|
|
### Implementierung
|
|
|
|
```cpp
|
|
// src/mail/DelayedSend.h/cpp
|
|
class DelayedSend {
|
|
public:
|
|
struct ScheduledEmail {
|
|
QString id;
|
|
QString to;
|
|
QString subject;
|
|
QString body;
|
|
QDateTime sendAt; // Wann versenden
|
|
QString status; // "scheduled", "sending", "sent", "cancelled"
|
|
};
|
|
|
|
void scheduleEmail(Email email, QDateTime sendAt) {
|
|
"""
|
|
Plane Email-Versand für später
|
|
"""
|
|
ScheduledEmail scheduled;
|
|
scheduled.id = generateId();
|
|
scheduled.to = email.to;
|
|
scheduled.subject = email.subject;
|
|
scheduled.body = email.body;
|
|
scheduled.sendAt = sendAt;
|
|
scheduled.status = "scheduled";
|
|
|
|
db.insert("scheduled_emails", scheduled);
|
|
|
|
// Zeige Timer in Entwürfe
|
|
emit scheduledEmailCreated(scheduled);
|
|
}
|
|
|
|
void checkAndSendScheduled() {
|
|
"""
|
|
Alle 1 Minute prüfen: Welche Emails sind reif zum Versenden?
|
|
"""
|
|
auto now = QDateTime::currentDateTime();
|
|
|
|
auto ready = db.query(
|
|
"SELECT * FROM scheduled_emails WHERE sendAt <= ? AND status = 'scheduled'",
|
|
now
|
|
);
|
|
|
|
for (auto &email : ready) {
|
|
sendEmail(email);
|
|
db.update("scheduled_emails", email.id, {"status": "sent"});
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### UI: Entwürfe mit Timer
|
|
|
|
```
|
|
Entwürfe (3)
|
|
|
|
[📝 Kundenangebot für Alice]
|
|
├─ Status: Versand geplant
|
|
├─ Versend um: 2025-02-05 14:30
|
|
├─ Countdown: 2h 15min
|
|
└─ [❌ Abbrechen] [✏️ Bearbeiten]
|
|
|
|
[📝 Besprechungsnotizen]
|
|
├─ Status: Normal (nicht geplant)
|
|
|
|
[📝 Test Email]
|
|
├─ Status: Fehler beim Versand
|
|
├─ Fehler: "SMTP Error 550"
|
|
└─ [🔄 Erneut versuchen] [Löschen]
|
|
|
|
Versand abbrechen:
|
|
┌──────────────────────────┐
|
|
│ Email wird versendet in: │
|
|
│ │
|
|
│ ⏱ [████░░░░░░░░░] 30s │
|
|
│ │
|
|
│ [❌ Jetzt abbrechen] │
|
|
│ [▶️ Weiter mit 'E'] │
|
|
└──────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 8. AD/Microsoft Integration (Phase D+)
|
|
|
|
### LDAP + Outlook Sync
|
|
|
|
```python
|
|
# src/integration/MicrosoftAD.py
|
|
|
|
class MicrosoftADIntegration:
|
|
def __init__(self):
|
|
self.ldap_server = "ldap://ad.company.com"
|
|
self.graph_api = "https://graph.microsoft.com/v1.0"
|
|
|
|
def syncContacts(self):
|
|
"""
|
|
Hole Kontakte aus AD LDAP
|
|
"""
|
|
conn = ldap.initialize(self.ldap_server)
|
|
results = conn.search_s(
|
|
"OU=Users,DC=company,DC=com",
|
|
ldap.SCOPE_SUBTREE,
|
|
"(objectClass=person)",
|
|
['mail', 'displayName', 'telephoneNumber']
|
|
)
|
|
|
|
# Speichere in lokale Kontakt-Datenbank
|
|
for dn, attrs in results:
|
|
contact = {
|
|
"name": attrs['displayName'][0],
|
|
"email": attrs['mail'][0],
|
|
"phone": attrs.get('telephoneNumber', [''])[0],
|
|
"source": "AD"
|
|
}
|
|
db.save_contact(contact)
|
|
|
|
def syncCalendar(self, user_email):
|
|
"""
|
|
Hole Outlook-Kalender über MS Graph API
|
|
"""
|
|
# Benötigt OAuth2 Token
|
|
headers = {"Authorization": f"Bearer {self.get_token()}"}
|
|
|
|
response = requests.get(
|
|
f"{self.graph_api}/me/events",
|
|
headers=headers
|
|
)
|
|
|
|
for event in response.json()['value']:
|
|
cal_event = {
|
|
"title": event['subject'],
|
|
"start": event['start']['dateTime'],
|
|
"end": event['end']['dateTime'],
|
|
"organizer": event['organizer']['emailAddress']['address'],
|
|
"source": "outlook"
|
|
}
|
|
db.save_calendar_event(cal_event)
|
|
|
|
def showADAvailability(self, email):
|
|
"""
|
|
Zeige AD-User Verfügbarkeit im Kalender
|
|
"""
|
|
# Prüfe: Ist User frei/busy?
|
|
# Zeige in Kalender-UI
|
|
```
|
|
|
|
### UI: AD-Integration
|
|
|
|
```
|
|
Kontakte
|
|
|
|
Quelle: [Alle] [Lokal] [AD] [Outlook]
|
|
|
|
Alice Schmidt (AD)
|
|
├─ Email: alice@company.de
|
|
├─ Phone: +49-30-12345678
|
|
├─ Verfügbar: 10:30-11:30 (aus Outlook)
|
|
└─ [Termin vereinbaren]
|
|
|
|
Bob Müller (AD)
|
|
├─ Email: bob@company.de
|
|
├─ Verfügbar: Ganztag frei
|
|
└─ [Termin vereinbaren]
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Zusammenfassung: Erweiterte Features
|
|
|
|
| Feature | Phase | Priorität | Komplexität |
|
|
|---------|-------|-----------|-------------|
|
|
| SQLite → MariaDB | E+ | Niedrig | Hoch |
|
|
| Aufbewahrungsfristen | B+ | Hoch | Mittel |
|
|
| Lazy-Load Anhänge | B+ | Hoch | Mittel |
|
|
| Ungelesene-Zähler | B+ | Hoch | Niedrig |
|
|
| Serienbriefe | C | Mittel | Hoch |
|
|
| Verzögerter Versand | C | Mittel | Mittel |
|
|
| AD/Outlook Integration | D+ | Niedrig | Hoch |
|
|
|
|
### MVP (Must-Have Phase B+):
|
|
1. ✅ SQLite mit SQLCipher
|
|
2. ✅ Aufbewahrungsfristen (Auto-Löschung)
|
|
3. ✅ Lazy-Load Anhänge
|
|
4. ✅ Ungelesene-Zähler + Navigation
|
|
|
|
### Nice-to-Have (Phase C+):
|
|
5. ⏳ Serienbriefe
|
|
6. ⏳ Verzögerter Versand
|
|
7. ⏳ AD Integration
|