diff --git a/cron/mail_queue.php b/cron/mail_queue.php
new file mode 100644
index 00000000..11993ef7
--- /dev/null
+++ b/cron/mail_queue.php
@@ -0,0 +1,339 @@
+email = $email;
+ $this->accessToken = $accessToken;
+ }
+ public function getOauth64(): string {
+ $auth = "user={$this->email}\x01auth=Bearer {$this->accessToken}\x01\x01";
+ return base64_encode($auth);
+ }
+}
+
+/** =======================================================================
+ * Load settings
+ * ======================================================================= */
+$sql_settings = mysqli_query($mysqli, "SELECT * FROM settings WHERE company_id = 1");
+$row = mysqli_fetch_array($sql_settings);
+
+$config_enable_cron = intval($row['config_enable_cron']);
+
+// SMTP baseline
+$config_smtp_host = $row['config_smtp_host'];
+$config_smtp_username = $row['config_smtp_username'];
+$config_smtp_password = $row['config_smtp_password'];
+$config_smtp_port = intval($row['config_smtp_port']);
+$config_smtp_encryption = $row['config_smtp_encryption'];
+
+// SMTP provider + shared OAuth fields
+$config_smtp_provider = $row['config_smtp_provider'] ?? 'standard_smtp'; // 'standard_smtp' | 'google_oauth' | 'microsoft_oauth'
+$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_tenant_id = $row['config_mail_oauth_tenant_id'] ?? '';
+$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_expires_at = $row['config_mail_oauth_access_token_expires_at'] ?? '';
+
+if ($config_enable_cron == 0) {
+ logApp("Cron-Mail-Queue", "error", "Cron Mail Queue unable to run - cron not enabled in admin settings.");
+ exit("Cron: is not enabled -- Quitting..");
+}
+
+/** =======================================================================
+ * Lock file
+ * ======================================================================= */
+$temp_dir = sys_get_temp_dir();
+$lock_file_path = "{$temp_dir}/itflow_mail_queue_{$installation_id}.lock";
+
+if (file_exists($lock_file_path)) {
+ $file_age = time() - filemtime($lock_file_path);
+ if ($file_age > 600) {
+ 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.");
+ } else {
+ 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.");
+ }
+}
+
+file_put_contents($lock_file_path, "Locked");
+
+/** =======================================================================
+ * Mail sender function (defined inside this cron)
+ * - Handles standard SMTP and XOAUTH2 for Google/Microsoft
+ * - Reuses shared OAuth settings
+ * ======================================================================= */
+function sendQueueEmail(
+ string $provider,
+ string $host,
+ int $port,
+ string $encryption,
+ string $username,
+ string $password,
+ string $from_email,
+ string $from_name,
+ string $to_email,
+ string $to_name,
+ string $subject,
+ string $html_body,
+ string $ics_str,
+ 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
+) {
+ // Sensible defaults for OAuth providers if fields were left blank
+ if ($provider === 'google_oauth') {
+ if (!$host) $host = 'smtp.gmail.com';
+ if (!$port) $port = 587;
+ if (!$encryption) $encryption = 'tls';
+ if (!$username) $username = $from_email;
+ } elseif ($provider === 'microsoft_oauth') {
+ if (!$host) $host = 'smtp.office365.com';
+ if (!$port) $port = 587;
+ if (!$encryption) $encryption = 'tls';
+ if (!$username) $username = $from_email;
+ }
+
+ $mail = new PHPMailer(true);
+ $mail->CharSet = "UTF-8";
+ $mail->SMTPDebug = 0;
+ $mail->isSMTP();
+ $mail->Host = $host;
+ $mail->Port = $port;
+
+ $enc = strtolower($encryption);
+ if ($enc === '' || $enc === 'none') {
+ $mail->SMTPAutoTLS = false;
+ $mail->SMTPSecure = false;
+ $mail->SMTPOptions = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
+ } else {
+ $mail->SMTPSecure = $enc; // 'tls' | 'ssl'
+ }
+
+ if ($provider === 'google_oauth' || $provider === 'microsoft_oauth') {
+ // XOAUTH2
+ $mail->SMTPAuth = true;
+ $mail->AuthType = 'XOAUTH2';
+ $mail->Username = $username;
+
+ // Pick/refresh access token
+ $accessToken = trim($oauth_access_token);
+ $needsRefresh = empty($accessToken);
+ if (!$needsRefresh && !empty($oauth_access_token_expires_at)) {
+ $expTs = strtotime($oauth_access_token_expires_at);
+ if ($expTs && $expTs <= time() + 60) $needsRefresh = true;
+ }
+
+ if ($needsRefresh) {
+ 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.");
+ }
+
+ $mail->setOAuth(new StaticTokenProvider($username, $accessToken));
+ } else {
+ // Standard SMTP (with or without auth)
+ $mail->SMTPAuth = !empty($username);
+ $mail->Username = $username ?: '';
+ $mail->Password = $password ?: '';
+ }
+
+ // Recipients & content
+ $mail->setFrom($from_email, $from_name);
+ $mail->addAddress($to_email, $to_name);
+ $mail->isHTML(true);
+ $mail->Subject = $subject;
+ $mail->Body = "
{$html_body}
";
+
+ if (!empty($ics_str)) {
+ $mail->addStringAttachment($ics_str, 'Scheduled_ticket.ics', 'base64', 'text/calendar');
+ }
+
+ $mail->send();
+ return true;
+}
+
+/** =======================================================================
+ * SEND: status = 0 (Queued)
+ * ======================================================================= */
+$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) {
+ while ($rowq = mysqli_fetch_array($sql_queue)) {
+ $email_id = (int)$rowq['email_id'];
+ $email_from = $rowq['email_from'];
+ $email_from_name = $rowq['email_from_name'];
+ $email_recipient = $rowq['email_recipient'];
+ $email_recipient_name = $rowq['email_recipient_name'];
+ $email_subject = $rowq['email_subject'];
+ $email_content = $rowq['email_content'];
+ $email_ics_str = $rowq['email_cal_str'];
+
+ if (!filter_var($email_from, FILTER_VALIDATE_EMAIL)) {
+ $email_from_logging = sanitizeInput($rowq['email_from']);
+ mysqli_query($mysqli, "UPDATE email_queue SET email_status = 2, email_attempts = 99 WHERE email_id = $email_id");
+ logApp("Cron-Mail-Queue", "Error", "Failed to send email #$email_id due to invalid sender address: $email_from_logging - check configuration in settings.");
+ appNotify("Mail", "Failed to send email #$email_id due to invalid sender address");
+ continue;
+ }
+
+ 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 = 99 WHERE email_id = $email_id");
+ $email_subject_logging = sanitizeInput($rowq['email_subject']);
+ logApp("Cron-Mail-Queue", "Error", "Failed to send email: $email_id due to invalid recipient address. Email subject was: $email_subject_logging");
+ 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 = 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);
diff --git a/functions.php b/functions.php
index 272bd397..70c9117a 100644
--- a/functions.php
+++ b/functions.php
@@ -7,13 +7,10 @@ DEFINE("WORDING_ROLECHECK_FAILED", "You are not permitted to do that!");
require_once "plugins/PHPMailer/src/Exception.php";
require_once "plugins/PHPMailer/src/PHPMailer.php";
require_once "plugins/PHPMailer/src/SMTP.php";
-require_once "plugins/PHPMailer/src/OAuthTokenProvider.php";
-require_once "plugins/PHPMailer/src/OAuth.php";
// Initiate PHPMailer
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
-use PHPMailer\PHPMailer\OAuthTokenProvider;
// Function to generate both crypto & URL safe random strings
function randomString($length = 16) {
@@ -691,23 +688,6 @@ function validateAccountantRole() {
}
}
-/**
- * Minimal token provider for PHPMailer XOAUTH2 without external deps.
- */
-class StaticTokenProvider implements OAuthTokenProvider {
- private $email;
- private $accessToken;
- public function __construct(string $email, string $accessToken) {
- $this->email = $email;
- $this->accessToken = $accessToken;
- }
- public function getOauth64(): string {
- // XOAUTH2 SASL string: "user=\x01auth=Bearer \x01\x01"
- $authString = "user={$this->email}\x01auth=Bearer {$this->accessToken}\x01\x01";
- return base64_encode($authString);
- }
-}
-
// Send a single email to a single recipient
function sendSingleEmail($config_smtp_host, $config_smtp_username, $config_smtp_password, $config_smtp_encryption, $config_smtp_port, $from_email, $from_name, $to_email, $to_name, $subject, $body, $ics_str)
{