'not_configured']; $mbox = @imap_open($host, $user, $pass, 0, 1, ['DISABLE_AUTHENTICATOR' => 'GSSAPI']); if (!$mbox) return ['error' => imap_last_error() ?: 'connection_failed']; $total = imap_num_msg($mbox); $unseen = imap_search($mbox, 'UNSEEN') ?: []; $unread = count($unseen); $msgs = []; for ($i = $total; $i >= max(1, $total - $maxMsgs + 1); $i--) { $hdr = @imap_headerinfo($mbox, $i); if (!$hdr) continue; $from = $hdr->from[0] ?? null; $fromName = $from && isset($from->personal) ? imap_utf8($from->personal) : ''; $fromEmail = $from ? ($from->mailbox . '@' . ($from->host ?? '')) : ''; $subject = isset($hdr->subject) ? imap_utf8($hdr->subject) : '(no subject)'; $date = isset($hdr->date) ? date('M j g:ia', strtotime($hdr->date)) : ''; $dateRaw = isset($hdr->date) ? date('Y-m-d H:i:s', strtotime($hdr->date)) : null; $isUnread = in_array($i, $unseen); $msgId = md5(($hdr->message_id ?? '') ?: ($fromEmail . $subject . $date)); // Preview $preview = ''; $struct = @imap_fetchstructure($mbox, $i); if ($struct) { if ($struct->type === 0) { $raw = @imap_fetchbody($mbox, $i, '1'); $enc = $struct->encoding ?? 0; if ($enc === 3) $raw = base64_decode($raw); elseif ($enc === 4) $raw = quoted_printable_decode($raw); $preview = mb_substr(strip_tags($raw), 0, 400); } else { foreach (($struct->parts ?? []) as $idx => $part) { if ($part->type === 0) { $raw = @imap_fetchbody($mbox, $i, (string)($idx + 1)); $enc = $part->encoding ?? 0; if ($enc === 3) $raw = base64_decode($raw); elseif ($enc === 4) $raw = quoted_printable_decode($raw); $preview = mb_substr(strip_tags($raw), 0, 400); break; } } } } $preview = trim(preg_replace('/\s+/', ' ', $preview)); $msgs[] = [ 'id' => $i, 'msg_id' => $msgId, 'from_name' => $fromName ?: $fromEmail, 'from_email' => $fromEmail, 'subject' => $subject, 'date' => $date, 'date_raw' => $dateRaw, 'unread' => $isUnread, 'preview' => $preview, ]; } imap_close($mbox); return ['total' => $total, 'unread' => $unread, 'messages' => $msgs]; } // ── Action item detection ───────────────────────────────────────────────────── function detectActionItem(string $subject, string $preview): array { $text = strtolower($subject . ' ' . mb_substr($preview, 0, 500)); $apptKw = ['meeting','zoom call','teams call','video call','conference call','join us','calendar invite', 'appointment','interview','webinar','you are invited','let\'s meet','lets meet', 'scheduled for','scheduled at','set up a call','book a call','at \d{1,2}(:\d{2})?\s*(am|pm)']; $taskKw = ['action required','action needed','please review','please respond','please confirm', 'please send','please provide','please complete','please sign','please approve', 'follow up','follow-up','reminder:','deadline','due by','due date', 'asap','urgent','your attention','needs your','waiting for you', 'can you','could you','would you please','i need you to','kindly']; $apptScore = 0; $taskScore = 0; foreach ($apptKw as $kw) { if (@preg_match('/' . $kw . '/i', $text)) { $apptScore += 25; break; } } foreach ($taskKw as $kw) { if (strpos($text, $kw) !== false) { $taskScore += 20; break; } } // Bonus: explicit deadline date in subject if (preg_match('/\b(by\s+)?(monday|tuesday|wednesday|thursday|friday|saturday|sunday|tomorrow|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|\d{1,2}\/\d{1,2})\b/i', $subject)) { $taskScore += 15; } // Suggested date extraction $sugDate = null; if (preg_match('/\b(tomorrow|today|next\s+\w+|this\s+(monday|tuesday|wednesday|thursday|friday)|monday|tuesday|wednesday|thursday|friday|\d{1,2}[\/\-]\d{1,2}(?:[\/\-]\d{2,4})?)\b/i', $subject . ' ' . mb_substr($preview, 0, 200), $dm)) { $ts = strtotime($dm[0]); if ($ts !== false && $ts > time() - 86400) $sugDate = date('Y-m-d', $ts); } $type = null; if ($apptScore >= 25 && $apptScore >= $taskScore) $type = 'appointment'; elseif ($taskScore >= 20) $type = 'task'; return ['type' => $type, 'suggested_date' => $sugDate]; } // ── Upsert email actions ────────────────────────────────────────────────────── function storeEmailActions(string $account, array $messages): void { foreach ($messages as $msg) { $ai = detectActionItem($msg['subject'], $msg['preview'] ?? ''); if (!$ai['type']) continue; JarvisDB::execute( "INSERT INTO email_actions (account,message_uid,from_email,from_name,subject,preview,received_at,action_type,suggested_title,suggested_date) VALUES (?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE action_type=VALUES(action_type), suggested_title=VALUES(suggested_title), suggested_date=VALUES(suggested_date), from_name=VALUES(from_name), preview=VALUES(preview), received_at=VALUES(received_at)", [$account, $msg['msg_id'], $msg['from_email'], $msg['from_name'], $msg['subject'], mb_substr($msg['preview'] ?? '', 0, 500), $msg['date_raw'], $ai['type'], mb_substr($msg['subject'], 0, 255), $ai['suggested_date']] ); } } // ── Route ───────────────────────────────────────────────────────────────────── $actionType = $data['action'] ?? $_GET['action'] ?? ''; // ── Create task from email action ───────────────────────────────────────────── if ($method === 'POST' && $actionType === 'create_task') { $id = (int)($data['id'] ?? 0); if (!$id) { echo json_encode(['error' => 'Missing id']); exit; } $ea = JarvisDB::single('SELECT * FROM email_actions WHERE id=?', [$id]); if (!$ea) { echo json_encode(['error' => 'Not found']); exit; } $title = trim($data['title'] ?? $ea['suggested_title']); $due_date = trim($data['due_date'] ?? $ea['suggested_date'] ?? ''); $notes = "From: {$ea['from_name']} <{$ea['from_email']}>\nSubject: {$ea['subject']}"; $taskId = JarvisDB::insert( 'INSERT INTO tasks (title,notes,category,priority,due_date) VALUES (?,?,?,?,?)', [$title, $notes, 'work', 'normal', $due_date ?: null] ); JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?', [$taskId, $id]); echo json_encode(['success' => true, 'task_id' => $taskId]); exit; } // ── Create appointment from email action ────────────────────────────────────── if ($method === 'POST' && $actionType === 'create_appt') { $id = (int)($data['id'] ?? 0); if (!$id) { echo json_encode(['error' => 'Missing id']); exit; } $ea = JarvisDB::single('SELECT * FROM email_actions WHERE id=?', [$id]); if (!$ea) { echo json_encode(['error' => 'Not found']); exit; } $title = trim($data['title'] ?? $ea['suggested_title']); $start_at = trim($data['start_at'] ?? ''); if (!$start_at && $ea['suggested_date']) $start_at = $ea['suggested_date'] . ' 09:00:00'; if (!$start_at) $start_at = date('Y-m-d') . ' 09:00:00'; $desc = "From: {$ea['from_name']} <{$ea['from_email']}>"; $apptId = JarvisDB::insert( 'INSERT INTO appointments (title,description,category,start_at) VALUES (?,?,?,?)', [$title, $desc, 'work', $start_at] ); JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?', [$apptId, $id]); echo json_encode(['success' => true, 'appointment_id' => $apptId]); exit; } // ── Dismiss email action ────────────────────────────────────────────────────── if ($method === 'POST' && $actionType === 'dismiss') { $id = (int)($data['id'] ?? 0); if ($id) JarvisDB::execute('UPDATE email_actions SET dismissed=1 WHERE id=?', [$id]); echo json_encode(['success' => true]); exit; } // ── List detected action items ──────────────────────────────────────────────── if ($actionType === 'action_items') { $rows = JarvisDB::query( "SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 50" ) ?? []; echo json_encode(['action_items' => $rows, 'count' => count($rows)]); exit; } // ── Inbox fetch (main) ──────────────────────────────────────────────────────── $account = $data['account'] ?? $_GET['account'] ?? 'all'; $force = !empty($data['force']) || !empty($_GET['force']); $cacheKey = 'email_' . $account; $cacheTtl = 300; if (!$force) { $cached = JarvisDB::single("SELECT data, UNIX_TIMESTAMP(updated_at) ts FROM api_cache WHERE cache_key=?", [$cacheKey]); if ($cached && (time() - (int)$cached['ts']) < $cacheTtl) { $out = json_decode($cached['data'], true); $out['cache_age_s'] = time() - (int)$cached['ts']; // Merge unactioned email_actions count $out['action_items_count'] = (int)(JarvisDB::single("SELECT COUNT(*) c FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL")['c'] ?? 0); echo json_encode($out); exit; } } $result = ['accounts' => []]; $accounts_to_fetch = []; if (in_array($account, ['all', 'gmail']) && defined('GMAIL_USER') && GMAIL_USER) $accounts_to_fetch['gmail'] = ['{imap.gmail.com:993/imap/ssl}INBOX', GMAIL_USER, GMAIL_PASS]; if (in_array($account, ['all', 'outlook']) && defined('OUTLOOK_USER') && OUTLOOK_USER) $accounts_to_fetch['outlook'] = ['{outlook.office365.com:993/imap/ssl}INBOX', OUTLOOK_USER, OUTLOOK_PASS]; if (in_array($account, ['all', 'icloud']) && defined('ICLOUD_USER') && ICLOUD_USER) $accounts_to_fetch['icloud'] = ['{imap.mail.me.com:993/imap/ssl}INBOX', ICLOUD_USER, ICLOUD_PASS]; foreach ($accounts_to_fetch as $acct => [$host, $user, $pass]) { $r = imapFetch($host, $user, $pass, 15); $result['accounts'][$acct] = $r; if (!empty($r['messages'])) storeEmailActions($acct, $r['messages']); } // Build summary $totalUnread = 0; $allMessages = []; foreach ($result['accounts'] as $acct => $r) { if (isset($r['unread'])) $totalUnread += (int)$r['unread']; foreach ($r['messages'] ?? [] as $m) { $m['account'] = $acct; $allMessages[] = $m; } } // Sort by date descending usort($allMessages, fn($a,$b) => strtotime($b['date_raw']??'0') - strtotime($a['date_raw']??'0')); $result['summary'] = [ 'total_unread' => $totalUnread, 'recent' => array_slice($allMessages, 0, 15), 'fetched_at' => date('c'), ]; $result['action_items_count'] = (int)(JarvisDB::single("SELECT COUNT(*) c FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL")['c'] ?? 0); JarvisDB::execute( "INSERT INTO api_cache (cache_key,data,updated_at) VALUES (?,?,NOW()) ON DUPLICATE KEY UPDATE data=VALUES(data),updated_at=NOW()", [$cacheKey, json_encode($result)] ); $result['cache_age_s'] = 0; echo json_encode($result);