diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index f298380..e855be1 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -300,6 +300,86 @@ if (!$reply && preg_match('/(is|are|what.s|status|state).*(on|off|light|switch|p } +// ── Email queries ───────────────────────────────────────────────────────── +if (!$reply && preg_match('/\b(email|emails|inbox|gmail|outlook|mail|unread|messages)\b/i', $message)) { + $emailUrl = (defined('SITE_URL') ? SITE_URL : 'https://jarvis.orbishosting.com') . '/api/email'; + $account = 'all'; + if (preg_match('/\bgmail\b/i', $message)) $account = 'gmail'; + if (preg_match('/\boutlook\b/i', $message)) $account = 'outlook'; + if (preg_match('/\bicloud\b/i', $message)) $account = 'icloud'; + + $ch = curl_init($emailUrl . '?account=' . $account); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['X-Session-Token: ' . ($_SESSION['jarvis_token'] ?? '')], + CURLOPT_TIMEOUT => 20, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_SSL_VERIFYPEER => false, + ]); + $emailJson = curl_exec($ch); + $emailCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($emailCode === 200 && $emailJson) { + $ed = json_decode($emailJson, true) ?? []; + $summary = $ed['summary'] ?? []; + $unread = (int)($summary['total_unread'] ?? 0); + $recent = $summary['recent'] ?? []; + $accts = $ed['accounts'] ?? []; + + // "How many" / "any" / check → just count + if (preg_match('/\b(how many|any|check|count|unread)\b/i', $message) && !preg_match('/\bread\b/i', $message)) { + if ($unread === 0) { + $reply = "No unread emails, {$userAddr}."; + } else { + // break down by account + $parts = []; + foreach ($accts as $a => $r) { + if (!empty($r['unread'])) $parts[] = $r['unread'] . ' on ' . ucfirst($a); + } + $breakdown = $parts ? ' (' . implode(', ', $parts) . ')' : ''; + $reply = "You have {$unread} unread email" . ($unread>1?'s':'') . "{$breakdown}, {$userAddr}."; + } + $source = 'email:count'; + + // "emails from [person]" → filter by sender + } elseif (preg_match('/\bfrom\s+(.+)/i', $message, $fm)) { + $sender = strtolower(trim($fm[1])); + $matches = array_filter($recent, fn($m) => + stripos($m['from_name']??'', $sender) !== false || + stripos($m['from_email']??'', $sender) !== false + ); + if ($matches) { + $m = array_values($matches)[0]; + $reply = "Email from {$m['from_name']}: \"{$m['subject']}\" — {$m['date']}."; + if (!empty($m['preview'])) $reply .= ' Preview: ' . mb_substr($m['preview'], 0, 150); + } else { + $reply = "No recent emails from {$sender}, {$userAddr}."; + } + $source = 'email:search'; + + // "read" / "latest" / "recent" → read top emails + } else { + $unreadOnly = array_values(array_filter($recent, fn($m) => $m['unread'])); + $toRead = $unreadOnly ?: $recent; + $toRead = array_slice($toRead, 0, 3); + if (empty($toRead)) { + $reply = "No emails to report, {$userAddr}."; + } else { + $lines = []; + foreach ($toRead as $m) { + $flag = $m['unread'] ? '' : ''; + $acct = isset($m['account']) ? ' [' . strtoupper($m['account']) . ']' : ''; + $lines[] = "From {$m['from_name']}: \"{$m['subject']}\", {$m['date']}{$acct}."; + } + $intro = $unread > 0 ? "You have {$unread} unread. " : ""; + $reply = $intro . implode(' ', $lines); + } + $source = 'email:read'; + } + } +} + // ── Tier 0.5: Network Device Management ────────────────────────────────── if (!$reply) { // Flow state stored in kb_facts (session_write_close() is called before this runs) diff --git a/api/endpoints/email.php b/api/endpoints/email.php new file mode 100644 index 0000000..d752948 --- /dev/null +++ b/api/endpoints/email.php @@ -0,0 +1,139 @@ + '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); + + // Fetch most recent messages (newest first) + $start = max(1, $total - $maxMsgs + 1); + $msgs = []; + for ($i = $total; $i >= $start; $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)) : ''; + $isUnread = in_array($i, $unseen); + + // Fetch plain text preview (first 300 chars) + $preview = ''; + $struct = @imap_fetchstructure($mbox, $i); + if ($struct) { + if ($struct->type === 0) { + // Single-part message + $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, 300); + } else { + // Multi-part — find first text/plain part + foreach (($struct->parts ?? []) as $idx => $part) { + if ($part->type === 0) { // text + $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, 300); + break; + } + } + } + } + $preview = trim(preg_replace('/\s+/', ' ', $preview)); + + $msgs[] = [ + 'id' => $i, + 'from_name' => $fromName ?: $fromEmail, + 'from_email'=> $fromEmail, + 'subject' => $subject, + 'date' => $date, + 'unread' => $isUnread, + 'preview' => $preview, + ]; + } + + imap_close($mbox); + return ['total' => $total, 'unread' => $unread, 'messages' => $msgs]; +} + +$account = $data['account'] ?? $_GET['account'] ?? 'all'; +$force = !empty($data['force']) || !empty($_GET['force']); +$search = trim($data['search'] ?? $_GET['search'] ?? ''); + +$cacheKey = 'email_' . $account; +$cacheTtl = 300; // 5 minutes + +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']; + echo json_encode($out); + exit; + } +} + +$result = ['accounts' => [], 'summary' => []]; + +if (in_array($account, ['all', 'gmail']) && defined('GMAIL_USER') && GMAIL_USER) { + $r = imapFetch('{imap.gmail.com:993/imap/ssl}INBOX', GMAIL_USER, GMAIL_PASS); + $result['accounts']['gmail'] = $r; +} + +if (in_array($account, ['all', 'outlook']) && defined('OUTLOOK_USER') && OUTLOOK_USER) { + $r = imapFetch('{outlook.office365.com:993/imap/ssl}INBOX', OUTLOOK_USER, OUTLOOK_PASS); + $result['accounts']['outlook'] = $r; +} + +if (in_array($account, ['all', 'icloud']) && defined('ICLOUD_USER') && ICLOUD_USER) { + $r = imapFetch('{imap.mail.me.com:993/imap/ssl}INBOX', ICLOUD_USER, ICLOUD_PASS); + $result['accounts']['icloud'] = $r; +} + +// Build summary across all accounts +$totalUnread = 0; +$allMessages = []; +foreach ($result['accounts'] as $acct => $r) { + if (isset($r['unread'])) $totalUnread += (int)$r['unread']; + if (!empty($r['messages'])) { + foreach ($r['messages'] as $m) { + $m['account'] = $acct; + $allMessages[] = $m; + } + } +} + +// Sort by date descending (approximate — already newest first per account) +$result['summary'] = [ + 'total_unread' => $totalUnread, + 'recent' => array_slice($allMessages, 0, 10), + 'fetched_at' => date('c'), +]; + +// Cache result +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); diff --git a/public_html/api.php b/public_html/api.php index 6326865..67fc938 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -72,6 +72,9 @@ switch ($endpoint) { case 'tts': require __DIR__ . '/../api/endpoints/tts.php'; break; + case 'email': + require __DIR__ . '/../api/endpoints/email.php'; + break; case 'do': require __DIR__ . '/../api/endpoints/do_server.php'; break;