['token'=>..., 'expires'=>...] public function __construct($pdo) { $this->pdo = $pdo; } public function getToken($tenantId, $clientId, $clientSecret) { $key = $tenantId; if (isset($this->tokenCache[$key]) && $this->tokenCache[$key]['expires'] > time()) { return $this->tokenCache[$key]['token']; } $ch = curl_init("https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"); curl_setopt_array($ch, [CURLOPT_POST=>true, CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>15, CURLOPT_POSTFIELDS=>http_build_query([ 'grant_type'=>'client_credentials','client_id'=>$clientId, 'client_secret'=>$clientSecret,'scope'=>'https://graph.microsoft.com/.default' ])]); $r = json_decode(curl_exec($ch), true); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!isset($r['access_token'])) return null; $this->tokenCache[$key] = ['token'=>$r['access_token'], 'expires'=>time()+3500]; return $r['access_token']; } public function getRandomTenant() { return $this->pdo->query(" SELECT * FROM admin.graph_tenants WHERE status='active' AND sends_today < daily_limit ORDER BY RANDOM() LIMIT 1 ")->fetch(\PDO::FETCH_ASSOC); } public function getRandomUser($token, $tenantDomain) { $ch = curl_init("https://graph.microsoft.com/v1.0/users?\$top=50&\$select=userPrincipalName,displayName"); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_HTTPHEADER=>["Authorization: Bearer $token"]]); $users = json_decode(curl_exec($ch), true); curl_close($ch); $valid = array_filter($users['value'] ?? [], function($u) { return strpos($u['userPrincipalName'], '#') === false; // skip external users }); return $valid ? $valid[array_rand($valid)]['userPrincipalName'] : null; } /** * Send test email via Graph API * Returns ['success'=>bool, 'sender'=>string, 'error'=>string, 'tenant'=>string] */ public function sendTest($toEmail, $subject, $body, $headers = []) { $tenant = $this->getRandomTenant(); if (!$tenant) return ['success'=>false, 'error'=>'No available tenant', 'sender'=>'', 'tenant'=>'']; $token = $this->getToken($tenant['tenant_id'], $tenant['client_id'], $tenant['client_secret']); if (!$token) return ['success'=>false, 'error'=>'Token failed', 'sender'=>'', 'tenant'=>$tenant['tenant_domain']]; $sender = $this->getRandomUser($token, $tenant['tenant_domain']); if (!$sender) return ['success'=>false, 'error'=>'No valid user', 'sender'=>'', 'tenant'=>$tenant['tenant_domain']]; $msg = [ 'message' => [ 'subject' => $subject, 'body' => ['contentType' => 'HTML', 'content' => $body], 'toRecipients' => [['emailAddress' => ['address' => $toEmail]]], ], 'saveToSentItems' => false ]; // Add custom headers via internetMessageHeaders if (!empty($headers)) { $msg['message']['internetMessageHeaders'] = []; foreach ($headers as $k => $v) { if ($v) $msg['message']['internetMessageHeaders'][] = ['name' => $k, 'value' => $v]; } } $url = "https://graph.microsoft.com/v1.0/users/" . urlencode($sender) . "/sendMail"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST=>true, CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>15, CURLOPT_HTTPHEADER=>["Authorization: Bearer $token","Content-Type: application/json"], CURLOPT_POSTFIELDS=>json_encode($msg) ]); $resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code == 202) { $this->pdo->exec("UPDATE admin.graph_tenants SET sends_today=sends_today+1, last_send_at=NOW() WHERE tenant_domain='{$tenant['tenant_domain']}'"); return ['success'=>true, 'sender'=>$sender, 'error'=>'', 'tenant'=>$tenant['tenant_domain']]; } $err = json_decode($resp, true); $errMsg = $err['error']['message'] ?? $resp; // If mailbox stale, try another user if (strpos($errMsg, 'Stale') !== false || strpos($errMsg, 'NotFound') !== false) { // Retry with different user for ($retry = 0; $retry < 3; $retry++) { $sender2 = $this->getRandomUser($token, $tenant['tenant_domain']); if (!$sender2 || $sender2 == $sender) continue; $ch = curl_init("https://graph.microsoft.com/v1.0/users/" . urlencode($sender2) . "/sendMail"); curl_setopt_array($ch, [CURLOPT_POST=>true, CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>15, CURLOPT_HTTPHEADER=>["Authorization: Bearer $token","Content-Type: application/json"], CURLOPT_POSTFIELDS=>json_encode($msg)]); $resp2 = curl_exec($ch); $code2 = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code2 == 202) { $this->pdo->exec("UPDATE admin.graph_tenants SET sends_today=sends_today+1, last_send_at=NOW() WHERE tenant_domain='{$tenant['tenant_domain']}'"); return ['success'=>true, 'sender'=>$sender2, 'error'=>'', 'tenant'=>$tenant['tenant_domain']]; } } } return ['success'=>false, 'sender'=>$sender, 'error'=>substr($errMsg, 0, 200), 'tenant'=>$tenant['tenant_domain']]; } /** Reset daily counters (call at midnight) */ public function resetDailyCounters() { $this->pdo->exec("UPDATE admin.graph_tenants SET sends_today=0 WHERE status='active'"); } }