Changes for M365 oAuth

- Added OAuth token lifecycle helpers (expiry check, refresh, persistence).
- Updated SMTP XOAUTH2 send path to automatically refresh expired/missing access tokens for Microsoft/Google providers before sending queued mail.
This commit is contained in:
cs2000
2026-02-04 13:25:32 +00:00
committed by GitHub
parent f6845a046f
commit f3f9d0dd71

View File

@@ -43,7 +43,7 @@ class StaticTokenProvider implements OAuthTokenProvider {
* Load settings * Load settings
* ======================================================================= */ * ======================================================================= */
$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1"); $sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
$row = mysqli_fetch_assoc($sql_settings); $row = mysqli_fetch_array($sql_settings);
$config_enable_cron = intval($row['config_enable_cron']); $config_enable_cron = intval($row['config_enable_cron']);
@@ -93,10 +93,121 @@ if (file_exists($lock_file_path)) {
file_put_contents($lock_file_path, "Locked"); file_put_contents($lock_file_path, "Locked");
/** ======================================================================= /** =======================================================================
* Mail sender function (defined inside this cron) * Mail OAuth helpers + sender function
* - Handles standard SMTP and XOAUTH2 for Google/Microsoft
* - Reuses shared OAuth settings
* ======================================================================= */ * ======================================================================= */
function token_is_expired(?string $expires_at): bool {
if (empty($expires_at)) {
return true;
}
$ts = strtotime($expires_at);
if ($ts === false) {
return true;
}
return ($ts - 60) <= time();
}
function http_form_post(string $url, array $fields): array {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&'));
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'ok' => ($raw !== false && $code >= 200 && $code < 300),
'body' => $raw,
'code' => $code,
'err' => $err,
];
}
function persist_mail_oauth_tokens(string $access_token, string $expires_at, ?string $refresh_token = null): void {
global $mysqli;
$access_token_esc = mysqli_real_escape_string($mysqli, $access_token);
$expires_at_esc = mysqli_real_escape_string($mysqli, $expires_at);
$refresh_sql = '';
if (!empty($refresh_token)) {
$refresh_token_esc = mysqli_real_escape_string($mysqli, $refresh_token);
$refresh_sql = ", config_mail_oauth_refresh_token = '{$refresh_token_esc}'";
}
mysqli_query($mysqli, "UPDATE settings SET config_mail_oauth_access_token = '{$access_token_esc}', config_mail_oauth_access_token_expires_at = '{$expires_at_esc}'{$refresh_sql} WHERE company_id = 1");
}
function refresh_mail_oauth_access_token(string $provider, string $oauth_client_id, string $oauth_client_secret, string $oauth_tenant_id, string $oauth_refresh_token): ?array {
if (empty($oauth_client_id) || empty($oauth_client_secret) || empty($oauth_refresh_token)) {
return null;
}
if ($provider === 'google_oauth') {
$response = http_form_post('https://oauth2.googleapis.com/token', [
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'refresh_token' => $oauth_refresh_token,
'grant_type' => 'refresh_token',
]);
} elseif ($provider === 'microsoft_oauth') {
if (empty($oauth_tenant_id)) {
return null;
}
$token_url = "https://login.microsoftonline.com/" . rawurlencode($oauth_tenant_id) . "/oauth2/v2.0/token";
$response = http_form_post($token_url, [
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'refresh_token' => $oauth_refresh_token,
'grant_type' => 'refresh_token',
]);
} else {
return null;
}
if (!$response['ok']) {
return null;
}
$json = json_decode($response['body'], true);
if (!is_array($json) || empty($json['access_token'])) {
return null;
}
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
return [
'access_token' => $json['access_token'],
'expires_at' => $expires_at,
'refresh_token' => $json['refresh_token'] ?? null,
];
}
function resolve_mail_oauth_access_token(string $provider, string $oauth_client_id, string $oauth_client_secret, string $oauth_tenant_id, string $oauth_refresh_token, string $oauth_access_token, string $oauth_access_token_expires_at): ?string {
if (!empty($oauth_access_token) && !token_is_expired($oauth_access_token_expires_at)) {
return $oauth_access_token;
}
$tokens = refresh_mail_oauth_access_token($provider, $oauth_client_id, $oauth_client_secret, $oauth_tenant_id, $oauth_refresh_token);
if (!is_array($tokens) || empty($tokens['access_token']) || empty($tokens['expires_at'])) {
return null;
}
persist_mail_oauth_tokens($tokens['access_token'], $tokens['expires_at'], $tokens['refresh_token'] ?? null);
return $tokens['access_token'];
}
function sendQueueEmail( function sendQueueEmail(
string $provider, string $provider,
string $host, string $host,
@@ -153,27 +264,21 @@ function sendQueueEmail(
$mail->AuthType = 'XOAUTH2'; $mail->AuthType = 'XOAUTH2';
$mail->Username = $username; $mail->Username = $username;
// Pick/refresh access token $access_token = resolve_mail_oauth_access_token(
$accessToken = trim($oauth_access_token); $provider,
$needsRefresh = empty($accessToken); trim($oauth_client_id),
if (!$needsRefresh && !empty($oauth_access_token_expires_at)) { trim($oauth_client_secret),
$expTs = strtotime($oauth_access_token_expires_at); trim($oauth_tenant_id),
if ($expTs && $expTs <= time() + 60) $needsRefresh = true; trim($oauth_refresh_token),
} trim($oauth_access_token),
trim($oauth_access_token_expires_at)
);
if ($needsRefresh) { if (empty($access_token)) {
if ($provider === 'google_oauth' && function_exists('getGoogleAccessToken')) {
$accessToken = getGoogleAccessToken($username);
} elseif ($provider === 'microsoft_oauth' && function_exists('getMicrosoftAccessToken')) {
$accessToken = getMicrosoftAccessToken($username);
}
}
if (empty($accessToken)) {
throw new Exception("Missing OAuth access token for XOAUTH2 SMTP."); throw new Exception("Missing OAuth access token for XOAUTH2 SMTP.");
} }
$mail->setOAuth(new StaticTokenProvider($username, $accessToken)); $mail->setOAuth(new StaticTokenProvider($username, $access_token));
} else { } else {
// Standard SMTP (with or without auth) // Standard SMTP (with or without auth)
$mail->SMTPAuth = !empty($username); $mail->SMTPAuth = !empty($username);
@@ -202,7 +307,7 @@ function sendQueueEmail(
$sql_queue = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()"); $sql_queue = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()");
if (mysqli_num_rows($sql_queue) > 0) { if (mysqli_num_rows($sql_queue) > 0) {
while ($rowq = mysqli_fetch_assoc($sql_queue)) { while ($rowq = mysqli_fetch_array($sql_queue)) {
$email_id = (int)$rowq['email_id']; $email_id = (int)$rowq['email_id'];
$email_from = $rowq['email_from']; $email_from = $rowq['email_from'];
$email_from_name = $rowq['email_from_name']; $email_from_name = $rowq['email_from_name'];
@@ -296,7 +401,7 @@ $sql_failed_queue = mysqli_query(
); );
if (mysqli_num_rows($sql_failed_queue) > 0) { if (mysqli_num_rows($sql_failed_queue) > 0) {
while ($rowf = mysqli_fetch_assoc($sql_failed_queue)) { while ($rowf = mysqli_fetch_array($sql_failed_queue)) {
$email_id = (int)$rowf['email_id']; $email_id = (int)$rowf['email_id'];
$email_from = $rowf['email_from']; $email_from = $rowf['email_from'];
$email_from_name = $rowf['email_from_name']; $email_from_name = $rowf['email_from_name'];