1008 lines
58 KiB
PHP
1008 lines
58 KiB
PHP
<?php
|
|
header('Content-Type: application/json', 'Host: weval-consulting.com');
|
|
header('Access-Control-Allow-Origin: *');
|
|
if($_SERVER['REQUEST_METHOD']==='OPTIONS'){http_response_code(200);exit;}
|
|
|
|
$dataDir = '/var/www/weval/wevia-ia/wevialife-data';
|
|
@mkdir($dataDir, 0755, true);
|
|
|
|
$action = $_GET['action'] ?? 'status';
|
|
|
|
// === EMAIL INTEGRATION FOR WEVIA LIFE ===
|
|
// Handles: Gmail IMAP, Outlook/Namecheap IMAP, Local sync
|
|
|
|
|
|
// IMAP proxy through S95 (to avoid IP lockouts)
|
|
function imap_via_proxy($server, $port, $email, $pass_b64) {
|
|
// Try local first
|
|
$mb = "{" . $server . ":" . $port . "/imap/ssl/novalidate-cert}INBOX";
|
|
$conn = @imap_open($mb, $email, base64_decode($pass_b64), 0, 1);
|
|
if ($conn) return $conn;
|
|
|
|
// If local fails (IP blocked), try via S95
|
|
$php = '$c=@imap_open("{'.$server.':'.$port.'/imap/ssl/novalidate-cert}INBOX","'.$email.'",base64_decode("'.$pass_b64.'"),0,1);if($c){$i=imap_check($c);$t=$i->Nmsgs;$s=max(1,$t-19);$r=[];$ov=imap_fetch_overview($c,"$s:$t",0);foreach(array_reverse($ov?:[])as$o){$r[]=$o->uid."|".$o->from."|".$o->subject."|".$o->date."|".($o->seen??0);}imap_close($c);echo json_encode(["ok"=>1,"count"=>$t,"emails"=>$r]);}else echo json_encode(["error"=>imap_last_error()]);';
|
|
$url = "http://10.1.0.3:5890/api/sentinel-brain.php?action=exec&cmd=" . urlencode("php -r " . escapeshellarg($php));
|
|
$ctx = stream_context_create(["http" => ["timeout" => 25]]);
|
|
$resp = @file_get_contents($url, false, $ctx);
|
|
if ($resp) {
|
|
$data = json_decode($resp, true);
|
|
if (isset($data["output"])) {
|
|
$output = json_decode($data["output"], true);
|
|
if ($output && isset($output["ok"])) {
|
|
// Store results for later use
|
|
$GLOBALS["_imap_proxy_result"] = $output;
|
|
return "S95_PROXY";
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
$emailSourcesFile = "$dataDir/email-sources.json";
|
|
|
|
function loadEmailSources($f) {
|
|
return file_exists($f) ? json_decode(file_get_contents($f), true) ?: [] : [];
|
|
}
|
|
function saveEmailSources($f, $d) {
|
|
file_put_contents($f, json_encode($d, JSON_PRETTY_PRINT));
|
|
}
|
|
|
|
// === EMAIL ACTIONS ===
|
|
// WEVIA Life v2 - Dashboard API endpoints
|
|
// Added to wevialife-api.php
|
|
|
|
// === DASHBOARD: Morning Brief ===
|
|
|
|
// Auto-rebalance Eisenhower on each brief load
|
|
function rebalance_eisenhower($pdo) {
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='eliminate', requires_action='f' WHERE (resolved IS NULL OR resolved = false) AND category='transactional' AND (subject LIKE '%created%' OR subject LIKE '%rebuilt%' OR subject LIKE '%Server %' OR subject LIKE '%Verify%' OR subject LIKE '%confirmation code%')");
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='eliminate', requires_action='f' WHERE (resolved IS NULL OR resolved = false) AND category='newsletter'");
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='eliminate' WHERE (resolved IS NULL OR resolved = false) AND category='transactional' AND subject NOT LIKE '%Payment%' AND subject NOT LIKE '%Warning%' AND subject NOT LIKE '%expired%' AND requires_action='f'");
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='schedule', importance_score=4 WHERE category LIKE '%opportunity%'");
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='do_first', urgency_score=5, requires_action='t' WHERE (resolved IS NULL OR resolved = false) AND (subject LIKE '%Payment Warning%' OR subject LIKE '%SSL%expired%' OR subject LIKE '%NOTICE%expired%')");
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='schedule' WHERE category LIKE '%risk%' AND eisenhower_quadrant='do_first' AND subject NOT LIKE '%expired%' AND subject NOT LIKE '%Payment%' AND subject NOT LIKE '%Warning%'");
|
|
$pdo->exec("UPDATE admin.email_classifications SET eisenhower_quadrant='schedule' WHERE category LIKE '%opportunity%risk%'");
|
|
}
|
|
|
|
// === DISMISS ALERT ===
|
|
if ($action === 'dismiss_alert') {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$id = (int)($input['id'] ?? 0);
|
|
$note = $input['note'] ?? 'Dismissed from UI';
|
|
if (!$id) { echo json_encode(['error' => 'missing id']); exit; }
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$pdo->exec("SET search_path TO admin,public");
|
|
$stmt = $pdo->prepare("UPDATE email_classifications SET resolved = true, resolved_note = ?, eisenhower_quadrant = 'eliminate', requires_action = false WHERE id = ?");
|
|
$stmt->execute([$note, $id]);
|
|
echo json_encode(['ok' => true, 'dismissed' => $stmt->rowCount()]);
|
|
exit;
|
|
}
|
|
|
|
// === AUTO-RESOLVE ===
|
|
if ($action === 'auto_resolve') {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$pdo->exec("SET search_path TO admin,public");
|
|
$total = 0;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: 2FA/code', eisenhower_quadrant='eliminate', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND (LOWER(subject) LIKE '%your%code%' OR LOWER(subject) LIKE '%verification%code%' OR LOWER(subject) LIKE '%confirm%email%')");
|
|
$total += (int)$r;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: password', eisenhower_quadrant='eliminate', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND (LOWER(subject) LIKE '%password%changed%' OR LOWER(subject) LIKE '%password%reset%')");
|
|
$total += (int)$r;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: server ops', eisenhower_quadrant='eliminate', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND (LOWER(subject) LIKE '%server%created%' OR LOWER(subject) LIKE '%server%rebuilt%' OR LOWER(subject) LIKE '%hardware reset%')");
|
|
$total += (int)$r;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: old newsletter', eisenhower_quadrant='eliminate', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND category IN ('newsletter','spam') AND received_at < NOW() - INTERVAL '7 days'");
|
|
$total += (int)$r;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: old transactional', eisenhower_quadrant='eliminate', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND category='transactional' AND received_at < NOW() - INTERVAL '14 days'");
|
|
$total += (int)$r;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: sent by user', eisenhower_quadrant='eliminate', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND folder='SENT'");
|
|
$total += (int)$r;
|
|
$sent_subjs = $pdo->query("SELECT REPLACE(subject, '[SENT] ', '') FROM email_classifications WHERE folder='SENT'")->fetchAll(PDO::FETCH_COLUMN);
|
|
$matched = 0;
|
|
foreach ($sent_subjs as $s) {
|
|
$s = trim(str_replace(['RE: ','Re: ','Fwd: '], '', $s));
|
|
if (strlen($s) < 10) continue;
|
|
$r = $pdo->exec("UPDATE email_classifications SET resolved=true, resolved_note='Auto: replied', requires_action=false WHERE (resolved IS NULL OR resolved=false) AND folder='INBOX' AND subject LIKE '%" . pg_escape_string(substr($s,0,50)) . "%'");
|
|
$matched += (int)$r;
|
|
}
|
|
$total += $matched;
|
|
$stats = $pdo->query("SELECT COALESCE(resolved,false) as r, COUNT(*) FROM email_classifications GROUP BY resolved")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['resolved' => $total, 'matched_replies' => $matched, 'stats' => $stats]);
|
|
exit;
|
|
}
|
|
|
|
// === SYNC SENT ===
|
|
if ($action === 'sync_sent') {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$pdo->exec("SET search_path TO admin,public");
|
|
$mb = '{server105.web-hosting.com:993/imap/ssl/novalidate-cert}';
|
|
$conn = @imap_open($mb.'INBOX.Sent', 'ymahboub@weval-consulting.com', base64_decode('WUBAY2luZTE5ODVAQA=='), 0, 1);
|
|
if (!$conn) { echo json_encode(['error'=>imap_last_error()]); exit; }
|
|
$info = imap_check($conn); $total = $info->Nmsgs;
|
|
$start = max(1, $total - 99);
|
|
$ov = imap_fetch_overview($conn, "$start:$total", 0);
|
|
$stmt = $pdo->prepare("INSERT INTO email_classifications (uid, folder, from_email, from_name, to_email, subject, received_at, category, eisenhower_quadrant, summary, resolved, resolved_note) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (uid,folder) DO NOTHING");
|
|
$ok = 0;
|
|
foreach (array_reverse($ov?:[]) as $o) {
|
|
$uid=(int)($o->uid??0); if(!$uid) continue;
|
|
$subj=isset($o->subject)?@imap_utf8($o->subject):'';
|
|
$to=isset($o->to)?@imap_utf8($o->to):'';
|
|
$date=isset($o->date)?date('Y-m-d H:i:s',strtotime($o->date)):date('Y-m-d');
|
|
$stmt->execute([$uid,'SENT','ymahboub@weval-consulting.com','Yacine (ENVOYE)',$to,'[SENT] '.$subj,$date,'sent_by_user','eliminate',"ENVOYE: $subj",true,'Sent by user']);
|
|
$ok++;
|
|
}
|
|
imap_close($conn);
|
|
echo json_encode(['synced'=>$ok,'total_sent'=>$total]);
|
|
exit;
|
|
}
|
|
|
|
|
|
// === DESKTOP FILE SYNC — receives files from Razer Blade Sentinel ===
|
|
if ($action === 'desktop_sync') {
|
|
header('Content-Type: application/json');
|
|
$uploadDir = '/var/www/weval/wevia-ia/wevialife-data/documents/';
|
|
@mkdir($uploadDir, 0755, true);
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$pdo->exec("SET search_path TO admin,public");
|
|
|
|
// Accept POST with file content (base64) or multipart upload
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (isset($input['files']) && is_array($input['files'])) {
|
|
// Batch sync: [{name, content_b64, modified}]
|
|
$synced = 0;
|
|
foreach ($input['files'] as $file) {
|
|
$name = preg_replace('/[^a-zA-Z0-9._\-\s]/', '_', $file['name'] ?? 'unknown');
|
|
$content = base64_decode($file['content_b64'] ?? '');
|
|
$modified = $file['modified'] ?? date('Y-m-d H:i:s');
|
|
if (!$content) continue;
|
|
|
|
$dest = $uploadDir . $name;
|
|
file_put_contents($dest, $content);
|
|
|
|
// Extract text for indexing
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
$text = '';
|
|
if (in_array($ext, ['txt','md','csv','json','log'])) {
|
|
$text = mb_substr($content, 0, 50000);
|
|
} elseif ($ext === 'pdf') {
|
|
@exec("pdftotext " . escapeshellarg($dest) . " - 2>/dev/null", $lines);
|
|
$text = implode("\n", $lines);
|
|
}
|
|
|
|
// Upsert to DB
|
|
$existing = $pdo->prepare("SELECT id FROM admin.documents WHERE filename = ?");
|
|
$existing->execute([$name]);
|
|
if ($existing->fetch()) {
|
|
$pdo->prepare("UPDATE admin.documents SET content_text = ?, filesize = ?, indexed_at = NOW(), source = 'desktop_sync' WHERE filename = ?")->execute([mb_substr($text, 0, 50000), strlen($content), $name]);
|
|
} else {
|
|
$pdo->prepare("INSERT INTO admin.documents (filename, filepath, filetype, filesize, content_text, source, indexed_at) VALUES (?,?,?,?,?,?,NOW())")->execute([$name, $dest, $ext, strlen($content), mb_substr($text, 0, 50000), 'desktop_sync']);
|
|
}
|
|
$synced++;
|
|
}
|
|
echo json_encode(['ok' => true, 'synced' => $synced]);
|
|
exit;
|
|
}
|
|
|
|
// Single file upload via multipart
|
|
if (isset($_FILES['file'])) {
|
|
$file = $_FILES['file'];
|
|
$name = preg_replace('/[^a-zA-Z0-9._\-\s]/', '_', $file['name']);
|
|
$dest = $uploadDir . $name;
|
|
move_uploaded_file($file['tmp_name'], $dest);
|
|
$pdo->prepare("INSERT INTO admin.documents (filename, filepath, filetype, filesize, source, indexed_at) VALUES (?,?,?,?,?,NOW()) ON CONFLICT DO NOTHING")->execute([$name, $dest, pathinfo($name, PATHINFO_EXTENSION), $file['size'], 'desktop_sync']);
|
|
echo json_encode(['ok' => true, 'file' => $name]);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// GET: list synced desktop files
|
|
$docs = $pdo->query("SELECT filename, filesize, source, indexed_at FROM admin.documents WHERE source = 'desktop_sync' ORDER BY indexed_at DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['documents' => $docs, 'total' => count($docs), 'sync_dir' => 'C:\\Users\\Yace\\Desktop\\WEVIA LIFE']);
|
|
exit;
|
|
}
|
|
|
|
|
|
// === CLAUDE MULTI-ACCOUNT SYNC ===
|
|
if ($action === 'claude_sync') {
|
|
header('Content-Type: application/json');
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$pdo->exec("SET search_path TO admin,public");
|
|
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$account = $input['account_id'] ?? $_GET['account'] ?? '';
|
|
|
|
// POST: receive transcripts
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($input['files'])) {
|
|
if (!$account) { echo json_encode(['error'=>'missing account_id']); exit; }
|
|
$uploadDir = '/var/www/weval/wevia-ia/wevialife-data/documents/';
|
|
@mkdir($uploadDir, 0755, true);
|
|
$synced = 0;
|
|
foreach ($input['files'] as $file) {
|
|
$name = preg_replace('/[^a-zA-Z0-9._\-\s]/', '_', $file['name'] ?? 'unknown');
|
|
$prefix = strtoupper($account);
|
|
$dest_name = "{$prefix}-{$name}";
|
|
$content = base64_decode($file['content_b64'] ?? '');
|
|
if (!$content) continue;
|
|
$dest = $uploadDir . $dest_name;
|
|
file_put_contents($dest, $content);
|
|
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
$text = in_array($ext, ['txt','md','csv','json','log']) ? mb_substr($content, 0, 50000) : '';
|
|
$existing = $pdo->prepare("SELECT id FROM documents WHERE filename = ?");
|
|
$existing->execute([$dest_name]);
|
|
if ($existing->fetch()) {
|
|
$pdo->prepare("UPDATE documents SET content_text=?, filesize=?, indexed_at=NOW(), claude_account=? WHERE filename=?")->execute([mb_substr($text,0,50000), strlen($content), $account, $dest_name]);
|
|
} else {
|
|
$pdo->prepare("INSERT INTO documents (filename,filepath,filetype,filesize,content_text,source,claude_account,indexed_at) VALUES (?,?,?,?,?,?,?,NOW())")->execute([$dest_name, $dest, $ext, strlen($content), mb_substr($text,0,50000), 'claude_transcript', $account]);
|
|
}
|
|
$synced++;
|
|
}
|
|
// Update account stats
|
|
$pdo->prepare("UPDATE claude_accounts SET last_sync=NOW(), total_transcripts=(SELECT COUNT(*) FROM documents WHERE claude_account=?), total_size_bytes=(SELECT COALESCE(SUM(filesize),0) FROM documents WHERE claude_account=?), last_transcript=(SELECT filename FROM documents WHERE claude_account=? ORDER BY indexed_at DESC LIMIT 1) WHERE account_id=?")->execute([$account,$account,$account,$account]);
|
|
echo json_encode(['ok'=>true,'synced'=>$synced,'account'=>$account]);
|
|
exit;
|
|
}
|
|
|
|
// GET: monitoring dashboard data
|
|
$accounts = $pdo->query("SELECT * FROM claude_accounts ORDER BY account_id")->fetchAll(PDO::FETCH_ASSOC);
|
|
$total_docs = $pdo->query("SELECT COUNT(*) FROM documents WHERE source IN ('claude_transcript','claude_output')")->fetchColumn();
|
|
$total_size = $pdo->query("SELECT COALESCE(SUM(filesize),0) FROM documents WHERE source IN ('claude_transcript','claude_output')")->fetchColumn();
|
|
$recent = $pdo->query("SELECT filename, claude_account, filesize, source, filetype, indexed_at FROM documents WHERE source IN ('claude_transcript','claude_output') ORDER BY indexed_at DESC LIMIT 30")->fetchAll(PDO::FETCH_ASSOC);
|
|
$by_account = $pdo->query("SELECT claude_account, COUNT(*) as cnt, COALESCE(SUM(filesize),0) as sz FROM documents WHERE source IN ('claude_transcript','claude_output') GROUP BY claude_account")->fetchAll(PDO::FETCH_ASSOC);
|
|
$by_source = $pdo->query("SELECT source, filetype, COUNT(*) as cnt FROM documents GROUP BY source, filetype ORDER BY source, cnt DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$all_sources = $pdo->query("SELECT source, COUNT(*) as cnt, COALESCE(SUM(filesize),0) as sz FROM documents GROUP BY source ORDER BY cnt DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['accounts'=>$accounts, 'total_docs'=>(int)$total_docs, 'total_size'=>(int)$total_size, 'recent'=>$recent, 'by_account'=>$by_account, 'by_source'=>$by_source, 'all_sources'=>$all_sources]);
|
|
exit;
|
|
}
|
|
if ($action === 'morning_brief') {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$today = date('Y-m-d');
|
|
rebalance_eisenhower($pdo);
|
|
|
|
// Urgent items (do_first)
|
|
$urgent = $pdo->query("SELECT * FROM admin.email_classifications WHERE eisenhower_quadrant='do_first' AND received_at > NOW() - INTERVAL '7 days' ORDER BY urgency_score DESC, received_at DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Scheduled items
|
|
$scheduled = $pdo->query("SELECT * FROM admin.email_classifications WHERE eisenhower_quadrant='schedule' AND received_at > NOW() - INTERVAL '7 days' ORDER BY importance_score DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Opportunities
|
|
$opportunities = $pdo->query("SELECT * FROM admin.email_classifications WHERE category='opportunity' AND received_at > NOW() - INTERVAL '14 days' ORDER BY importance_score DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Risks
|
|
$risks = $pdo->query("SELECT * FROM admin.email_classifications WHERE category='risk' AND received_at > NOW() - INTERVAL '14 days' ORDER BY urgency_score DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Deadlines coming up
|
|
$deadlines = $pdo->query("SELECT * FROM admin.email_classifications WHERE extracted_deadlines != '[]' AND received_at > NOW() - INTERVAL '30 days' ORDER BY received_at DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Actions required
|
|
$actions = $pdo->query("SELECT * FROM admin.email_classifications WHERE requires_action=true AND received_at > NOW() - INTERVAL '7 days' ORDER BY urgency_score DESC, importance_score DESC LIMIT 15")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Stats
|
|
$stats = $pdo->query("SELECT
|
|
COUNT(*) as total,
|
|
SUM(CASE WHEN eisenhower_quadrant='do_first' THEN 1 ELSE 0 END) as do_first,
|
|
SUM(CASE WHEN eisenhower_quadrant='schedule' THEN 1 ELSE 0 END) as schedule_count,
|
|
SUM(CASE WHEN eisenhower_quadrant='delegate' THEN 1 ELSE 0 END) as delegate_count,
|
|
SUM(CASE WHEN eisenhower_quadrant='eliminate' THEN 1 ELSE 0 END) as eliminate_count,
|
|
SUM(CASE WHEN category='opportunity' THEN 1 ELSE 0 END) as opportunities_count,
|
|
SUM(CASE WHEN category='risk' THEN 1 ELSE 0 END) as risks_count,
|
|
SUM(CASE WHEN requires_action=true THEN 1 ELSE 0 END) as actions_count
|
|
FROM admin.email_classifications")->fetch(PDO::FETCH_ASSOC);
|
|
|
|
// Unread today
|
|
$today_count = $pdo->query("SELECT COUNT(*) FROM admin.email_classifications WHERE DATE(received_at)='$today'")->fetchColumn();
|
|
|
|
echo json_encode([
|
|
'date' => $today,
|
|
'stats' => $stats,
|
|
'today_count' => (int)$today_count,
|
|
'urgent' => $urgent,
|
|
'scheduled' => $scheduled,
|
|
'opportunities' => $opportunities,
|
|
'risks' => $risks,
|
|
'deadlines' => $deadlines,
|
|
'actions' => $actions
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
// === EISENHOWER MATRIX ===
|
|
if ($action === 'eisenhower') {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$days = (int)($_GET['days'] ?? 7);
|
|
|
|
$q = $pdo->prepare("SELECT id, uid, folder, from_name, from_email, subject, received_at, category, urgency_score, importance_score, eisenhower_quadrant, requires_action, summary, suggested_action, extracted_deadlines FROM admin.email_classifications WHERE received_at > NOW() - INTERVAL '$days days' ORDER BY urgency_score DESC, importance_score DESC");
|
|
$q->execute();
|
|
$all = $q->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$matrix = ['do_first'=>[], 'schedule'=>[], 'delegate'=>[], 'eliminate'=>[]];
|
|
foreach ($all as $item) {
|
|
$quad = $item['eisenhower_quadrant'] ?? 'eliminate';
|
|
if (isset($matrix[$quad])) $matrix[$quad][] = $item;
|
|
}
|
|
|
|
echo json_encode(['matrix' => $matrix, 'total' => count($all), 'days' => $days]);
|
|
exit;
|
|
}
|
|
|
|
// === DOCUMENT UPLOAD ===
|
|
if ($action === 'upload') {
|
|
$uploadDir = '/var/www/weval/wevia-ia/wevialife-data/documents/';
|
|
@mkdir($uploadDir, 0755, true);
|
|
|
|
if (!isset($_FILES['file'])) { echo json_encode(['error' => 'no file']); exit; }
|
|
|
|
$file = $_FILES['file'];
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
$allowed = ['pdf','doc','docx','xls','xlsx','ppt','pptx','txt','csv','jpg','jpeg','png','gif','md'];
|
|
|
|
if (!in_array($ext, $allowed)) { echo json_encode(['error' => 'type not allowed: '.$ext]); exit; }
|
|
|
|
$dest = $uploadDir . date('Ymd_His') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file['name']);
|
|
move_uploaded_file($file['tmp_name'], $dest);
|
|
|
|
// Extract text based on type
|
|
$text = '';
|
|
if ($ext === 'txt' || $ext === 'md' || $ext === 'csv') {
|
|
$text = file_get_contents($dest);
|
|
} elseif ($ext === 'pdf') {
|
|
$text = shell_exec("pdftotext '$dest' - 2>/dev/null") ?: '';
|
|
if (!trim($text)) {
|
|
// OCR fallback for scanned PDFs
|
|
$text = shell_exec("tesseract '$dest' stdout 2>/dev/null") ?: '';
|
|
}
|
|
} elseif (in_array($ext, ['docx','doc'])) {
|
|
// Extract text from DOCX via unzip
|
|
$tmpDir = sys_get_temp_dir() . '/docx_' . uniqid();
|
|
@mkdir($tmpDir, 0755, true);
|
|
shell_exec("unzip -o '$dest' -d '$tmpDir' 2>/dev/null");
|
|
$xmlFile = $tmpDir . '/word/document.xml';
|
|
if (file_exists($xmlFile)) {
|
|
$xml = file_get_contents($xmlFile);
|
|
$text = strip_tags(str_replace(['</w:p>', '</w:r>'], ["\n", ' '], $xml));
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|
}
|
|
shell_exec("rm -rf '$tmpDir'");
|
|
} elseif (in_array($ext, ['xlsx','xls','csv'])) {
|
|
if ($ext === 'csv') {
|
|
$text = file_get_contents($dest);
|
|
} else {
|
|
// Extract from xlsx via unzip
|
|
$tmpDir = sys_get_temp_dir() . '/xlsx_' . uniqid();
|
|
@mkdir($tmpDir, 0755, true);
|
|
shell_exec("unzip -o '$dest' -d '$tmpDir' 2>/dev/null");
|
|
$sheets = glob($tmpDir . '/xl/worksheets/sheet*.xml');
|
|
$text = '';
|
|
foreach ($sheets as $sheet) {
|
|
$xml = file_get_contents($sheet);
|
|
$text .= strip_tags($xml) . "\n";
|
|
}
|
|
// Also get shared strings
|
|
$ss = $tmpDir . '/xl/sharedStrings.xml';
|
|
if (file_exists($ss)) {
|
|
$xml = file_get_contents($ss);
|
|
$text .= strip_tags($xml);
|
|
}
|
|
shell_exec("rm -rf '$tmpDir'");
|
|
}
|
|
} elseif (in_array($ext, ['pptx','ppt'])) {
|
|
$tmpDir = sys_get_temp_dir() . '/pptx_' . uniqid();
|
|
@mkdir($tmpDir, 0755, true);
|
|
shell_exec("unzip -o '$dest' -d '$tmpDir' 2>/dev/null");
|
|
$slides = glob($tmpDir . '/ppt/slides/slide*.xml');
|
|
$text = '';
|
|
foreach ($slides as $slide) {
|
|
$xml = file_get_contents($slide);
|
|
$text .= strip_tags(str_replace(['</a:p>'], ["\n"], $xml)) . "\n---\n";
|
|
}
|
|
shell_exec("rm -rf '$tmpDir'");
|
|
} elseif (in_array($ext, ['jpg','jpeg','png','gif'])) {
|
|
// OCR on images
|
|
$text = shell_exec("tesseract '$dest' stdout 2>/dev/null") ?: '';
|
|
}
|
|
// For DOCX/XLSX/PPTX - will use PHPOffice later
|
|
|
|
// Store in DB
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$ins = $pdo->prepare("INSERT INTO admin.documents (filename, filepath, filetype, filesize, content_text, source) VALUES (?,?,?,?,?,?)");
|
|
$ins->execute([$file['name'], $dest, $ext, $file['size'], mb_substr($text, 0, 50000), 'upload']);
|
|
|
|
echo json_encode(['ok' => true, 'id' => $pdo->lastInsertId(), 'filename' => $file['name'], 'size' => $file['size']]);
|
|
exit;
|
|
}
|
|
|
|
// === DOCUMENTS LIST ===
|
|
if ($action === 'documents') {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$docs = $pdo->query("SELECT id, filename, filetype, filesize, summary, indexed_at, source FROM admin.documents ORDER BY indexed_at DESC LIMIT 100")->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['documents' => $docs, 'total' => count($docs)]);
|
|
exit;
|
|
}
|
|
|
|
// === CLASSIFY STATUS ===
|
|
if ($action === 'classify_status') {
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$total = $pdo->query("SELECT COUNT(*) FROM admin.email_classifications")->fetchColumn();
|
|
$byQuad = $pdo->query("SELECT eisenhower_quadrant, COUNT(*) as cnt FROM admin.email_classifications GROUP BY eisenhower_quadrant")->fetchAll(PDO::FETCH_ASSOC);
|
|
$byCat = $pdo->query("SELECT category, COUNT(*) as cnt FROM admin.email_classifications GROUP BY category ORDER BY cnt DESC")->fetchAll(PDO::FETCH_ASSOC);
|
|
$latest = $pdo->query("SELECT classified_at FROM admin.email_classifications ORDER BY classified_at DESC LIMIT 1")->fetchColumn();
|
|
echo json_encode(['total' => (int)$total, 'by_quadrant' => $byQuad, 'by_category' => $byCat, 'latest' => $latest]);
|
|
exit;
|
|
}
|
|
|
|
// WEVIA Life - RAG Chat + Enhanced Upload
|
|
|
|
if ($action === 'ai_chat') {
|
|
$input = json_decode(file_get_contents('php://input'), true) ?: [];
|
|
$question = $input['message'] ?? $_REQUEST['message'] ?? '';
|
|
if (!$question) { echo json_encode(['error' => 'empty']); exit; }
|
|
|
|
require_once '/opt/wevads/vault/credentials.php';
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$context = "";
|
|
|
|
// Search emails by keywords
|
|
$words = array_filter(explode(' ', preg_replace('/[^a-zA-Z0-9 ]/ui', ' ', $question)));
|
|
$keywords = array_slice(array_filter($words, fn($w) => strlen($w) >= 3), 0, 5);
|
|
$emails = [];
|
|
|
|
if ($keywords) {
|
|
$clauses = []; $params = [];
|
|
foreach ($keywords as $kw) {
|
|
$clauses[] = "(LOWER(subject) LIKE ? OR LOWER(from_name) LIKE ? OR LOWER(summary) LIKE ?)";
|
|
$p = '%' . strtolower($kw) . '%';
|
|
$params = array_merge($params, [$p, $p, $p]);
|
|
}
|
|
$stmt = $pdo->prepare("SELECT from_name, from_email, subject, received_at, category, eisenhower_quadrant, summary, suggested_action, raw_body_preview FROM admin.email_classifications WHERE " . implode(' OR ', $clauses) . " ORDER BY received_at DESC LIMIT 10");
|
|
$stmt->execute($params);
|
|
$emails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
if ($emails) {
|
|
$context .= "=== EMAILS ===\n";
|
|
foreach ($emails as $e) {
|
|
$context .= "De: {$e['from_name']} | Objet: {$e['subject']} | Date: {$e['received_at']} | Cat: {$e['category']}\n";
|
|
if ($e['summary']) $context .= "Resume: {$e['summary']}\n";
|
|
if ($e['suggested_action']) $context .= "Action: {$e['suggested_action']}\n";
|
|
$context .= "---\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stats
|
|
$s = $pdo->query("SELECT COUNT(*) as t, SUM(CASE WHEN eisenhower_quadrant='do_first' THEN 1 ELSE 0 END) as u, SUM(CASE WHEN category LIKE '%opportunity%' THEN 1 ELSE 0 END) as o, SUM(CASE WHEN category LIKE '%risk%' THEN 1 ELSE 0 END) as r FROM admin.email_classifications")->fetch(PDO::FETCH_ASSOC);
|
|
$context .= "\nSTATS: {$s['t']} emails analyses, {$s['u']} urgents, {$s['o']} opportunites, {$s['r']} risques\n";
|
|
|
|
// Documents
|
|
$docs = $pdo->query("SELECT filename, filetype, summary, content_text FROM admin.documents WHERE content_text IS NOT NULL AND content_text != '' ORDER BY indexed_at DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
|
if ($docs) {
|
|
$context .= "\n=== DOCUMENTS ===\n";
|
|
foreach ($docs as $d) {
|
|
$context .= "Fichier: {$d['filename']} | Resume: " . mb_substr($d['summary'] ?? $d['content_text'], 0, 300) . "\n---\n";
|
|
}
|
|
}
|
|
|
|
// Recent important
|
|
$recent = $pdo->query("SELECT from_name, subject, summary, eisenhower_quadrant FROM admin.email_classifications WHERE eisenhower_quadrant IN ('do_first','schedule') ORDER BY received_at DESC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
|
|
if ($recent) {
|
|
$context .= "\n=== RECENTS IMPORTANTS ===\n";
|
|
foreach ($recent as $e) $context .= "{$e['from_name']}: {$e['subject']} [{$e['eisenhower_quadrant']}] {$e['summary']}\n";
|
|
}
|
|
|
|
// Call Groq 70B (fallback 8B)
|
|
$sys = "Tu es WEVIA Life, assistant executif IA souverain de WEVAL Consulting. Tu assistes Yacine Mahboub (CEO) et son equipe dirigeante (Ambre: Pharma/Marketing, Kaouther: Ethica Tunisie).
|
|
|
|
WEVAL = cabinet conseil transformation digitale (SAP, Cloud, IA, Cybersecurite, Supply Chain, Life Sciences). Casablanca HQ, Paris, 8 pays, 200+ projets, partenaires SAP/Vistex/Huawei Cloud/IQVIA.
|
|
|
|
Produits: WEVADS (217K emails/jour, 7.3M contacts), Ethica (48,899 HCPs MA/TN/DZ, 98%% emails), WEVIA (51 modeles Ollama GPU souverain), CRM (7 companies pipeline).
|
|
|
|
REGLES: Reponds en francais, concis et actionnable. Structure: bullet points decisions, tableaux comparaisons. Propose 2-3 options concretes. Cite les sources (expediteur, date). Signale les risques business. ZERO envoi automatique sur tous les serveurs - tout envoi valide manuellement.";
|
|
|
|
$models = ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'];
|
|
$reply = null;
|
|
foreach ($models as $model) {
|
|
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
|
|
curl_setopt_array($ch, [CURLOPT_HTTPHEADER=>['Authorization: Bearer '.GROQ_KEY,'Content-Type: application/json'],CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>json_encode(['model'=>$model,'messages'=>[['role'=>'system','content'=>$sys],['role'=>'user','content'=>"CONTEXTE:\n".mb_substr($context,0,4000)."\n\nQUESTION: $question"]],'temperature'=>0.3,'max_tokens'=>800]),CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>25]);
|
|
$resp = curl_exec($ch); $hc = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
|
|
if ($hc === 200) {
|
|
$data = json_decode($resp, true);
|
|
$reply = $data['choices'][0]['message']['content'] ?? null;
|
|
if ($reply) break;
|
|
}
|
|
}
|
|
|
|
echo json_encode(['reply' => $reply ?? 'Service temporairement indisponible.', 'context_emails' => count($emails), 'context_docs' => count($docs)]);
|
|
exit;
|
|
}
|
|
|
|
// === ENHANCED UPLOAD WITH PARSING ===
|
|
if ($action === 'upload_parse') {
|
|
$uploadDir = '/var/www/weval/wevia-ia/wevialife-data/documents/';
|
|
@mkdir($uploadDir, 0755, true);
|
|
if (!isset($_FILES['file'])) { echo json_encode(['error' => 'no file']); exit; }
|
|
$file = $_FILES['file'];
|
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
|
$dest = $uploadDir . date('Ymd_His') . '_' . preg_replace('/[^a-zA-Z0-9._-]/', '_', $file['name']);
|
|
move_uploaded_file($file['tmp_name'], $dest);
|
|
|
|
// Parse text
|
|
require_once '/var/www/weval/wevia-ia/scripts/doc-parser.php';
|
|
$parsed = parseDocument($dest, $ext);
|
|
$analysis = analyzeDocument($parsed['text'], $file['name']);
|
|
|
|
$pdo = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'", "postgres", "");
|
|
$ins = $pdo->prepare("INSERT INTO admin.documents (filename,filepath,filetype,filesize,content_text,summary,extracted_deadlines,extracted_actions,page_count,source) VALUES (?,?,?,?,?,?,?,?,?,?)");
|
|
$ins->execute([$file['name'], $dest, $ext, $file['size'], mb_substr($parsed['text'], 0, 50000), $analysis['summary'] ?? '', json_encode($analysis['deadlines'] ?? []), json_encode($analysis['actions'] ?? []), $parsed['pages'], 'upload']);
|
|
|
|
// Create action items from document
|
|
if (!empty($analysis['actions'])) {
|
|
foreach ($analysis['actions'] as $act) {
|
|
$pdo->prepare("INSERT INTO admin.action_items (title, description, source_type, source_id, urgency_score, importance_score, eisenhower_quadrant, status) VALUES (?,?,?,?,?,?,?,?)")->execute([
|
|
$act['title'] ?? 'Action', $file['name'], 'document', $pdo->lastInsertId(),
|
|
$act['priority'] === 'high' ? 4 : ($act['priority'] === 'medium' ? 3 : 2),
|
|
$act['priority'] === 'high' ? 4 : ($act['priority'] === 'medium' ? 3 : 2),
|
|
$act['priority'] === 'high' ? 'do_first' : 'schedule', 'pending'
|
|
]);
|
|
}
|
|
}
|
|
|
|
echo json_encode(['ok'=>true, 'id'=>$pdo->lastInsertId(), 'filename'=>$file['name'], 'pages'=>$parsed['pages'], 'text_length'=>strlen($parsed['text']), 'summary'=>$analysis['summary']??'', 'actions'=>$analysis['actions']??[]]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'email_sources') {
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
// Don't expose passwords
|
|
$safe = array_map(function($s) {
|
|
return ['id'=>$s['id'],'name'=>$s['name'],'email'=>$s['email'],'type'=>$s['type'],'server'=>$s['server'],'status'=>$s['status']??'configured','last_sync'=>$s['last_sync']??null,'count'=>$s['count']??0];
|
|
}, $sources);
|
|
echo json_encode(['sources' => $safe]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'email_connect') {
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$source = [
|
|
'id' => 'src_' . uniqid(),
|
|
'name' => $input['name'] ?? '',
|
|
'email' => $input['email'] ?? '',
|
|
'type' => $input['type'] ?? 'imap',
|
|
'server' => $input['server'] ?? '',
|
|
'port' => (int)($input['port'] ?? 993),
|
|
'password' => base64_encode($input['password'] ?? ''),
|
|
'ssl' => $input['ssl'] ?? true,
|
|
'status' => 'testing',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// Test connection
|
|
$mailbox = '{' . $source['server'] . ':' . $source['port'] . '/imap/ssl/novalidate-cert}INBOX';
|
|
$conn = @imap_open($mailbox, $source['email'], base64_decode($source['password']), 0, 1);
|
|
if ($conn) {
|
|
$info = imap_check($conn);
|
|
$source['status'] = 'active';
|
|
$source['count'] = $info->Nmsgs ?? 0;
|
|
$source['last_sync'] = date('Y-m-d H:i:s');
|
|
imap_close($conn);
|
|
} else {
|
|
$source['status'] = 'error';
|
|
$source['error'] = imap_last_error();
|
|
}
|
|
|
|
// Remove existing source with same email
|
|
$sources = array_values(array_filter($sources, fn($s) => $s['email'] !== $source['email']));
|
|
$sources[] = $source;
|
|
saveEmailSources($emailSourcesFile, $sources);
|
|
|
|
echo json_encode(['ok' => true, 'status' => $source['status'], 'error' => $source['error'] ?? null, 'count' => $source['count'] ?? 0]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'email_disconnect') {
|
|
$id = $_GET['id'] ?? '';
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$sources = array_values(array_filter($sources, fn($s) => $s['id'] !== $id));
|
|
saveEmailSources($emailSourcesFile, $sources);
|
|
echo json_encode(['ok' => true]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'email_fetch') {
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$allEmails = [];
|
|
$limit = (int)($_GET['limit'] ?? 50);
|
|
$folders = ["INBOX"];
|
|
foreach ($sources as $s) {
|
|
if ($s['status'] !== 'active') continue;
|
|
$pwd = base64_decode($s['password'] ?? '');
|
|
$server = $s['server'] ?? 'server105.web-hosting.com';
|
|
$port = $s['port'] ?? 993;
|
|
foreach ($folders as $folder) {
|
|
$mb = "{{$server}:{$port}/imap/ssl/novalidate-cert}{$folder}";
|
|
$conn = @imap_open($mb, $s['email'], $pwd, 0, 1);
|
|
if (!$conn) continue;
|
|
$info = imap_check($conn);
|
|
$total = $info->Nmsgs;
|
|
$start = max(1, $total - $limit + 1);
|
|
$ov = imap_fetch_overview($conn, "$start:$total", 0);
|
|
foreach (array_reverse($ov ?: []) as $o) {
|
|
$allEmails[] = [
|
|
'uid' => $o->uid,
|
|
'from' => isset($o->from) ? mb_decode_mimeheader($o->from) : '',
|
|
'subject' => isset($o->subject) ? mb_decode_mimeheader($o->subject) : '',
|
|
'date' => $o->date ?? '',
|
|
'seen' => ($o->seen ?? 0) ? true : false,
|
|
'folder' => $folder,
|
|
'source' => $s['email'],
|
|
'source_id' => $s['id']
|
|
];
|
|
}
|
|
imap_close($conn);
|
|
}
|
|
}
|
|
usort($allEmails, function($a, $b) { return strtotime($b['date'] ?? '0') - strtotime($a['date'] ?? '0'); });
|
|
echo json_encode(['emails' => $allEmails, 'total' => count($allEmails)]);
|
|
exit;
|
|
}
|
|
if (false && $action === 'email_fetch_disabled') {
|
|
$srcId = $_GET['source'] ?? '';
|
|
$page = (int)($_GET['page'] ?? 1);
|
|
$perPage = 20;
|
|
$search = $_GET['q'] ?? '';
|
|
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$source = null;
|
|
foreach ($sources as $s) { if ($s['id'] === $srcId || $s['email'] === $srcId) { $source = $s; break; } }
|
|
|
|
if (!$source) {
|
|
// If no specific source, try all active sources
|
|
$allEmails = [];
|
|
foreach ($sources as $s) {
|
|
if ($s['status'] !== 'active') continue;
|
|
$mailbox = '{' . $s['server'] . ':' . $s['port'] . '/imap/ssl/novalidate-cert}INBOX';
|
|
$conn = @imap_open($mailbox, $s['email'], base64_decode($s['password']), 0, 1);
|
|
if (!$conn) continue;
|
|
$emails = fetchEmails($conn, $s, $search, $perPage);
|
|
$allEmails = array_merge($allEmails, $emails);
|
|
imap_close($conn);
|
|
}
|
|
usort($allEmails, fn($a,$b) => strtotime($b['date']) - strtotime($a['date']));
|
|
echo json_encode(['emails' => array_slice($allEmails, 0, $perPage), 'total' => count($allEmails)]);
|
|
exit;
|
|
}
|
|
|
|
$mailbox = '{' . $source['server'] . ':' . $source['port'] . '/imap/ssl/novalidate-cert}INBOX';
|
|
$conn = @imap_open($mailbox, $source['email'], base64_decode($source['password']), 0, 1);
|
|
if (!$conn) {
|
|
echo json_encode(['error' => 'Connection failed: ' . imap_last_error()]);
|
|
exit;
|
|
}
|
|
|
|
$emails = fetchEmails($conn, $source, $search, $perPage);
|
|
$info = imap_check($conn);
|
|
imap_close($conn);
|
|
|
|
echo json_encode(['emails' => $emails, 'total' => $info->Nmsgs ?? 0, 'source' => $source['email']]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'email_read') {
|
|
$srcId = $_GET['source'] ?? '';
|
|
$uid = (int)($_GET['uid'] ?? 0);
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$source = null;
|
|
foreach ($sources as $s) { if ($s['id'] === $srcId || $s['email'] === $srcId) { $source = $s; break; } }
|
|
if (!$source || !$uid) { echo json_encode(['error' => 'Invalid']); exit; }
|
|
$folder = $_GET['folder'] ?? 'INBOX';
|
|
$url = 'http://10.1.0.3:5890/api/imap-proxy.php?a=read&e=' . urlencode($source['email']) . '&sv=' . urlencode($source['server']) . '&pw=' . urlencode($source['password']) . '&uid=' . $uid . '&folder=' . urlencode($folder);
|
|
$ctx = stream_context_create(['http' => ['timeout' => 30]]);
|
|
$resp = @file_get_contents($url, false, $ctx);
|
|
if ($resp) { $d = json_decode($resp, true); if ($d) { $d['source'] = $source['email']; echo json_encode($d); exit; } }
|
|
echo json_encode(['error' => 'proxy failed']);
|
|
exit;
|
|
}
|
|
if (false && $action === 'email_read_disabled') {
|
|
$srcId = $_GET['source'] ?? '';
|
|
$uid = (int)($_GET['uid'] ?? 0);
|
|
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$source = null;
|
|
foreach ($sources as $s) { if ($s['id'] === $srcId || $s['email'] === $srcId) { $source = $s; break; } }
|
|
if (!$source || !$uid) { echo json_encode(['error' => 'Invalid']); exit; }
|
|
|
|
$mailbox = '{' . $source['server'] . ':' . $source['port'] . '/imap/ssl/novalidate-cert}INBOX';
|
|
$conn = @imap_open($mailbox, $source['email'], base64_decode($source['password']), 0, 1);
|
|
if (!$conn) { echo json_encode(['error' => 'Connection failed']); exit; }
|
|
|
|
$msgno = imap_msgno($conn, $uid);
|
|
if (!$msgno) { imap_close($conn); echo json_encode(['error' => 'Message not found']); exit; }
|
|
|
|
$header = imap_headerinfo($conn, $msgno);
|
|
$body = getEmailBody($conn, $msgno);
|
|
imap_close($conn);
|
|
|
|
echo json_encode([
|
|
'uid' => $uid,
|
|
'from' => decodeHeader($header->fromaddress ?? ''),
|
|
'to' => decodeHeader($header->toaddress ?? ''),
|
|
'subject' => decodeHeader($header->subject ?? ''),
|
|
'date' => date('Y-m-d H:i:s', strtotime($header->date ?? '')),
|
|
'body' => $body,
|
|
'source' => $source['email']
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'email_search') {
|
|
$q = $_GET['q'] ?? '';
|
|
if (!$q) { echo json_encode(['emails' => []]); exit; }
|
|
|
|
$sources = loadEmailSources($emailSourcesFile);
|
|
$allEmails = [];
|
|
foreach ($sources as $s) {
|
|
if ($s['status'] !== 'active') continue;
|
|
$mailbox = '{' . $s['server'] . ':' . $s['port'] . '/imap/ssl/novalidate-cert}INBOX';
|
|
$conn = @imap_open($mailbox, $s['email'], base64_decode($s['password']), 0, 1);
|
|
if (!$conn) continue;
|
|
$emails = fetchEmails($conn, $s, $q, 30);
|
|
$allEmails = array_merge($allEmails, $emails);
|
|
imap_close($conn);
|
|
}
|
|
usort($allEmails, fn($a,$b) => strtotime($b['date']) - strtotime($a['date']));
|
|
echo json_encode(['emails' => array_slice($allEmails, 0, 30), 'query' => $q]);
|
|
exit;
|
|
}
|
|
|
|
// === HELPER FUNCTIONS ===
|
|
function fetchEmails($conn, $source, $search = '', $limit = 20) {
|
|
$emails = [];
|
|
if ($search) {
|
|
$uids = imap_search($conn, 'SUBJECT "' . addslashes($search) . '"', SE_UID);
|
|
if (!$uids) $uids = imap_search($conn, 'FROM "' . addslashes($search) . '"', SE_UID);
|
|
if (!$uids) $uids = imap_search($conn, 'BODY "' . addslashes($search) . '"', SE_UID);
|
|
if (!$uids) return [];
|
|
$uids = array_reverse($uids);
|
|
$uids = array_slice($uids, 0, $limit);
|
|
} else {
|
|
$info = imap_check($conn);
|
|
$total = $info->Nmsgs;
|
|
if ($total == 0) return [];
|
|
$start = max(1, $total - $limit + 1);
|
|
$range = "$start:$total";
|
|
$overview = imap_fetch_overview($conn, $range, 0);
|
|
if (!$overview) return [];
|
|
$uids = array_map(fn($o) => $o->uid, $overview);
|
|
$uids = array_reverse($uids);
|
|
}
|
|
|
|
foreach ($uids as $uid) {
|
|
$msgno = @imap_msgno($conn, $uid);
|
|
if (!$msgno) continue;
|
|
$header = @imap_headerinfo($conn, $msgno);
|
|
if (!$header) continue;
|
|
$emails[] = [
|
|
'uid' => $uid,
|
|
'from' => decodeHeader($header->fromaddress ?? ''),
|
|
'from_email' => $header->from[0]->mailbox . '@' . ($header->from[0]->host ?? ''),
|
|
'to' => decodeHeader($header->toaddress ?? ''),
|
|
'subject' => decodeHeader($header->subject ?? '(sans sujet)'),
|
|
'date' => date('Y-m-d H:i:s', strtotime($header->date ?? '')),
|
|
'seen' => ($header->Unseen ?? '') !== 'U',
|
|
'size' => $header->Size ?? 0,
|
|
'source' => $source['email'],
|
|
'source_id' => $source['id']
|
|
];
|
|
}
|
|
return $emails;
|
|
}
|
|
|
|
function getEmailBody($conn, $msgno) {
|
|
$struct = imap_fetchstructure($conn, $msgno);
|
|
$body = '';
|
|
|
|
if ($struct->type == 0) { // Simple
|
|
$body = imap_fetchbody($conn, $msgno, 1);
|
|
if ($struct->encoding == 3) $body = base64_decode($body);
|
|
if ($struct->encoding == 4) $body = quoted_printable_decode($body);
|
|
} else { // Multipart
|
|
$body = getMultipartBody($conn, $msgno, $struct);
|
|
}
|
|
|
|
// Try to detect charset and convert to UTF-8
|
|
if ($struct->parameters ?? null) {
|
|
foreach ($struct->parameters as $p) {
|
|
if (strtolower($p->attribute) === 'charset' && strtolower($p->value) !== 'utf-8') {
|
|
$body = @mb_convert_encoding($body, 'UTF-8', $p->value) ?: $body;
|
|
}
|
|
}
|
|
}
|
|
|
|
return mb_substr($body, 0, 50000); // Limit size
|
|
}
|
|
|
|
function getMultipartBody($conn, $msgno, $struct, $prefix = '') {
|
|
$body = '';
|
|
foreach ($struct->parts as $i => $part) {
|
|
$partNum = $prefix ? "$prefix." . ($i + 1) : ($i + 1);
|
|
if ($part->type == 0 && ($part->subtype === 'HTML' || $part->subtype === 'PLAIN')) {
|
|
$text = imap_fetchbody($conn, $msgno, $partNum);
|
|
if ($part->encoding == 3) $text = base64_decode($text);
|
|
if ($part->encoding == 4) $text = quoted_printable_decode($text);
|
|
if ($part->subtype === 'HTML') return $text;
|
|
$body = $text;
|
|
}
|
|
if (isset($part->parts)) {
|
|
$sub = getMultipartBody($conn, $msgno, $part, $partNum);
|
|
if ($sub) return $sub;
|
|
}
|
|
}
|
|
return $body;
|
|
}
|
|
|
|
function decodeHeader($str) {
|
|
$decoded = imap_mime_header_decode($str);
|
|
$result = '';
|
|
foreach ($decoded as $part) {
|
|
$charset = $part->charset;
|
|
$text = $part->text;
|
|
if ($charset && $charset !== 'default' && strtolower($charset) !== 'utf-8') {
|
|
$text = @mb_convert_encoding($text, 'UTF-8', $charset) ?: $text;
|
|
}
|
|
$result .= $text;
|
|
}
|
|
return $result ?: $str;
|
|
}
|
|
|
|
|
|
|
|
// EMAIL RESPONSE DRAFTER GLM5 Alibaba cascade
|
|
if ($action === 'draft_response') {
|
|
$uid = $_REQUEST['uid'] ?? ''; $folder = $_REQUEST['folder'] ?? 'INBOX'; $tone = $_REQUEST['tone'] ?? 'professional'; $instructions = $_REQUEST['instructions'] ?? '';
|
|
if (!$uid) { echo json_encode(['error'=>'uid required']); exit; }
|
|
$classif = null;
|
|
try { $pdo_c = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'","postgres",""); $st = $pdo_c->prepare("SELECT * FROM admin.email_classifications WHERE uid=? AND folder=?"); $st->execute([$uid, $folder]); $classif = $st->fetch(PDO::FETCH_ASSOC); } catch(Exception $e) {}
|
|
$sf = "/var/www/weval/wevia-ia/wevialife-data/email-sources.json"; $sources = file_exists($sf) ? json_decode(file_get_contents($sf), true) : [];
|
|
$email_body = ''; $email_from = $classif['from_email'] ?? ''; $email_subject = $classif['subject'] ?? ''; $email_date = $classif['received_at'] ?? '';
|
|
foreach ($sources as $src) { if ($src['status'] !== 'active') continue; $params = 'e='.urlencode($src['email']).'&sv='.urlencode($src['server']).'&pw='.urlencode($src['password']); $url = "http://10.1.0.3:5890/api/imap-proxy.php?a=read&$params&folder=$folder&uid=$uid"; $resp = @file_get_contents($url, false, stream_context_create(['http'=>['timeout'=>15]])); if ($resp) { $data = json_decode($resp, true); if ($data && isset($data['body'])) { $email_body = strip_tags($data['body']); $email_from = $data['from'] ?? $email_from; $email_subject = $data['subject'] ?? $email_subject; break; } } }
|
|
$history = [];
|
|
if ($email_from) { try { $fc = preg_replace('/.*<|>.*/', '', $email_from); $st2 = $pdo_c->prepare("SELECT subject, category, summary, eisenhower_quadrant, received_at FROM admin.email_classifications WHERE from_email LIKE ? ORDER BY received_at DESC LIMIT 5"); $st2->execute(['%'.trim($fc).'%']); $history = $st2->fetchAll(PDO::FETCH_ASSOC); } catch(Exception $e) {} }
|
|
$ht = ''; if ($history) { $ht = "\nHISTORIQUE:\n"; foreach ($history as $h) { $ht .= "- [{$h['received_at']}] {$h['subject']} ({$h['category']}): {$h['summary']}\n"; } }
|
|
$urg = $classif ? "Urgence: {$classif['urgency_score']}/5, Quadrant: {$classif['eisenhower_quadrant']}, Cat: {$classif['category']}" : "Non classifie";
|
|
$sug = $classif['suggested_action'] ?? '';
|
|
$tones = ['professional'=>'professionnel et courtois','friendly'=>'amical et chaleureux','formal'=>'formel et institutionnel','brief'=>'bref et direct (max 3 phrases)'];
|
|
$td = $tones[$tone] ?? 'professionnel';
|
|
$sp = "Tu es l'assistant executif de Yacine Mahboub, CEO de WEVAL Consulting. Redige des reponses email au nom de Yacine. Ton: $td. Sig: Yacine Mahboub | CEO WEVAL Consulting | +212 6 57 78 52 92";
|
|
$up = "Redige une reponse:\nDE: $email_from\nSUJET: $email_subject\nDATE: $email_date\n$urg\n".($sug?"ACTION: $sug\n":"")."$ht\nCONTENU:\n".mb_substr($email_body,0,2000)."\n".($instructions?"INSTRUCTIONS: $instructions\n":"")."\nRedige UNIQUEMENT le corps. Francais sauf si email en anglais.";
|
|
$draft = null; $prov = 'none';
|
|
$nk = trim(@file_get_contents('/var/www/html/api/blade-tasks/nvidia-key.txt'));
|
|
$pvs = [
|
|
['n'=>'Sovereign-Cerebras','u'=>'http://127.0.0.1:4000/v1/chat/completions','k'=>'local','m'=>'auto','t'=>10,'x'=>[]],
|
|
['n'=>'GLM-5','u'=>'https://integrate.api.nvidia.com/v1/chat/completions','k'=>$nk,'m'=>'z-ai/glm5','t'=>15,'x'=>['stream'=>false]],
|
|
['n'=>'Alibaba','u'=>'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions','k'=>'sk-34db1ad3152443cd86563d1bfc576c30','m'=>'qwen-plus','t'=>15,'x'=>[]],
|
|
['n'=>'Ollama-WEVIA','u'=>'http://127.0.0.1:11434/v1/chat/completions','k'=>'local','m'=>'weval-brain-v3','t'=>30,'x'=>[]]
|
|
];
|
|
foreach ($pvs as $p) { if (!$p['k'] && $p['n'] !== 'Sovereign-Cerebras' && $p['n'] !== 'Ollama-WEVIA') continue; $b = array_merge(['model'=>$p['m'],'temperature'=>0.4,'max_tokens'=>800,'messages'=>[['role'=>'system','content'=>$sp],['role'=>'user','content'=>$up]]], $p['x']); $ch = curl_init($p['u']); curl_setopt_array($ch, [CURLOPT_HTTPHEADER=>(in_array($p['n'],['Sovereign-Cerebras','Ollama-WEVIA']) ? ['Content-Type: application/json'] : ['Authorization: Bearer '.$p['k'],'Content-Type: application/json']),CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>json_encode($b),CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>$p['t']]); $r = curl_exec($ch); $hc = curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); if ($hc===200 && $r) { $d=json_decode($r,true); $t=trim($d['choices'][0]['message']['content']??''); $t=preg_replace('/^```\w*\s*/','',trim($t)); $t=preg_replace('/\s*```$/','',$t); if(strlen($t)>20){$draft=$t;$prov=$p['n'];break;} } }
|
|
echo json_encode(['ok'=>(bool)$draft,'draft'=>$draft?:'Erreur generation.','provider'=>$prov,'email'=>['from'=>$email_from,'subject'=>$email_subject,'date'=>$email_date],'classification'=>$classif?['category'=>$classif['category'],'urgency'=>$classif['urgency_score'],'quadrant'=>$classif['eisenhower_quadrant'],'summary'=>$classif['summary']]:null,'tone'=>$tone,'history_count'=>count($history)],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE); exit;
|
|
}
|
|
|
|
|
|
if ($action === 'all_emails') {
|
|
$limit = intval($_REQUEST['limit'] ?? 50); $offset = intval($_REQUEST['offset'] ?? 0);
|
|
try { $pdo_c = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'","postgres","");
|
|
$total = $pdo_c->query("SELECT count(*) FROM admin.email_classifications")->fetchColumn();
|
|
$st = $pdo_c->prepare("SELECT uid, folder, from_email, from_name, subject, received_at, category, urgency_score, importance_score, eisenhower_quadrant, requires_action, summary, suggested_action FROM admin.email_classifications ORDER BY urgency_score DESC, received_at DESC LIMIT ? OFFSET ?"); $st->execute([$limit, $offset]); $emails = $st->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['ok'=>true,'count'=>count($emails),'total'=>(int)$total,'offset'=>$offset,'emails'=>$emails],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE); } catch(Exception $e) { echo json_encode(['error'=>$e->getMessage()]); } exit;
|
|
}
|
|
|
|
if ($action === 'urgent_to_respond') {
|
|
$limit = intval($_REQUEST['limit'] ?? 10); $offset = intval($_REQUEST['offset'] ?? 0);
|
|
try { $pdo_c = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'","postgres",""); $st = $pdo_c->prepare("SELECT uid, folder, from_email, from_name, subject, received_at, category, urgency_score, importance_score, eisenhower_quadrant, requires_action, summary, suggested_action FROM admin.email_classifications WHERE eisenhower_quadrant IN ('do_first','schedule') AND requires_action = true ORDER BY CASE eisenhower_quadrant WHEN 'do_first' THEN 1 WHEN 'schedule' THEN 2 ELSE 3 END, urgency_score DESC, importance_score DESC LIMIT ? OFFSET ?"); $st->execute([$limit, $offset]); $emails = $st->fetchAll(PDO::FETCH_ASSOC);
|
|
echo json_encode(['ok'=>true,'count'=>count($emails),'emails'=>$emails],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE); } catch(Exception $e) { echo json_encode(['error'=>$e->getMessage()]); } exit;
|
|
}
|
|
|
|
if ($action === 'batch_draft') {
|
|
$limit = intval($_REQUEST['limit'] ?? 5); $tone = $_REQUEST['tone'] ?? 'professional';
|
|
try { $pdo_c = new PDO("pgsql:host=127.0.0.1;dbname=wevia_db;options='--search_path=admin,public'","postgres",""); $st = $pdo_c->prepare("SELECT uid, folder, from_email, subject, urgency_score, eisenhower_quadrant, summary FROM admin.email_classifications WHERE eisenhower_quadrant IN ('do_first','schedule') AND requires_action = true ORDER BY CASE eisenhower_quadrant WHEN 'do_first' THEN 1 ELSE 2 END, urgency_score DESC LIMIT ?"); $st->execute([$limit]); $emails = $st->fetchAll(PDO::FETCH_ASSOC);
|
|
$results = [];
|
|
foreach ($emails as $em) { $url = "https://weval-consulting.com/products/wevialife-api.php?action=draft_response&uid=".urlencode($em['uid'])."&folder=".urlencode($em['folder'])."&tone=$tone"; $r = @file_get_contents($url, false, stream_context_create(['http'=>['timeout'=>25],'ssl'=>['verify_peer'=>false,'verify_peer_name'=>false]])); $d = $r ? json_decode($r, true) : null; $results[] = ['uid'=>$em['uid'],'subject'=>$em['subject'],'from'=>$em['from_email'],'urgency'=>$em['urgency_score'],'quadrant'=>$em['eisenhower_quadrant'],'draft'=>$d['draft']??'Erreur','provider'=>$d['provider']??'none']; usleep(1000000); }
|
|
echo json_encode(['ok'=>true,'count'=>count($results),'drafts'=>$results],JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE); } catch(Exception $e) { echo json_encode(['error'=>$e->getMessage()]); } exit;
|
|
}
|
|
switch($action) {
|
|
case 'sources':
|
|
$docs = [];
|
|
$f = "$dataDir/documents.json";
|
|
if(file_exists($f)) $docs = json_decode(file_get_contents($f), true) ?: [];
|
|
echo json_encode(['documents' => $docs, 'count' => count($docs)]);
|
|
break;
|
|
|
|
case 'vault_list':
|
|
$items = [];
|
|
$f = "$dataDir/vault.json";
|
|
if(file_exists($f)) $items = json_decode(file_get_contents($f), true) ?: [];
|
|
echo json_encode(['items' => $items, 'count' => count($items)]);
|
|
break;
|
|
|
|
case 'alerts':
|
|
echo json_encode(['alerts' => [], 'count' => 0]);
|
|
break;
|
|
|
|
case 'chat':
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$msg = $input['message'] ?? '';
|
|
if(!$msg) { echo json_encode(['error' => 'no message']); break; }
|
|
// Proxy to WEVIA chatbot
|
|
$ch = curl_init('https://127.0.0.1/api/weval-ia');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Host: weval-consulting.com'],
|
|
CURLOPT_POSTFIELDS => json_encode(['message' => $msg, 'conversationId' => 'wevialife-' . uniqid(), 'mode' => 'widget']),
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 60,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => 0
|
|
]);
|
|
$resp = curl_exec($ch);
|
|
curl_close($ch);
|
|
$data = json_decode($resp, true);
|
|
echo json_encode([
|
|
'reply' => $data['response'] ?? 'Service temporairement indisponible.',
|
|
'response' => $data['response'] ?? 'Service temporairement indisponible.',
|
|
'provider' => 'WEVIA Life',
|
|
'model' => $data['provider'] ?? 'WEVIA',
|
|
'context_docs' => 0,
|
|
'sources_used' => []
|
|
]);
|
|
break;
|
|
|
|
case 'upload':
|
|
if(empty($_FILES['file'])) { echo json_encode(['error' => 'no file']); break; }
|
|
$file = $_FILES['file'];
|
|
$id = uniqid('doc_');
|
|
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
|
|
$dest = "$dataDir/$id.$ext";
|
|
move_uploaded_file($file['tmp_name'], $dest);
|
|
$f = "$dataDir/documents.json";
|
|
$docs = file_exists($f) ? json_decode(file_get_contents($f), true) ?: [] : [];
|
|
$docs[] = ['id' => $id, 'name' => $file['name'], 'size' => $file['size'], 'type' => $ext, 'path' => $dest, 'created' => date('Y-m-d H:i:s')];
|
|
file_put_contents($f, json_encode($docs, JSON_PRETTY_PRINT));
|
|
echo json_encode(['ok' => true, 'id' => $id, 'name' => $file['name']]);
|
|
break;
|
|
|
|
case 'delete_doc':
|
|
$id = $_GET['id'] ?? '';
|
|
$f = "$dataDir/documents.json";
|
|
$docs = file_exists($f) ? json_decode(file_get_contents($f), true) ?: [] : [];
|
|
$docs = array_values(array_filter($docs, function($d) use($id) { return $d['id'] !== $id; }));
|
|
file_put_contents($f, json_encode($docs, JSON_PRETTY_PRINT));
|
|
echo json_encode(['ok' => true]);
|
|
break;
|
|
|
|
case 'vault_add':
|
|
$input = json_decode(file_get_contents('php://input'), true);
|
|
$f = "$dataDir/vault.json";
|
|
$items = file_exists($f) ? json_decode(file_get_contents($f), true) ?: [] : [];
|
|
$item = ['id' => uniqid('v_'), 'title' => $input['title'] ?? '', 'content' => $input['content'] ?? '', 'tags' => $input['tags'] ?? [], 'created' => date('Y-m-d H:i:s')];
|
|
$items[] = $item;
|
|
file_put_contents($f, json_encode($items, JSON_PRETTY_PRINT));
|
|
echo json_encode(['ok' => true, 'item' => $item]);
|
|
break;
|
|
|
|
case 'vault_get':
|
|
$id = $_GET['id'] ?? '';
|
|
$f = "$dataDir/vault.json";
|
|
$items = file_exists($f) ? json_decode(file_get_contents($f), true) ?: [] : [];
|
|
$found = null;
|
|
foreach($items as $i) { if($i['id'] === $id) { $found = $i; break; } }
|
|
echo json_encode($found ?: ['error' => 'not found']);
|
|
break;
|
|
|
|
case 'vault_delete':
|
|
$id = $_GET['id'] ?? '';
|
|
$f = "$dataDir/vault.json";
|
|
$items = file_exists($f) ? json_decode(file_get_contents($f), true) ?: [] : [];
|
|
$items = array_values(array_filter($items, function($i) use($id) { return $i['id'] !== $id; }));
|
|
file_put_contents($f, json_encode($items, JSON_PRETTY_PRINT));
|
|
echo json_encode(['ok' => true]);
|
|
break;
|
|
|
|
default:
|
|
echo json_encode(['status' => 'ok', 'version' => '1.0', 'app' => 'WEVIA Life']);
|
|
} |