Compare commits

..

3 Commits

Author SHA1 Message Date
NiekRabelink 85850f47ad Merge pull request 'Dev' (#93) from dev into main
Reviewed-on: #93
2026-09-15 22:32:58 +00:00
NiekRabelink 45a883319a Outlook changes 2026-09-16 00:32:29 +02:00
NiekRabelink 460fa967ee Jira 28 2026-09-15 22:46:21 +02:00
4 changed files with 1184 additions and 467 deletions
+13 -4
View File
@@ -22,13 +22,22 @@ header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
try {
// 1. Inbox berichten ophalen
if ($action === 'get_inbox') {
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 25;
// 0. Mail mappen ophalen
if ($action === 'get_folders') {
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$res = microsoft_fetch_mail_folders($userId, $forceRefresh);
echo json_encode($res);
exit;
}
// 1. Inbox of specifieke map berichten ophalen
if ($action === 'get_folder_messages' || $action === 'get_inbox') {
$folderId = trim($_GET['folder_id'] ?? ($_POST['folder_id'] ?? 'inbox'));
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 30;
$unreadOnly = !empty($_GET['unread_only']) && $_GET['unread_only'] === '1';
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$res = microsoft_fetch_inbox_messages($userId, $limit, $unreadOnly, $forceRefresh);
$res = microsoft_fetch_folder_messages($userId, $folderId, $limit, $unreadOnly, $forceRefresh);
echo json_encode($res);
exit;
}
+369 -13
View File
@@ -940,11 +940,156 @@ function microsoft_reconcile_tasks_with_outlook(int $userId, string $startDate,
/**
* Levert realistische demo e-mails voor accounts zonder actieve Outlook verbinding
*/
function microsoft_get_mail_demo_data(): array {
function microsoft_get_mail_demo_data(?string $folderId = 'inbox'): array {
$now = time();
$fId = strtolower(trim($folderId ?: 'inbox'));
if ($fId === 'demo-folder-dx') {
return [
[
'id' => 'demo-mail-dx-1',
'folder_id' => 'demo-folder-dx',
'subject' => '⚡ Update: REST endpoint v2 specificatie voor UWV & Koppelingen',
'body_preview' => 'Beste collega, hierbij de bijgewerkte documentatie voor de REST endpoints en de schema-wijzigingen voor de datadump...',
'body_html' => '<p>Beste collega,</p><p>Hierbij de bijgewerkte documentatie voor de <strong>REST endpoints</strong> en de schema-wijzigingen voor de datadump.</p><p>Graag controleren of de data mapping aansluit bij onze migratiestappen.</p><p>Met vriendelijke groet,<br><strong>Team Koppelingen & DX</strong></p>',
'from_name' => 'Koppelingen Team DX',
'from_email' => 'dx-koppelingen@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (25 * 60)),
'received_human' => '25 min geleden',
'is_read' => false,
'has_attachments' => true,
'importance' => 'high',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
[
'id' => 'demo-mail-dx-2',
'folder_id' => 'demo-folder-dx',
'subject' => 'Migratie draaiboek koppelingen fase 3 gereed',
'body_preview' => 'Het draaiboek voor de uitrol van de vernieuwde interfaces is goedgekeurd door de lead architect...',
'body_html' => '<p>Het draaiboek voor de uitrol van de vernieuwde interfaces is goedgekeurd door de lead architect. Zie de bijlage voor de tijdsplanning.</p>',
'from_name' => 'Lead Architect',
'from_email' => 'architect@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (4 * 3600)),
'received_human' => '4 uur geleden',
'is_read' => true,
'has_attachments' => false,
'importance' => 'normal',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
[
'id' => 'demo-mail-dx-3',
'folder_id' => 'demo-folder-dx',
'subject' => 'SSO Certificaat verificatie voor staging omgeving',
'body_preview' => 'Het SSL/TLS certificaat voor de authenticatiekoppeling verloopt over 30 dagen. Het nieuwe certificaat is klaargezet...',
'body_html' => '<p>Het SSL/TLS certificaat voor de authenticatiekoppeling verloopt over 30 dagen. Het nieuwe certificaat is klaargezet op de staging server.</p>',
'from_name' => 'Security & Certificaten',
'from_email' => 'security@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (26 * 3600)),
'received_human' => 'Gisteren',
'is_read' => true,
'has_attachments' => false,
'importance' => 'normal',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
];
}
if ($fId === 'demo-folder-klanten') {
return [
[
'id' => 'demo-mail-klant-1',
'folder_id' => 'demo-folder-klanten',
'subject' => 'Project kick-off partnerportaal integratie',
'body_preview' => 'Beste Fociz team, bedankt voor de toelichting gisteren. We kijken uit naar de gezamenlijke sprintstart volgende maand...',
'body_html' => '<p>Beste Fociz team,</p><p>Bedankt voor de toelichting gisteren. We kijken uit naar de gezamenlijke sprintstart volgende maand.</p><p>Vriendelijke groet,<br><strong>Partner Relatiemanager</strong></p>',
'from_name' => 'Partner Relaties',
'from_email' => 'partners@klantportaal.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (5 * 3600)),
'received_human' => '5 uur geleden',
'is_read' => true,
'has_attachments' => false,
'importance' => 'normal',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
[
'id' => 'demo-mail-klant-2',
'folder_id' => 'demo-folder-klanten',
'subject' => 'Vraag over API sleutels en documentatie',
'body_preview' => 'Hallo, kunnen we de sandbox tokens alvast ontvangen voor onze ontwikkelaars?...',
'body_html' => '<p>Hallo, kunnen we de sandbox tokens alvast ontvangen voor onze ontwikkelaars? Alvast dank!</p>',
'from_name' => 'IT Lead Klant',
'from_email' => 'tech@partnerbedrijf.com',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (28 * 3600)),
'received_human' => 'Gisteren',
'is_read' => true,
'has_attachments' => false,
'importance' => 'normal',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
];
}
if ($fId === 'demo-folder-fin') {
return [
[
'id' => 'demo-mail-fin-1',
'folder_id' => 'demo-folder-fin',
'subject' => 'Nieuwe factuur September licenties ter goedkeuring',
'body_preview' => 'Beste directie, bijgaand de specificatie van de maandelijks cloud infrastructuur kosten ter akkoord...',
'body_html' => '<p>Beste directie,</p><p>Bijgaand de specificatie van de maandelijkse cloud infrastructuur kosten ter akkoord. Graag uiterlijk vrijdag verwerken.</p>',
'from_name' => 'Financiële Administratie',
'from_email' => 'facturen@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (40 * 60)),
'received_human' => '40 min geleden',
'is_read' => false,
'has_attachments' => true,
'importance' => 'high',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
[
'id' => 'demo-mail-fin-2',
'folder_id' => 'demo-folder-fin',
'subject' => 'Betaalbevestiging software koppelingen',
'body_preview' => 'Uw betaling voor de kwartaallicenties is succesvol verwerkt...',
'body_html' => '<p>Uw betaling voor de kwartaallicenties is succesvol verwerkt. Factuurnummer INV-2026-09.</p>',
'from_name' => 'Accounts Payable',
'from_email' => 'billing@cloudvendor.com',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)),
'received_human' => '2 dagen geleden',
'is_read' => true,
'has_attachments' => false,
'importance' => 'low',
'web_link' => 'https://outlook.office.com/mail/',
'is_demo' => true,
],
];
}
// Standaard Inbox demo berichten
return [
[
'id' => 'demo-mail-1',
'folder_id' => 'inbox',
'subject' => '⚡ Urgent: UWV Koppeling REST-endpoints en Datamigratie timing',
'body_preview' => 'Beste collega, we hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de UWV REST-endpoints en de validatie van werknemersdossiers...',
'body_html' => '<p>Beste collega,</p><p>We hebben vanuit de overheidsspecificaties aanvullende eisen ontvangen omtrent de <strong>UWV REST-endpoints</strong> en de validatie van werknemersdossiers.</p><p>Zou je kunnen verifiëren of onze SSO-koppeling en de datadump export hier al op aansluiten? Het zou fijn zijn als we dit vanmiddag kort kunnen afstemmen in de planning.</p><p>Met vriendelijke groet,<br><strong>Directie UWV Koppelingen & Integraties</strong></p>',
@@ -952,7 +1097,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'koppelingen@uwv-overheid.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (12 * 60)), // 12 minuten geleden
'received_at' => date('Y-m-d H:i:s', $now - (12 * 60)),
'received_human' => '12 min geleden',
'is_read' => false,
'has_attachments' => true,
@@ -962,6 +1107,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-2',
'folder_id' => 'inbox',
'subject' => 'Factuur & Urenverantwoording September — Goedkeuring vereist',
'body_preview' => 'Hallo team, bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de week accorderen...',
'body_html' => '<p>Hallo team,</p><p>Bijgaand tref je de voorlopige urenstaat en factuurspecificatie aan voor de lopende sprint. Graag voor het einde van de werkweek accorderen in het systeem.</p><p>Laat het weten indien er correcties nodig zijn op de dagregistraties.</p><p>Hartelijke groet,<br><strong>Financiële Administratie Fociz</strong></p>',
@@ -969,7 +1115,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'facturatie@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (65 * 60)), // 1 uur geleden
'received_at' => date('Y-m-d H:i:s', $now - (65 * 60)),
'received_human' => '1 uur geleden',
'is_read' => false,
'has_attachments' => false,
@@ -979,6 +1125,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-3',
'folder_id' => 'inbox',
'subject' => 'Jira SSO Migratie & Staging server testomgeving',
'body_preview' => 'Goedemiddag, de testomgeving voor de nieuwe Atlassian API migratie (CHANGE-2046) is succesvol klaargezet. We kunnen starten met de verificatietesten...',
'body_html' => '<p>Goedemiddag,</p><p>De testomgeving voor de nieuwe Atlassian API migratie (CHANGE-2046) is succesvol klaargezet. We kunnen starten met de verificatietesten op de Servicedesk wachtrijen en DX filters.</p><p>Alle endpoint logs zijn beschikbaar via het beheerpaneel.</p><p>Groeten,<br><strong>Team DevOps & Infrastructuur</strong></p>',
@@ -986,7 +1133,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'infra@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (3 * 3600)), // 3 uur geleden
'received_at' => date('Y-m-d H:i:s', $now - (3 * 3600)),
'received_human' => '3 uur geleden',
'is_read' => true,
'has_attachments' => true,
@@ -996,6 +1143,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-4',
'folder_id' => 'inbox',
'subject' => 'Herinnering: Kwartaal Roadmap Review & Strategie sessie',
'body_preview' => 'Beste collega\'s, vergeet niet je prioriteiten en feature requests in te dienen in de Fociz Roadmap module voor de meeting van aanstaande donderdag...',
'body_html' => '<p>Beste collega\'s,</p><p>Vergeet niet je prioriteiten en feature requests in te dienen in de <strong>Fociz Roadmap</strong> module voor de meeting van aanstaande donderdag.</p><p>We behandelen dan tevens de nieuwe Kanban borden en de Outlook time-blocking ervaring.</p><p>Tot dan!<br><strong>Product Management</strong></p>',
@@ -1003,7 +1151,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'product@fociz.nl',
'to_recipients' => [['name' => 'Alle Medewerkers', 'address' => 'team@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (24 * 3600)), // Gisteren
'received_at' => date('Y-m-d H:i:s', $now - (24 * 3600)),
'received_human' => 'Gisteren',
'is_read' => true,
'has_attachments' => false,
@@ -1013,6 +1161,7 @@ function microsoft_get_mail_demo_data(): array {
],
[
'id' => 'demo-mail-5',
'folder_id' => 'inbox',
'subject' => 'Welkom bij Fociz Live — Handige tips voor jouw dagplanning',
'body_preview' => 'Welkom bij Fociz! Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw dagrooster?...',
'body_html' => '<p>Welkom bij Fociz!</p><p>Wist je dat je binnengekomen Outlook e-mails en Jira tickets met één klik direct kunt inplannen in jouw persoonlijke dagplanning?</p><p>Hierdoor houd je maximale grip op je werkdag en behoud je de ultieme focus.</p><p>Veel productiviteit gewenst,<br><strong>Het Fociz Team</strong></p>',
@@ -1020,7 +1169,7 @@ function microsoft_get_mail_demo_data(): array {
'from_email' => 'no-reply@fociz.nl',
'to_recipients' => [['name' => 'Collega', 'address' => 'collega@fociz.nl']],
'cc_recipients' => [],
'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)), // 2 dagen geleden
'received_at' => date('Y-m-d H:i:s', $now - (48 * 3600)),
'received_human' => '2 dagen geleden',
'is_read' => true,
'has_attachments' => false,
@@ -1032,10 +1181,10 @@ function microsoft_get_mail_demo_data(): array {
}
/**
* Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data)
* Haalt alle beschikbare mailmappen op van Microsoft Graph API (exclusief SPAM/Junk, Prullenbak/Deleted, Outbox en Concepten)
*/
function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unreadOnly = false, bool $forceRefresh = false): array {
$cacheKey = "ms_inbox_{$userId}_{$limit}_" . ($unreadOnly ? '1' : '0');
function microsoft_fetch_mail_folders(int $userId, bool $forceRefresh = false): array {
$cacheKey = "ms_folders_{$userId}";
if (!$forceRefresh) {
$cached = fociz_cache_get($cacheKey);
if ($cached !== null && is_array($cached)) {
@@ -1050,13 +1199,208 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$isLive = !empty($integration['access_token']) && empty($integration['is_demo_mode']);
if (!$isLive) {
$demo = microsoft_get_mail_demo_data();
$demoFolders = [
[
'id' => 'inbox',
'name' => 'Postvak IN',
'unread_count' => 2,
'total_count' => 5,
'is_inbox' => true,
],
[
'id' => 'demo-folder-dx',
'name' => 'Projecten & DX Koppelingen',
'unread_count' => 1,
'total_count' => 3,
'is_inbox' => false,
],
[
'id' => 'demo-folder-klanten',
'name' => 'Klanten & Partners',
'unread_count' => 0,
'total_count' => 2,
'is_inbox' => false,
],
[
'id' => 'demo-folder-fin',
'name' => 'Financiën & Facturatie',
'unread_count' => 1,
'total_count' => 2,
'is_inbox' => false,
],
];
$totalUnread = array_sum(array_column($demoFolders, 'unread_count'));
return [
'success' => true,
'is_demo' => true,
'folders' => $demoFolders,
'total_unread' => $totalUnread,
];
}
$token = microsoft_get_valid_token($userId);
if (!$token) {
return [
'success' => false,
'error' => 'Geen geldig Microsoft 365 token beschikbaar.',
'folders' => [],
'total_unread' => 0,
];
}
$url = MS_GRAPH_BASE_URL . '/me/mailFolders?$top=100&$select=id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount,wellKnownName';
$res = fociz_http_request($url, [
'method' => 'GET',
'headers' => [
'Authorization' => "Bearer {$token}",
'Accept' => 'application/json',
],
'timeout' => 15,
]);
if ($res['status'] !== 200 || !isset($res['json']['value'])) {
$graphError = $res['json']['error']['message'] ?? ($res['error'] ?? 'HTTP ' . $res['status']);
error_log("Microsoft fetch folders error: HTTP {$res['status']} - {$graphError}");
return [
'success' => false,
'error' => $graphError,
'folders' => [
[
'id' => 'inbox',
'name' => 'Postvak IN',
'unread_count' => 0,
'total_count' => 0,
'is_inbox' => true,
]
],
'total_unread' => 0,
];
}
$rawFolders = $res['json']['value'];
// Haal eventuele submappen op voor mappen met childFolderCount > 0
foreach ($res['json']['value'] as $f) {
if (!empty($f['childFolderCount']) && (int)$f['childFolderCount'] > 0 && !empty($f['id'])) {
$childUrl = MS_GRAPH_BASE_URL . '/me/mailFolders/' . urlencode($f['id']) . '/childFolders?$top=50&$select=id,displayName,parentFolderId,childFolderCount,unreadItemCount,totalItemCount,wellKnownName';
$cRes = fociz_http_request($childUrl, [
'method' => 'GET',
'headers' => [
'Authorization' => "Bearer {$token}",
'Accept' => 'application/json',
],
'timeout' => 10,
]);
if ($cRes['status'] === 200 && !empty($cRes['json']['value'])) {
foreach ($cRes['json']['value'] as $child) {
$rawFolders[] = $child;
}
}
}
}
// Uitsluitingen conform gebruikerswens: SPAM / Ongewenst, Prullenbak / Deleted, Outbox en Drafts
$excludedWellKnown = ['junkemail', 'deleteditems', 'outbox', 'drafts', 'conflicts', 'localfailures', 'serverfailures', 'syncissues'];
$excludedTerms = ['junk', 'spam', 'ongewenst', 'ongewenste e-mail', 'ongewenste email', 'deleted items', 'verwijderde items', 'prullenbak', 'trash', 'verwijderd', 'outbox', 'postvak uit', 'drafts', 'concepten'];
$folders = [];
$seenIds = [];
foreach ($rawFolders as $rf) {
$fId = $rf['id'] ?? '';
if (empty($fId) || isset($seenIds[$fId])) continue;
$seenIds[$fId] = true;
$wk = strtolower(trim($rf['wellKnownName'] ?? ''));
if (in_array($wk, $excludedWellKnown, true)) continue;
$dName = trim($rf['displayName'] ?? '');
$dNameLower = strtolower($dName);
$skip = false;
foreach ($excludedTerms as $term) {
if ($dNameLower === $term || strpos($dNameLower, $term) !== false) {
$skip = true;
break;
}
}
if ($skip) continue;
$isInbox = ($wk === 'inbox' || $dNameLower === 'inbox' || $dNameLower === 'postvak in');
$folders[] = [
'id' => $isInbox ? 'inbox' : $fId,
'name' => $dName ?: 'Map',
'unread_count' => (int)($rf['unreadItemCount'] ?? 0),
'total_count' => (int)($rf['totalItemCount'] ?? 0),
'is_inbox' => $isInbox,
];
}
// Zorg dat Inbox altijd vooraan staat
$hasInbox = false;
foreach ($folders as $f) {
if (!empty($f['is_inbox'])) {
$hasInbox = true;
break;
}
}
if (!$hasInbox) {
array_unshift($folders, [
'id' => 'inbox',
'name' => 'Postvak IN',
'unread_count' => 0,
'total_count' => 0,
'is_inbox' => true,
]);
}
usort($folders, function($a, $b) {
if (!empty($a['is_inbox'])) return -1;
if (!empty($b['is_inbox'])) return 1;
return strcasecmp($a['name'], $b['name']);
});
$totalUnread = array_sum(array_column($folders, 'unread_count'));
$result = [
'success' => true,
'is_demo' => false,
'folders' => $folders,
'total_unread' => $totalUnread,
];
fociz_cache_set($cacheKey, $result, 60); // 60 seconden cache
return $result;
}
/**
* Haalt berichten op voor een specifieke map van Microsoft Graph API (of retourneert demo data)
*/
function microsoft_fetch_folder_messages(int $userId, string $folderId = 'inbox', int $limit = 30, bool $unreadOnly = false, bool $forceRefresh = false): array {
$cleanFolderId = trim($folderId ?: 'inbox');
$cacheKey = "ms_folder_{$userId}_" . md5($cleanFolderId) . "_{$limit}_" . ($unreadOnly ? '1' : '0');
if (!$forceRefresh) {
$cached = fociz_cache_get($cacheKey);
if ($cached !== null && is_array($cached)) {
return $cached;
}
}
$db = get_db_connection();
$stmt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmt->execute([$userId]);
$integration = $stmt->fetch();
$isLive = !empty($integration['access_token']) && empty($integration['is_demo_mode']);
if (!$isLive) {
$demo = microsoft_get_mail_demo_data($cleanFolderId);
if ($unreadOnly) {
$demo = array_values(array_filter($demo, function($m) { return empty($m['is_read']); }));
}
return [
'success' => true,
'is_demo' => true,
'folder_id' => $cleanFolderId,
'messages' => array_slice($demo, 0, $limit),
'unread_count' => count(array_filter($demo, function($m) { return empty($m['is_read']); })),
'total_count' => count($demo),
@@ -1068,6 +1412,7 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
return [
'success' => false,
'error' => 'Geen geldig Microsoft 365 token beschikbaar.',
'folder_id' => $cleanFolderId,
'messages' => [],
'unread_count' => 0,
];
@@ -1083,7 +1428,8 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$queryParams['$filter'] = 'isRead eq false';
}
$url = MS_GRAPH_BASE_URL . '/me/mailFolders/inbox/messages?' . http_build_query($queryParams);
$folderEndpoint = ($cleanFolderId === 'inbox') ? 'inbox' : urlencode($cleanFolderId);
$url = MS_GRAPH_BASE_URL . '/me/mailFolders/' . $folderEndpoint . '/messages?' . http_build_query($queryParams);
$res = fociz_http_request($url, [
'method' => 'GET',
'headers' => [
@@ -1097,14 +1443,15 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
if ($res['status'] !== 200 || !isset($res['json']['value'])) {
$graphError = $res['json']['error']['message'] ?? ($res['error'] ?? 'HTTP ' . $res['status']);
$graphCode = $res['json']['error']['code'] ?? '';
error_log("Microsoft fetch inbox error: HTTP {$res['status']} [{$graphCode}] - {$graphError}");
error_log("Microsoft fetch folder messages error for {$cleanFolderId}: HTTP {$res['status']} [{$graphCode}] - {$graphError}");
$demo = microsoft_get_mail_demo_data();
$demo = microsoft_get_mail_demo_data($cleanFolderId);
$needsReconsent = ($res['status'] === 403 || stripos($graphError, 'access') !== false || stripos($graphError, 'privilege') !== false || stripos($graphError, 'denied') !== false || stripos($graphError, 'consent') !== false);
return [
'success' => true,
'is_demo' => true,
'folder_id' => $cleanFolderId,
'messages' => array_slice($demo, 0, $limit),
'unread_count' => count(array_filter($demo, function($m) { return empty($m['is_read']); })),
'total_count' => count($demo),
@@ -1153,6 +1500,7 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$messages[] = [
'id' => $msg['id'] ?? '',
'folder_id' => $cleanFolderId,
'subject' => $msg['subject'] ?: '(Geen onderwerp)',
'body_preview' => $msg['bodyPreview'] ?? '',
'from_name' => $fromName,
@@ -1178,6 +1526,7 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
$result = [
'success' => true,
'is_demo' => false,
'folder_id' => $cleanFolderId,
'messages' => $messages,
'unread_count' => $unreadCount,
'total_count' => count($messages),
@@ -1186,6 +1535,13 @@ function microsoft_fetch_inbox_messages(int $userId, int $limit = 25, bool $unre
return $result;
}
/**
* Haalt inbox berichten op van Microsoft Graph API (of retourneert demo data)
*/
function microsoft_fetch_inbox_messages(int $userId, int $limit = 30, bool $unreadOnly = false, bool $forceRefresh = false): array {
return microsoft_fetch_folder_messages($userId, 'inbox', $limit, $unreadOnly, $forceRefresh);
}
/**
* Haalt de volledige inhoud en details van een enkel e-mailbericht op
*/
+530 -363
View File
File diff suppressed because it is too large Load Diff
+272 -87
View File
@@ -14,15 +14,22 @@ $isAdmin = (($currentUser['role'] ?? '') === 'ADMIN');
$db = get_db_connection();
$userBoards = fociz_get_user_boards_with_lanes($db, $userId, $isAdmin);
// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
// Haal alle beschikbare mailmappen op (exclusief SPAM/Junk, Prullenbak/Deleted, Outbox, Drafts)
$forceRefresh = !empty($_GET['refresh']) && $_GET['refresh'] === '1';
$foldersData = microsoft_fetch_mail_folders($userId, $forceRefresh);
$mailFolders = $foldersData['folders'] ?? [];
$totalUnreadCount = (int)($foldersData['total_unread'] ?? 0);
// Haal initiële inbox data op (met 45s cache, tenzij ?refresh=1)
$inboxData = microsoft_fetch_inbox_messages($userId, 30, false, $forceRefresh);
$messages = $inboxData['messages'] ?? [];
$isDemo = !empty($inboxData['is_demo']);
$unreadCount = (int)($inboxData['unread_count'] ?? 0);
$warning = $inboxData['warning'] ?? '';
$isDemo = !empty($inboxData['is_demo']) || !empty($foldersData['is_demo']);
$warning = $inboxData['warning'] ?? ($foldersData['warning'] ?? '');
$needsReconsent = !empty($inboxData['needs_reconsent']);
// Bereken actuele ongelezen aantal: neem het hoogste van total_unread over mappen en inbox unread_count
$unreadCount = max($totalUnreadCount, (int)($inboxData['unread_count'] ?? 0));
// Controleer status
$stmtInt = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK' AND is_enabled = 1");
$stmtInt->execute([$userId]);
@@ -31,6 +38,7 @@ $isOutlookConfigured = !empty($outlookInt['access_token']) && empty($outlookInt[
$isLiveMail = $isOutlookConfigured && !$isDemo;
$jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$jsonFolders = json_encode($mailFolders, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
?>
<div class="max-w-7xl mx-auto space-y-6">
@@ -143,15 +151,15 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
<!-- Twee-kolommen Mail Interface -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-[600px]">
<!-- Linker kolom: Berichtenlijst (5 kolommen) -->
<!-- Linker kolom: Berichtenlijst & Mappen (5 kolommen) -->
<div id="mailListColumn" class="lg:col-span-5 fociz-card p-3 sm:p-4 flex flex-col h-[720px] overflow-hidden">
<div class="text-xs font-bold uppercase tracking-wider text-slate-400 dark:text-slate-500 px-2 py-1 mb-2 flex items-center justify-between">
<span>Inbox Berichten (<span id="visibleMailCount"><?= count($messages) ?></span>)</span>
<span>Inbox &amp; Mappen (<span id="visibleMailCount"><?= count($messages) ?></span>)</span>
<span class="text-[10px] text-slate-400 font-normal">Sorteer: Nieuwste eerst</span>
</div>
<div class="flex-1 overflow-y-auto space-y-1.5 pr-1" id="mailListContainer">
<!-- Wordt dynamisch gerenderd via JS -->
<div class="flex-1 overflow-y-auto space-y-2.5 pr-1 custom-scrollbar" id="mailFoldersContainer">
<!-- Wordt dynamisch gerenderd via JS (Inbox open, overige mappen inklapbaar) -->
</div>
</div>
@@ -527,7 +535,12 @@ $jsonMessages = json_encode($messages, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_Q
</div>
<script>
let allMessages = <?= $jsonMessages ?>;
let mailFolders = <?= $jsonFolders ?>;
let folderMessagesMap = {
'inbox': <?= $jsonMessages ?>
};
let expandedFolders = new Set(['inbox']); // Inbox is standaard geopend, overige mappen ingeklapt
let loadingFolders = new Set();
let activeFilter = 'all'; // 'all' | 'unread'
let selectedMessage = null;
@@ -537,101 +550,234 @@ function setMailFilter(filter) {
const btnUnread = document.getElementById('filterBtn_unread');
if (filter === 'all') {
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition";
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition";
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition cursor-pointer";
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition cursor-pointer";
} else {
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition";
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition";
}
btnUnread.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg bg-white dark:bg-slate-900 text-slate-900 dark:text-white shadow-xs transition cursor-pointer";
btnAll.className = "flex-1 sm:flex-none px-3.5 py-1.5 rounded-lg text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white transition cursor-pointer";
applyMailSearch();
}
function applyMailSearch() {
const q = (document.getElementById('mailSearchInput')?.value || '').toLowerCase().trim();
let filtered = allMessages;
if (activeFilter === 'unread') {
filtered = filtered.filter(m => !m.is_read);
}
if (q) {
filtered = filtered.filter(m => {
const sub = (m.subject || '').toLowerCase();
const from = (m.from_name || '').toLowerCase() + ' ' + (m.from_email || '').toLowerCase();
const prev = (m.body_preview || '').toLowerCase();
return sub.includes(q) || from.includes(q) || prev.includes(q);
// Bij ongelezen filter: klap automatisch alle mappen met ongelezen mails open!
mailFolders.forEach(f => {
const msgs = folderMessagesMap[f.id];
const hasUnread = msgs ? msgs.some(m => !m.is_read) : ((f.unread_count || 0) > 0);
if (hasUnread) {
expandedFolders.add(f.id);
if (!folderMessagesMap[f.id]) {
loadFolderMessagesInBackground(f.id);
}
}
});
}
renderMailList(filtered);
renderFolders();
}
function renderMailList(list) {
const container = document.getElementById('mailListContainer');
const countEl = document.getElementById('visibleMailCount');
if (countEl) countEl.innerText = list.length;
function applyMailSearch() {
renderFolders();
}
if (!list || list.length === 0) {
container.innerHTML = `
<div class="text-center py-12 text-slate-400 text-xs">
<i data-lucide="inbox" class="w-8 h-8 text-slate-300 dark:text-slate-700 mx-auto mb-2"></i>
<p>Geen berichten gevonden.</p>
</div>
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
async function loadFolderMessagesInBackground(folderId) {
if (loadingFolders.has(folderId) || folderMessagesMap[folderId]) return;
loadingFolders.add(folderId);
renderFolders();
try {
const res = await fetch(`/api/mail.php?action=get_folder_messages&folder_id=${encodeURIComponent(folderId)}&limit=30`);
const data = await res.json();
if (data.success && Array.isArray(data.messages)) {
folderMessagesMap[folderId] = data.messages;
const fObj = mailFolders.find(f => f.id === folderId);
if (fObj && typeof data.unread_count === 'number') {
fObj.unread_count = data.unread_count;
}
}
} catch (e) {
console.warn('Fout bij ophalen berichten voor map ' + folderId, e);
} finally {
loadingFolders.delete(folderId);
updateTotalCounters();
renderFolders();
}
}
async function toggleFolder(folderId) {
if (expandedFolders.has(folderId)) {
expandedFolders.delete(folderId);
renderFolders();
return;
}
let html = '';
list.forEach(m => {
const isSelected = selectedMessage && selectedMessage.id === m.id;
const isUnread = !m.is_read;
const initials = (m.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
expandedFolders.add(folderId);
let cardClass = isSelected
? 'bg-sky-50 dark:bg-sky-950/40 border-sky-400 dark:border-sky-700 shadow-xs'
: (isUnread ? 'bg-white dark:bg-[#111624] border-slate-200 dark:border-slate-800 hover:border-slate-300 font-semibold' : 'bg-slate-50/60 dark:bg-[#0c0f17]/60 border-slate-100 dark:border-slate-800/60 text-slate-600 dark:text-slate-400');
if (!folderMessagesMap[folderId]) {
loadingFolders.add(folderId);
renderFolders();
try {
const res = await fetch(`/api/mail.php?action=get_folder_messages&folder_id=${encodeURIComponent(folderId)}&limit=30`);
const data = await res.json();
if (data.success && Array.isArray(data.messages)) {
folderMessagesMap[folderId] = data.messages;
const fObj = mailFolders.find(f => f.id === folderId);
if (fObj && typeof data.unread_count === 'number') {
fObj.unread_count = data.unread_count;
}
} else {
folderMessagesMap[folderId] = [];
}
} catch (e) {
folderMessagesMap[folderId] = [];
} finally {
loadingFolders.delete(folderId);
updateTotalCounters();
renderFolders();
}
} else {
renderFolders();
}
}
function renderFolders() {
const container = document.getElementById('mailFoldersContainer');
if (!container) return;
const q = (document.getElementById('mailSearchInput')?.value || '').toLowerCase().trim();
let totalVisible = 0;
let html = '';
mailFolders.forEach(folder => {
const isInbox = (folder.id === 'inbox' || folder.is_inbox);
const isExpanded = expandedFolders.has(folder.id);
const isLoading = loadingFolders.has(folder.id);
const msgs = folderMessagesMap[folder.id] || [];
let unreadCount = 0;
if (folderMessagesMap[folder.id]) {
unreadCount = msgs.filter(m => !m.is_read).length;
} else {
unreadCount = folder.unread_count || 0;
}
let filteredMsgs = msgs;
if (activeFilter === 'unread') {
filteredMsgs = filteredMsgs.filter(m => !m.is_read);
}
if (q) {
filteredMsgs = filteredMsgs.filter(m => {
const sub = (m.subject || '').toLowerCase();
const from = (m.from_name || '').toLowerCase() + ' ' + (m.from_email || '').toLowerCase();
const prev = (m.body_preview || '').toLowerCase();
return sub.includes(q) || from.includes(q) || prev.includes(q);
});
}
if (isExpanded) {
totalVisible += filteredMsgs.length;
}
const folderTotal = folder.total_count || msgs.length;
html += `
<div onclick="selectMail('${m.id}')" class="group relative p-3 rounded-2xl border transition cursor-pointer ${cardClass}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center space-x-2 truncate">
<div class="w-6 h-6 rounded-lg ${isUnread ? 'bg-sky-600 text-white' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400'} text-[10px] font-bold flex items-center justify-center flex-shrink-0">
${initials}
<div class="rounded-2xl border border-slate-200/80 dark:border-slate-800/80 bg-white/70 dark:bg-slate-900/40 overflow-hidden shadow-2xs">
<!-- Folder Header Accordion Knop -->
<button type="button" onclick="toggleFolder('${folder.id}')" class="w-full flex items-center justify-between p-3 select-none transition cursor-pointer ${isInbox ? 'bg-slate-100/90 dark:bg-slate-800/80' : 'bg-slate-50/80 dark:bg-slate-800/40 hover:bg-slate-100 dark:hover:bg-slate-800/70'}">
<div class="flex items-center space-x-2.5 min-w-0">
<div class="w-7 h-7 rounded-xl ${isInbox ? 'bg-sky-600 text-white shadow-xs' : 'bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300'} flex items-center justify-center flex-shrink-0">
<i data-lucide="${isInbox ? 'inbox' : 'folder'}" class="w-3.5 h-3.5"></i>
</div>
<div class="flex items-center space-x-1.5 truncate">
<span class="text-xs font-bold text-slate-900 dark:text-white truncate">${escapeHtml(folder.name)}</span>
<span class="text-[10px] text-slate-400 font-semibold">(${folderTotal})</span>
</div>
<span class="text-xs ${isUnread ? 'font-black text-slate-900 dark:text-white' : 'font-medium text-slate-700 dark:text-slate-300'} truncate">
${escapeHtml(m.from_name)}
</span>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
${isUnread ? '<span class="w-2 h-2 rounded-full bg-sky-500"></span>' : ''}
${m.has_attachments ? '<i data-lucide="paperclip" class="w-3 h-3 text-slate-400"></i>' : ''}
<span class="text-[10px] text-slate-400 font-medium">${escapeHtml(m.received_human || '')}</span>
<div class="flex items-center space-x-2 flex-shrink-0 ml-2">
${unreadCount > 0 ? `<span class="px-2 py-0.5 rounded-full text-[10px] font-black bg-sky-100 dark:bg-sky-950/70 text-sky-700 dark:text-sky-300 ring-1 ring-sky-500/30">${unreadCount} ongelezen</span>` : ''}
<i data-lucide="chevron-down" class="w-4 h-4 text-slate-400 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}"></i>
</div>
</div>
</button>
<div class="mt-1 text-xs ${isUnread ? 'font-bold text-slate-900 dark:text-white' : 'text-slate-800 dark:text-slate-200'} truncate">
${escapeHtml(m.subject)}
</div>
<div class="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500 line-clamp-1 leading-snug">
${escapeHtml(m.body_preview || '')}
<!-- Folder Berichten Inhoud -->
<div class="${isExpanded ? '' : 'hidden'} p-2 space-y-1.5 bg-slate-50/40 dark:bg-[#0c0f17]/30 border-t border-slate-100 dark:border-slate-800/80">
${isLoading ? `
<div class="p-4 text-center text-xs text-slate-400 flex items-center justify-center space-x-1.5">
<i data-lucide="loader-2" class="w-4 h-4 animate-spin text-sky-500"></i>
<span>Berichten ophalen uit Outlook...</span>
</div>
` : (filteredMsgs.length === 0 ? `
<div class="py-4 text-center text-slate-400 text-xs italic">
${activeFilter === 'unread' ? 'Geen ongelezen berichten in deze map.' : 'Geen berichten gevonden.'}
</div>
` : filteredMsgs.map(m => renderMessageCardHtml(m)).join(''))}
</div>
</div>
`;
});
container.innerHTML = html;
const countEl = document.getElementById('visibleMailCount');
if (countEl) countEl.innerText = totalVisible;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function renderMessageCardHtml(m) {
const isSelected = selectedMessage && selectedMessage.id === m.id;
const isUnread = !m.is_read;
const initials = (m.from_name || 'NL').split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
let cardClass = isSelected
? 'bg-sky-50 dark:bg-sky-950/40 border-sky-400 dark:border-sky-700 shadow-xs'
: (isUnread ? 'bg-white dark:bg-[#111624] border-slate-200 dark:border-slate-800 hover:border-slate-300 font-semibold' : 'bg-slate-50/60 dark:bg-[#0c0f17]/60 border-slate-100 dark:border-slate-800/60 text-slate-600 dark:text-slate-400');
return `
<div onclick="selectMail('${m.id}')" class="group relative p-3 rounded-2xl border transition cursor-pointer ${cardClass}">
<div class="flex items-start justify-between gap-2">
<div class="flex items-center space-x-2 truncate">
<div class="w-6 h-6 rounded-lg ${isUnread ? 'bg-sky-600 text-white' : 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400'} text-[10px] font-bold flex items-center justify-center flex-shrink-0">
${initials}
</div>
<span class="text-xs ${isUnread ? 'font-black text-slate-900 dark:text-white' : 'font-medium text-slate-700 dark:text-slate-300'} truncate">
${escapeHtml(m.from_name)}
</span>
</div>
<div class="flex items-center space-x-1.5 flex-shrink-0">
${isUnread ? '<span class="w-2 h-2 rounded-full bg-sky-500"></span>' : ''}
${m.has_attachments ? '<i data-lucide="paperclip" class="w-3 h-3 text-slate-400"></i>' : ''}
<span class="text-[10px] text-slate-400 font-medium">${escapeHtml(m.received_human || '')}</span>
</div>
</div>
<div class="mt-1 text-xs ${isUnread ? 'font-bold text-slate-900 dark:text-white' : 'text-slate-800 dark:text-slate-200'} truncate">
${escapeHtml(m.subject)}
</div>
<div class="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500 line-clamp-1 leading-snug">
${escapeHtml(m.body_preview || '')}
</div>
</div>
`;
}
function renderMailList() {
renderFolders();
}
async function selectMail(id) {
const found = allMessages.find(m => m.id === id);
let found = null;
let foundFolderId = null;
for (const fId in folderMessagesMap) {
const m = (folderMessagesMap[fId] || []).find(x => x.id === id);
if (m) {
found = m;
foundFolderId = fId;
break;
}
}
if (!found) return;
selectedMessage = found;
renderMailList(allMessages); // Update actieve styling
renderFolders(); // Update actieve styling
// Toon detail container
document.getElementById('noMailSelectedState').classList.add('hidden');
@@ -659,9 +805,13 @@ async function selectMail(id) {
const data = await res.json();
if (data.success && data.message) {
renderMailBodyContent(bodyEl, data.message.body_html, data.message.body_text || found.body_preview);
found.is_read = true;
updateUnreadCount();
renderMailList(allMessages);
if (!found.is_read) {
found.is_read = true;
const fObj = mailFolders.find(f => f.id === foundFolderId);
if (fObj && fObj.unread_count > 0) fObj.unread_count--;
updateTotalCounters();
renderFolders();
}
} else {
bodyEl.innerHTML = `<div class="p-4 rounded-xl bg-red-50 text-red-700 text-xs">${data.error || 'Kon bericht niet inladen.'}</div>`;
}
@@ -784,9 +934,29 @@ function renderMailBodyContent(container, htmlContent, plainContent) {
}
function updateUnreadCount() {
const unread = allMessages.filter(m => !m.is_read).length;
const el = document.getElementById('countUnread');
if (el) el.innerText = unread;
updateTotalCounters();
}
function updateTotalCounters() {
let totalUnread = 0;
let totalAll = 0;
mailFolders.forEach(f => {
const msgs = folderMessagesMap[f.id];
if (msgs) {
totalUnread += msgs.filter(m => !m.is_read).length;
totalAll += msgs.length;
} else {
totalUnread += (f.unread_count || 0);
totalAll += (f.total_count || 0);
}
});
const elUnread = document.getElementById('countUnread');
if (elUnread) elUnread.innerText = totalUnread;
const elAll = document.getElementById('countAll');
if (elAll) elAll.innerText = totalAll;
}
function closeMobileDetail() {
@@ -1182,17 +1352,30 @@ async function refreshMailbox() {
if (icon) icon.classList.add('animate-spin');
try {
const res = await fetch('/api/mail.php?action=get_inbox&refresh=1');
const res = await fetch('/api/mail.php?action=get_folders&refresh=1');
const data = await res.json();
if (data.success && data.messages) {
allMessages = data.messages;
applyMailSearch();
updateUnreadCount();
if (data.success && Array.isArray(data.folders)) {
mailFolders = data.folders;
}
const promises = [...expandedFolders].map(async (fId) => {
try {
const r = await fetch(`/api/mail.php?action=get_folder_messages&folder_id=${encodeURIComponent(fId)}&limit=30&refresh=1`);
const d = await r.json();
if (d.success && Array.isArray(d.messages)) {
folderMessagesMap[fId] = d.messages;
const fObj = mailFolders.find(f => f.id === fId);
if (fObj && typeof d.unread_count === 'number') fObj.unread_count = d.unread_count;
}
} catch (e) {}
});
await Promise.all(promises);
} catch(err) {
console.warn('Mail refresh error:', err);
} finally {
if (icon) icon.classList.remove('animate-spin');
updateTotalCounters();
renderFolders();
}
}
@@ -1228,9 +1411,11 @@ document.addEventListener('keydown', function(e) {
});
document.addEventListener('DOMContentLoaded', () => {
applyMailSearch();
if (allMessages && allMessages.length > 0) {
selectMail(allMessages[0].id);
updateTotalCounters();
renderFolders();
const inboxMsgs = folderMessagesMap['inbox'] || [];
if (inboxMsgs.length > 0) {
selectMail(inboxMsgs[0].id);
}
// Luister naar live websocket/polling event voor nieuwe inkomende e-mails