mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: Gmail IMAP email integration with voice intents
- email.php: IMAP reader for Gmail/Outlook/iCloud with 5-min cache - api.php: add /api/email route - chat.php: email voice intents — check count, read recent, filter by sender - config.php: Gmail credentials (gitignored)
This commit is contained in:
@@ -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 ──────────────────────────────────
|
// ── Tier 0.5: Network Device Management ──────────────────────────────────
|
||||||
if (!$reply) {
|
if (!$reply) {
|
||||||
// Flow state stored in kb_facts (session_write_close() is called before this runs)
|
// Flow state stored in kb_facts (session_write_close() is called before this runs)
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
// JARVIS Email endpoint — IMAP reader for Gmail, Outlook, iCloud.
|
||||||
|
// GET ?action=email → recent + unread summary (cached 5 min)
|
||||||
|
// GET ?action=email&account=gmail → Gmail only
|
||||||
|
// GET ?action=email&force=1 → bypass cache, fetch live
|
||||||
|
// GET ?action=email&search=from:john → search emails
|
||||||
|
|
||||||
|
function imapFetch(string $host, string $user, string $pass, int $maxMsgs = 10): array {
|
||||||
|
if (!$user || !$pass) return ['error' => '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);
|
||||||
@@ -72,6 +72,9 @@ switch ($endpoint) {
|
|||||||
case 'tts':
|
case 'tts':
|
||||||
require __DIR__ . '/../api/endpoints/tts.php';
|
require __DIR__ . '/../api/endpoints/tts.php';
|
||||||
break;
|
break;
|
||||||
|
case 'email':
|
||||||
|
require __DIR__ . '/../api/endpoints/email.php';
|
||||||
|
break;
|
||||||
case 'do':
|
case 'do':
|
||||||
require __DIR__ . '/../api/endpoints/do_server.php';
|
require __DIR__ . '/../api/endpoints/do_server.php';
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user