129 lines
6.1 KiB
Plaintext
129 lines
6.1 KiB
Plaintext
<?php
|
|
/**
|
|
* GRAPH API SEND LIBRARY
|
|
* Replaces SMTP basic auth with OAuth2 client_credentials + Graph API sendMail
|
|
* Used by brain-pipeline.php
|
|
*/
|
|
|
|
class GraphSender {
|
|
private $pdo;
|
|
private $tokenCache = []; // tenant_id => ['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'");
|
|
}
|
|
}
|
|
|