Name shortcut zu mailadler
This commit is contained in:
585
ERWEITERTE_FEATURES.md
Normal file
585
ERWEITERTE_FEATURES.md
Normal file
@@ -0,0 +1,585 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user