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

@@ -1,357 +1,462 @@
<?php <?php
// Set working directory to the directory this cron script lives at. // Set working directory to the directory this cron script lives at.
chdir(dirname(__FILE__)); chdir(dirname(__FILE__));
// Ensure we're running from command line // Ensure we're running from command line
if (php_sapi_name() !== 'cli') { if (php_sapi_name() !== 'cli') {
die("This script must be run from the command line.\n"); die("This script must be run from the command line.\n");
} }
require_once "../config.php"; require_once "../config.php";
require_once "../includes/inc_set_timezone.php"; require_once "../includes/inc_set_timezone.php";
require_once "../functions.php"; require_once "../functions.php";
require_once "../plugins/vendor/autoload.php"; require_once "../plugins/vendor/autoload.php";
// PHP Mailer Libs // PHP Mailer Libs
require_once "../plugins/PHPMailer/src/Exception.php"; require_once "../plugins/PHPMailer/src/Exception.php";
require_once "../plugins/PHPMailer/src/PHPMailer.php"; require_once "../plugins/PHPMailer/src/PHPMailer.php";
require_once "../plugins/PHPMailer/src/SMTP.php"; require_once "../plugins/PHPMailer/src/SMTP.php";
require_once "../plugins/PHPMailer/src/OAuthTokenProvider.php"; require_once "../plugins/PHPMailer/src/OAuthTokenProvider.php";
require_once "../plugins/PHPMailer/src/OAuth.php"; require_once "../plugins/PHPMailer/src/OAuth.php";
use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception; use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\OAuthTokenProvider; use PHPMailer\PHPMailer\OAuthTokenProvider;
/** ======================================================================= /** =======================================================================
* XOAUTH2 Token Provider for PHPMailer (simple “static” provider) * XOAUTH2 Token Provider for PHPMailer (simple “static” provider)
* ======================================================================= */ * ======================================================================= */
class StaticTokenProvider implements OAuthTokenProvider { class StaticTokenProvider implements OAuthTokenProvider {
private string $email; private string $email;
private string $accessToken; private string $accessToken;
public function __construct(string $email, string $accessToken) { public function __construct(string $email, string $accessToken) {
$this->email = $email; $this->email = $email;
$this->accessToken = $accessToken; $this->accessToken = $accessToken;
} }
public function getOauth64(): string { public function getOauth64(): string {
$auth = "user={$this->email}\x01auth=Bearer {$this->accessToken}\x01\x01"; $auth = "user={$this->email}\x01auth=Bearer {$this->accessToken}\x01\x01";
return base64_encode($auth); return base64_encode($auth);
} }
} }
/** ======================================================================= /** =======================================================================
* 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']);
// SMTP baseline // SMTP baseline
$config_smtp_host = $row['config_smtp_host']; $config_smtp_host = $row['config_smtp_host'];
$config_smtp_username = $row['config_smtp_username']; $config_smtp_username = $row['config_smtp_username'];
$config_smtp_password = $row['config_smtp_password']; $config_smtp_password = $row['config_smtp_password'];
$config_smtp_port = intval($row['config_smtp_port']); $config_smtp_port = intval($row['config_smtp_port']);
$config_smtp_encryption = $row['config_smtp_encryption']; $config_smtp_encryption = $row['config_smtp_encryption'];
// SMTP provider + shared OAuth fields // SMTP provider + shared OAuth fields
$config_smtp_provider = $row['config_smtp_provider']; // 'standard_smtp' | 'google_oauth' | 'microsoft_oauth' $config_smtp_provider = $row['config_smtp_provider']; // 'standard_smtp' | 'google_oauth' | 'microsoft_oauth'
$config_mail_oauth_client_id = $row['config_mail_oauth_client_id'] ?? ''; $config_mail_oauth_client_id = $row['config_mail_oauth_client_id'] ?? '';
$config_mail_oauth_client_secret = $row['config_mail_oauth_client_secret'] ?? ''; $config_mail_oauth_client_secret = $row['config_mail_oauth_client_secret'] ?? '';
$config_mail_oauth_tenant_id = $row['config_mail_oauth_tenant_id'] ?? ''; $config_mail_oauth_tenant_id = $row['config_mail_oauth_tenant_id'] ?? '';
$config_mail_oauth_refresh_token = $row['config_mail_oauth_refresh_token'] ?? ''; $config_mail_oauth_refresh_token = $row['config_mail_oauth_refresh_token'] ?? '';
$config_mail_oauth_access_token = $row['config_mail_oauth_access_token'] ?? ''; $config_mail_oauth_access_token = $row['config_mail_oauth_access_token'] ?? '';
$config_mail_oauth_access_token_expires_at = $row['config_mail_oauth_access_token_expires_at'] ?? ''; $config_mail_oauth_access_token_expires_at = $row['config_mail_oauth_access_token_expires_at'] ?? '';
if ($config_enable_cron == 0) { if ($config_enable_cron == 0) {
logApp("Cron-Mail-Queue", "error", "Cron Mail Queue unable to run - cron not enabled in admin settings."); logApp("Cron-Mail-Queue", "error", "Cron Mail Queue unable to run - cron not enabled in admin settings.");
exit("Cron: is not enabled -- Quitting.."); exit("Cron: is not enabled -- Quitting..");
} }
if (empty($config_smtp_provider)) { if (empty($config_smtp_provider)) {
logApp("Cron-Mail-Queue", "info", "SMTP sending skipped: provider not configured."); logApp("Cron-Mail-Queue", "info", "SMTP sending skipped: provider not configured.");
exit(0); exit(0);
} }
/** ======================================================================= /** =======================================================================
* Lock file * Lock file
* ======================================================================= */ * ======================================================================= */
$temp_dir = sys_get_temp_dir(); $temp_dir = sys_get_temp_dir();
$lock_file_path = "{$temp_dir}/itflow_mail_queue_{$installation_id}.lock"; $lock_file_path = "{$temp_dir}/itflow_mail_queue_{$installation_id}.lock";
if (file_exists($lock_file_path)) { if (file_exists($lock_file_path)) {
$file_age = time() - filemtime($lock_file_path); $file_age = time() - filemtime($lock_file_path);
if ($file_age > 600) { if ($file_age > 600) {
unlink($lock_file_path); unlink($lock_file_path);
logApp("Cron-Mail-Queue", "warning", "Cron Mail Queue detected a lock file was present but was over 10 minutes old so it removed it."); logApp("Cron-Mail-Queue", "warning", "Cron Mail Queue detected a lock file was present but was over 10 minutes old so it removed it.");
} else { } else {
logApp("Cron-Mail-Queue", "info", "Cron Mail Queue attempted to execute but was already executing so instead it terminated."); logApp("Cron-Mail-Queue", "info", "Cron Mail Queue attempted to execute but was already executing so instead it terminated.");
exit("Script is already running. Exiting."); exit("Script is already running. Exiting.");
} }
} }
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)) {
function sendQueueEmail( return true;
string $provider, }
string $host,
int $port, $ts = strtotime($expires_at);
string $encryption,
string $username, if ($ts === false) {
string $password, return true;
string $from_email, }
string $from_name,
string $to_email, return ($ts - 60) <= time();
string $to_name, }
string $subject,
string $html_body, function http_form_post(string $url, array $fields): array {
string $ics_str, $ch = curl_init($url);
string $oauth_client_id, curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
string $oauth_client_secret, curl_setopt($ch, CURLOPT_POST, true);
string $oauth_tenant_id, curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&'));
string $oauth_refresh_token, curl_setopt($ch, CURLOPT_TIMEOUT, 20);
string $oauth_access_token,
string $oauth_access_token_expires_at $raw = curl_exec($ch);
) { $err = curl_error($ch);
// Sensible defaults for OAuth providers if fields were left blank $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($provider === 'google_oauth') {
if (!$host) $host = 'smtp.gmail.com'; curl_close($ch);
if (!$port) $port = 587;
if (!$encryption) $encryption = 'tls'; return [
if (!$username) $username = $from_email; 'ok' => ($raw !== false && $code >= 200 && $code < 300),
} elseif ($provider === 'microsoft_oauth') { 'body' => $raw,
if (!$host) $host = 'smtp.office365.com'; 'code' => $code,
if (!$port) $port = 587; 'err' => $err,
if (!$encryption) $encryption = 'tls'; ];
if (!$username) $username = $from_email; }
}
function persist_mail_oauth_tokens(string $access_token, string $expires_at, ?string $refresh_token = null): void {
$mail = new PHPMailer(true); global $mysqli;
$mail->CharSet = "UTF-8";
$mail->SMTPDebug = 0; $access_token_esc = mysqli_real_escape_string($mysqli, $access_token);
$mail->isSMTP(); $expires_at_esc = mysqli_real_escape_string($mysqli, $expires_at);
$mail->Host = $host;
$mail->Port = $port; $refresh_sql = '';
if (!empty($refresh_token)) {
$enc = strtolower($encryption); $refresh_token_esc = mysqli_real_escape_string($mysqli, $refresh_token);
if ($enc === '' || $enc === 'none') { $refresh_sql = ", config_mail_oauth_refresh_token = '{$refresh_token_esc}'";
$mail->SMTPAutoTLS = false; }
$mail->SMTPSecure = false;
$mail->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]]; 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");
} else { }
$mail->SMTPSecure = $enc; // 'tls' | 'ssl'
} 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)) {
if ($provider === 'google_oauth' || $provider === 'microsoft_oauth') { return null;
// XOAUTH2 }
$mail->SMTPAuth = true;
$mail->AuthType = 'XOAUTH2'; if ($provider === 'google_oauth') {
$mail->Username = $username; $response = http_form_post('https://oauth2.googleapis.com/token', [
'client_id' => $oauth_client_id,
// Pick/refresh access token 'client_secret' => $oauth_client_secret,
$accessToken = trim($oauth_access_token); 'refresh_token' => $oauth_refresh_token,
$needsRefresh = empty($accessToken); 'grant_type' => 'refresh_token',
if (!$needsRefresh && !empty($oauth_access_token_expires_at)) { ]);
$expTs = strtotime($oauth_access_token_expires_at); } elseif ($provider === 'microsoft_oauth') {
if ($expTs && $expTs <= time() + 60) $needsRefresh = true; if (empty($oauth_tenant_id)) {
} return null;
}
if ($needsRefresh) {
if ($provider === 'google_oauth' && function_exists('getGoogleAccessToken')) { $token_url = "https://login.microsoftonline.com/" . rawurlencode($oauth_tenant_id) . "/oauth2/v2.0/token";
$accessToken = getGoogleAccessToken($username); $response = http_form_post($token_url, [
} elseif ($provider === 'microsoft_oauth' && function_exists('getMicrosoftAccessToken')) { 'client_id' => $oauth_client_id,
$accessToken = getMicrosoftAccessToken($username); 'client_secret' => $oauth_client_secret,
} 'refresh_token' => $oauth_refresh_token,
} 'grant_type' => 'refresh_token',
]);
if (empty($accessToken)) { } else {
throw new Exception("Missing OAuth access token for XOAUTH2 SMTP."); return null;
} }
$mail->setOAuth(new StaticTokenProvider($username, $accessToken)); if (!$response['ok']) {
} else { return null;
// Standard SMTP (with or without auth) }
$mail->SMTPAuth = !empty($username);
$mail->Username = $username ?: ''; $json = json_decode($response['body'], true);
$mail->Password = $password ?: '';
} if (!is_array($json) || empty($json['access_token'])) {
return null;
// Recipients & content }
$mail->setFrom($from_email, $from_name);
$mail->addAddress($to_email, $to_name); $expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
$mail->isHTML(true);
$mail->Subject = $subject; return [
$mail->Body = $html_body; 'access_token' => $json['access_token'],
'expires_at' => $expires_at,
if (!empty($ics_str)) { 'refresh_token' => $json['refresh_token'] ?? null,
$mail->addStringAttachment($ics_str, 'Scheduled_ticket.ics', 'base64', 'text/calendar'); ];
} }
$mail->send(); 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 {
return true; if (!empty($oauth_access_token) && !token_is_expired($oauth_access_token_expires_at)) {
} return $oauth_access_token;
}
/** =======================================================================
* SEND: status = 0 (Queued) $tokens = refresh_mail_oauth_access_token($provider, $oauth_client_id, $oauth_client_secret, $oauth_tenant_id, $oauth_refresh_token);
* ======================================================================= */
$sql_queue = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()"); if (!is_array($tokens) || empty($tokens['access_token']) || empty($tokens['expires_at'])) {
return null;
if (mysqli_num_rows($sql_queue) > 0) { }
while ($rowq = mysqli_fetch_assoc($sql_queue)) {
$email_id = (int)$rowq['email_id']; persist_mail_oauth_tokens($tokens['access_token'], $tokens['expires_at'], $tokens['refresh_token'] ?? null);
$email_from = $rowq['email_from'];
$email_from_name = $rowq['email_from_name']; return $tokens['access_token'];
$email_recipient = $rowq['email_recipient']; }
$email_recipient_name = $rowq['email_recipient_name'];
$email_subject = $rowq['email_subject']; function sendQueueEmail(
$email_content = $rowq['email_content']; string $provider,
$email_ics_str = $rowq['email_cal_str']; string $host,
int $port,
// Check sender string $encryption,
if (!filter_var($email_from, FILTER_VALIDATE_EMAIL)) { string $username,
$email_from_logging = sanitizeInput($rowq['email_from']); string $password,
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id"); string $from_email,
logApp("Cron-Mail-Queue", "Error", "Failed to send email #$email_id due to invalid sender address: $email_from_logging - check configuration in settings."); string $from_name,
appNotify("Mail", "Failed to send email #$email_id due to invalid sender address"); string $to_email,
continue; string $to_name,
} string $subject,
string $html_body,
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 1 WHERE email_id = $email_id"); string $ics_str,
string $oauth_client_id,
// Basic recipient syntax check string $oauth_client_secret,
if (!filter_var($email_recipient, FILTER_VALIDATE_EMAIL)) { string $oauth_tenant_id,
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id"); string $oauth_refresh_token,
$email_to_logging = sanitizeInput($email_recipient); string $oauth_access_token,
$email_subject_logging = sanitizeInput($rowq['email_subject']); string $oauth_access_token_expires_at
logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_to_logging due to invalid recipient address. Email subject was: $email_subject_logging"); ) {
appNotify("Mail", "Failed to send email #$email_id to $email_to_logging due to invalid recipient address: Email subject was: $email_subject_logging"); // Sensible defaults for OAuth providers if fields were left blank
continue; if ($provider === 'google_oauth') {
} if (!$host) $host = 'smtp.gmail.com';
if (!$port) $port = 587;
// More intelligent recipient MX check (if not disabled with --no-mx-validation) if (!$encryption) $encryption = 'tls';
$domain = sanitizeInput(substr($email_recipient, strpos($email_recipient, '@') + 1)); if (!$username) $username = $from_email;
if (!in_array('--no-mx-validation', $argv) && !checkdnsrr($domain, 'MX')) { } elseif ($provider === 'microsoft_oauth') {
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id"); if (!$host) $host = 'smtp.office365.com';
$email_to_logging = sanitizeInput($email_recipient); if (!$port) $port = 587;
$email_subject_logging = sanitizeInput($rowq['email_subject']); if (!$encryption) $encryption = 'tls';
logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_to_logging due to invalid recipient domain (no MX). Email subject was: $email_subject_logging"); if (!$username) $username = $from_email;
appNotify("Mail", "Failed to send email #$email_id to $email_to_logging due to invalid recipient domain (no MX): Email subject was: $email_subject_logging"); }
continue;
} $mail = new PHPMailer(true);
$mail->CharSet = "UTF-8";
try { $mail->SMTPDebug = 0;
sendQueueEmail( $mail->isSMTP();
($config_smtp_provider ?: 'standard_smtp'), $mail->Host = $host;
$config_smtp_host, $mail->Port = $port;
(int)$config_smtp_port,
(string)$config_smtp_encryption, $enc = strtolower($encryption);
(string)$config_smtp_username, if ($enc === '' || $enc === 'none') {
(string)$config_smtp_password, $mail->SMTPAutoTLS = false;
(string)$email_from, $mail->SMTPSecure = false;
(string)$email_from_name, $mail->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
(string)$email_recipient, } else {
(string)$email_recipient_name, $mail->SMTPSecure = $enc; // 'tls' | 'ssl'
(string)$email_subject, }
(string)$email_content,
(string)$email_ics_str, if ($provider === 'google_oauth' || $provider === 'microsoft_oauth') {
(string)$config_mail_oauth_client_id, // XOAUTH2
(string)$config_mail_oauth_client_secret, $mail->SMTPAuth = true;
(string)$config_mail_oauth_tenant_id, $mail->AuthType = 'XOAUTH2';
(string)$config_mail_oauth_refresh_token, $mail->Username = $username;
(string)$config_mail_oauth_access_token,
(string)$config_mail_oauth_access_token_expires_at $access_token = resolve_mail_oauth_access_token(
); $provider,
trim($oauth_client_id),
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 3, email_sent_at = NOW(), email_attempts = 1 WHERE email_id = $email_id"); trim($oauth_client_secret),
trim($oauth_tenant_id),
} catch (Exception $e) { trim($oauth_refresh_token),
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_failed_at = NOW(), email_attempts = 1 WHERE email_id = $email_id"); trim($oauth_access_token),
trim($oauth_access_token_expires_at)
$email_recipient_logging = sanitizeInput($rowq['email_recipient']); );
$email_subject_logging = sanitizeInput($rowq['email_subject']);
$err = substr("Mailer Error: " . $e->getMessage(), 0, 100) . "..."; if (empty($access_token)) {
throw new Exception("Missing OAuth access token for XOAUTH2 SMTP.");
appNotify("Cron-Mail-Queue", "Failed to send email #$email_id to $email_recipient_logging"); }
logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_recipient_logging regarding $email_subject_logging. $err");
} $mail->setOAuth(new StaticTokenProvider($username, $access_token));
} } else {
} // Standard SMTP (with or without auth)
$mail->SMTPAuth = !empty($username);
/** ======================================================================= $mail->Username = $username ?: '';
* RETRIES: status = 2 (Failed), attempts < 4, wait 30 min $mail->Password = $password ?: '';
* NOTE: Backoff is `email_failed_at <= NOW() - INTERVAL 30 MINUTE` }
* =======================================================================
*/ // Recipients & content
$sql_failed_queue = mysqli_query( $mail->setFrom($from_email, $from_name);
$mysqli, $mail->addAddress($to_email, $to_name);
"SELECT * FROM email_queue $mail->isHTML(true);
WHERE email_status = 2 $mail->Subject = $subject;
AND email_attempts < 4 $mail->Body = $html_body;
AND email_failed_at <= NOW() - INTERVAL 30 MINUTE"
); if (!empty($ics_str)) {
$mail->addStringAttachment($ics_str, 'Scheduled_ticket.ics', 'base64', 'text/calendar');
if (mysqli_num_rows($sql_failed_queue) > 0) { }
while ($rowf = mysqli_fetch_assoc($sql_failed_queue)) {
$email_id = (int)$rowf['email_id']; $mail->send();
$email_from = $rowf['email_from']; return true;
$email_from_name = $rowf['email_from_name']; }
$email_recipient = $rowf['email_recipient'];
$email_recipient_name = $rowf['email_recipient_name']; /** =======================================================================
$email_subject = $rowf['email_subject']; * SEND: status = 0 (Queued)
$email_content = $rowf['email_content']; * ======================================================================= */
$email_ics_str = $rowf['email_cal_str']; $sql_queue = mysqli_query($mysqli, "SELECT * FROM email_queue WHERE email_status = 0 AND email_queued_at <= NOW()");
$email_attempts = (int)$rowf['email_attempts'] + 1;
if (mysqli_num_rows($sql_queue) > 0) {
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 1 WHERE email_id = $email_id"); while ($rowq = mysqli_fetch_array($sql_queue)) {
$email_id = (int)$rowq['email_id'];
if (!filter_var($email_recipient, FILTER_VALIDATE_EMAIL)) { $email_from = $rowq['email_from'];
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = $email_attempts WHERE email_id = $email_id"); $email_from_name = $rowq['email_from_name'];
continue; $email_recipient = $rowq['email_recipient'];
} $email_recipient_name = $rowq['email_recipient_name'];
$email_subject = $rowq['email_subject'];
try { $email_content = $rowq['email_content'];
sendQueueEmail( $email_ics_str = $rowq['email_cal_str'];
($config_smtp_provider ?: 'standard_smtp'),
$config_smtp_host, // Check sender
(int)$config_smtp_port, if (!filter_var($email_from, FILTER_VALIDATE_EMAIL)) {
(string)$config_smtp_encryption, $email_from_logging = sanitizeInput($rowq['email_from']);
(string)$config_smtp_username, mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id");
(string)$config_smtp_password, logApp("Cron-Mail-Queue", "Error", "Failed to send email #$email_id due to invalid sender address: $email_from_logging - check configuration in settings.");
(string)$email_from, appNotify("Mail", "Failed to send email #$email_id due to invalid sender address");
(string)$email_from_name, continue;
(string)$email_recipient, }
(string)$email_recipient_name,
(string)$email_subject, mysqli_query($mysqli, "UPDATE email_queue SET email_status = 1 WHERE email_id = $email_id");
(string)$email_content,
(string)$email_ics_str, // Basic recipient syntax check
(string)$config_mail_oauth_client_id, if (!filter_var($email_recipient, FILTER_VALIDATE_EMAIL)) {
(string)$config_mail_oauth_client_secret, mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id");
(string)$config_mail_oauth_tenant_id, $email_to_logging = sanitizeInput($email_recipient);
(string)$config_mail_oauth_refresh_token, $email_subject_logging = sanitizeInput($rowq['email_subject']);
(string)$config_mail_oauth_access_token, logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_to_logging due to invalid recipient address. Email subject was: $email_subject_logging");
(string)$config_mail_oauth_access_token_expires_at appNotify("Mail", "Failed to send email #$email_id to $email_to_logging due to invalid recipient address: Email subject was: $email_subject_logging");
); continue;
}
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 3, email_sent_at = NOW(), email_attempts = $email_attempts WHERE email_id = $email_id");
// More intelligent recipient MX check (if not disabled with --no-mx-validation)
} catch (Exception $e) { $domain = sanitizeInput(substr($email_recipient, strpos($email_recipient, '@') + 1));
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_failed_at = NOW(), email_attempts = $email_attempts WHERE email_id = $email_id"); if (!in_array('--no-mx-validation', $argv) && !checkdnsrr($domain, 'MX')) {
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id");
$email_recipient_logging = sanitizeInput($rowf['email_recipient']); $email_to_logging = sanitizeInput($email_recipient);
$email_subject_logging = sanitizeInput($rowf['email_subject']); $email_subject_logging = sanitizeInput($rowq['email_subject']);
$err = substr("Mailer Error: " . $e->getMessage(), 0, 100) . "..."; logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_to_logging due to invalid recipient domain (no MX). Email subject was: $email_subject_logging");
appNotify("Mail", "Failed to send email #$email_id to $email_to_logging due to invalid recipient domain (no MX): Email subject was: $email_subject_logging");
logApp("Cron-Mail-Queue", "Error", "Failed to re-send email #$email_id to $email_recipient_logging regarding $email_subject_logging. $err"); continue;
} }
}
} try {
sendQueueEmail(
/** ======================================================================= ($config_smtp_provider ?: 'standard_smtp'),
* Unlock $config_smtp_host,
* ======================================================================= */ (int)$config_smtp_port,
unlink($lock_file_path); (string)$config_smtp_encryption,
(string)$config_smtp_username,
(string)$config_smtp_password,
(string)$email_from,
(string)$email_from_name,
(string)$email_recipient,
(string)$email_recipient_name,
(string)$email_subject,
(string)$email_content,
(string)$email_ics_str,
(string)$config_mail_oauth_client_id,
(string)$config_mail_oauth_client_secret,
(string)$config_mail_oauth_tenant_id,
(string)$config_mail_oauth_refresh_token,
(string)$config_mail_oauth_access_token,
(string)$config_mail_oauth_access_token_expires_at
);
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 3, email_sent_at = NOW(), email_attempts = 1 WHERE email_id = $email_id");
} catch (Exception $e) {
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_failed_at = NOW(), email_attempts = 1 WHERE email_id = $email_id");
$email_recipient_logging = sanitizeInput($rowq['email_recipient']);
$email_subject_logging = sanitizeInput($rowq['email_subject']);
$err = substr("Mailer Error: " . $e->getMessage(), 0, 100) . "...";
appNotify("Cron-Mail-Queue", "Failed to send email #$email_id to $email_recipient_logging");
logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id to $email_recipient_logging regarding $email_subject_logging. $err");
}
}
}
/** =======================================================================
* RETRIES: status = 2 (Failed), attempts < 4, wait 30 min
* NOTE: Backoff is `email_failed_at <= NOW() - INTERVAL 30 MINUTE`
* =======================================================================
*/
$sql_failed_queue = mysqli_query(
$mysqli,
"SELECT * FROM email_queue
WHERE email_status = 2
AND email_attempts < 4
AND email_failed_at <= NOW() - INTERVAL 30 MINUTE"
);
if (mysqli_num_rows($sql_failed_queue) > 0) {
while ($rowf = mysqli_fetch_array($sql_failed_queue)) {
$email_id = (int)$rowf['email_id'];
$email_from = $rowf['email_from'];
$email_from_name = $rowf['email_from_name'];
$email_recipient = $rowf['email_recipient'];
$email_recipient_name = $rowf['email_recipient_name'];
$email_subject = $rowf['email_subject'];
$email_content = $rowf['email_content'];
$email_ics_str = $rowf['email_cal_str'];
$email_attempts = (int)$rowf['email_attempts'] + 1;
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 1 WHERE email_id = $email_id");
if (!filter_var($email_recipient, FILTER_VALIDATE_EMAIL)) {
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = $email_attempts WHERE email_id = $email_id");
continue;
}
try {
sendQueueEmail(
($config_smtp_provider ?: 'standard_smtp'),
$config_smtp_host,
(int)$config_smtp_port,
(string)$config_smtp_encryption,
(string)$config_smtp_username,
(string)$config_smtp_password,
(string)$email_from,
(string)$email_from_name,
(string)$email_recipient,
(string)$email_recipient_name,
(string)$email_subject,
(string)$email_content,
(string)$email_ics_str,
(string)$config_mail_oauth_client_id,
(string)$config_mail_oauth_client_secret,
(string)$config_mail_oauth_tenant_id,
(string)$config_mail_oauth_refresh_token,
(string)$config_mail_oauth_access_token,
(string)$config_mail_oauth_access_token_expires_at
);
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 3, email_sent_at = NOW(), email_attempts = $email_attempts WHERE email_id = $email_id");
} catch (Exception $e) {
mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_failed_at = NOW(), email_attempts = $email_attempts WHERE email_id = $email_id");
$email_recipient_logging = sanitizeInput($rowf['email_recipient']);
$email_subject_logging = sanitizeInput($rowf['email_subject']);
$err = substr("Mailer Error: " . $e->getMessage(), 0, 100) . "...";
logApp("Cron-Mail-Queue", "Error", "Failed to re-send email #$email_id to $email_recipient_logging regarding $email_subject_logging. $err");
}
}
}
/** =======================================================================
* Unlock
* ======================================================================= */
unlink($lock_file_path);