diff --git a/admin/database_updates.php b/admin/database_updates.php
index 55458c71..dd6442a3 100644
--- a/admin/database_updates.php
+++ b/admin/database_updates.php
@@ -3968,11 +3968,26 @@ if (LATEST_DATABASE_VERSION > CURRENT_DATABASE_VERSION) {
mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.2'");
}
+
+ if (CURRENT_DATABASE_VERSION == '2.3.2') {
+
+ mysqli_query($mysqli, "ALTER TABLE settings
+ ADD `config_imap_provider` ENUM('standard_imap','google_oauth','microsoft_oauth') NULL DEFAULT NULL AFTER `config_mail_from_name`,
+ ADD `config_mail_oauth_client_id` VARCHAR(255) NULL AFTER `config_imap_provider`,
+ ADD `config_mail_oauth_client_secret` VARCHAR(255) NULL AFTER `config_mail_oauth_client_id`,
+ ADD `config_mail_oauth_tenant_id` VARCHAR(255) NULL AFTER `config_mail_oauth_client_secret`,
+ ADD `config_mail_oauth_refresh_token` TEXT NULL AFTER `config_mail_oauth_tenant_id`,
+ ADD `config_mail_oauth_access_token` TEXT NULL AFTER `config_mail_oauth_refresh_token`,
+ ADD `config_mail_oauth_access_token_expires_at` DATETIME NULL AFTER `config_mail_oauth_access_token`
+ ");
+
+ mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.3'");
+ }
- // if (CURRENT_DATABASE_VERSION == '2.3.2') {
+ // if (CURRENT_DATABASE_VERSION == '2.3.3') {
// // Insert queries here required to update to DB version 2.3.3
// // Then, update the database to the next sequential version
- // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.3'");
+ // mysqli_query($mysqli, "UPDATE `settings` SET `config_current_database_version` = '2.3.4'");
// }
} else {
diff --git a/admin/post/settings_mail.php b/admin/post/settings_mail.php
index dae79f25..7954aafb 100644
--- a/admin/post/settings_mail.php
+++ b/admin/post/settings_mail.php
@@ -26,20 +26,91 @@ if (isset($_POST['edit_mail_imap_settings'])) {
validateCSRFToken($_POST['csrf_token']);
- $config_imap_host = sanitizeInput($_POST['config_imap_host']);
- $config_imap_username = sanitizeInput($_POST['config_imap_username']);
- $config_imap_password = sanitizeInput($_POST['config_imap_password']);
- $config_imap_port = intval($_POST['config_imap_port']);
- $config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']);
+ // Provider ('' -> NULL allowed)
+ $config_imap_provider = sanitizeInput($_POST['config_imap_provider']);
+ $allowed_providers = ['standard_imap','google_oauth','microsoft_oauth'];
+ if ($config_imap_provider !== '' && !in_array($config_imap_provider, $allowed_providers, true)) {
+ $config_imap_provider = 'standard_imap'; // fallback
+ }
- mysqli_query($mysqli,"UPDATE settings SET config_imap_host = '$config_imap_host', config_imap_port = $config_imap_port, config_imap_encryption = '$config_imap_encryption', config_imap_username = '$config_imap_username', config_imap_password = '$config_imap_password' WHERE company_id = 1");
+ // Standard IMAP fields (kept for all providers; OAuth still needs these endpoints)
+ $config_imap_host = sanitizeInput($_POST['config_imap_host']);
+ $config_imap_port = (int) sanitizeInput($_POST['config_imap_port']);
+ $config_imap_encryption = sanitizeInput($_POST['config_imap_encryption']); // '', 'tls', 'ssl'
+ $config_imap_username = sanitizeInput($_POST['config_imap_username']);
+ $config_imap_password = sanitizeInput($_POST['config_imap_password']); // ignored if OAuth selected
- logAction("Settings", "Edit", "$session_name edited IMAP mail settings");
+ // Shared OAuth fields (may or may not be present in your form yet)
+ $config_mail_oauth_client_id = sanitizeInput($_POST['config_mail_oauth_client_id']);
+ $config_mail_oauth_client_secret = sanitizeInput($_POST['config_mail_oauth_client_secret']);
+ $config_mail_oauth_tenant_id = sanitizeInput($_POST['config_mail_oauth_tenant_id']); // M365 only; harmless to keep when Google
+ $config_mail_oauth_refresh_token = sanitizeInput($_POST['config_mail_oauth_refresh_token']);
+ $config_mail_oauth_access_token = sanitizeInput($_POST['config_mail_oauth_access_token']); // optional manual paste
+ $config_mail_oauth_access_token_expires_at = sanitizeInput($_POST['config_mail_oauth_access_token_expires_at']); // 'YYYY-mm-dd HH:ii:ss' optional
- flash_alert("IMAP Mail Settings updated");
+ // If provider is not OAuth, purge OAuth values on save
+ $is_oauth = ($config_imap_provider === 'google_oauth' || $config_imap_provider === 'microsoft_oauth');
+
+ // Detect refresh token change to invalidate access token cache
+ // (Relies on $config_mail_oauth_refresh_token loaded earlier with settings)
+ $refresh_changed = false;
+ if ($is_oauth) {
+ $prev_refresh = isset($config_mail_oauth_refresh_token_current) ? $config_mail_oauth_refresh_token_current : ($config_mail_oauth_refresh_token ?? '');
+ // If you already load settings into $config_mail_oauth_refresh_token, use that:
+ if (isset($config_mail_oauth_refresh_token)) {
+ $prev_refresh = $config_mail_oauth_refresh_token;
+ }
+ $refresh_changed = ($config_mail_oauth_refresh_token !== '' && $config_mail_oauth_refresh_token !== $prev_refresh)
+ || ($config_mail_oauth_refresh_token === '' && $prev_refresh !== '');
+ }
+
+ // If OAuth refresh changed or provider just switched to non-OAuth, clear access token values
+ if (!$is_oauth || $refresh_changed) {
+ $config_mail_oauth_access_token = '';
+ $config_mail_oauth_access_token_expires_at = '';
+ }
+
+ // Helper for NULL / quoted values
+ $q = fn($v) => ($v !== '' ? "'" . mysqli_real_escape_string($mysqli, $v) . "'" : "NULL");
+
+ // Build UPDATE with correct NULL handling
+ $sql = "
+ UPDATE settings SET
+ config_imap_provider = " . ($config_imap_provider !== '' ? $q($config_imap_provider) : "NULL") . ",
+ config_imap_host = " . $q($config_imap_host) . ",
+ config_imap_port = " . (int)$config_imap_port . ",
+ config_imap_encryption = " . $q($config_imap_encryption) . ",
+ config_imap_username = " . $q($config_imap_username) . ",
+ config_imap_password = " . ($is_oauth ? "NULL" : $q($config_imap_password)) . ",
+
+ -- Shared OAuth fields (kept even if provider is Google or Microsoft; NULL if not used)
+ config_mail_oauth_client_id = " . ($is_oauth ? $q($config_mail_oauth_client_id) : "NULL") . ",
+ config_mail_oauth_client_secret = " . ($is_oauth ? $q($config_mail_oauth_client_secret) : "NULL") . ",
+ config_mail_oauth_tenant_id = " . ($is_oauth ? $q($config_mail_oauth_tenant_id) : "NULL") . ",
+ config_mail_oauth_refresh_token = " . ($is_oauth ? $q($config_mail_oauth_refresh_token) : "NULL") . ",
+ config_mail_oauth_access_token = " . ($is_oauth ? $q($config_mail_oauth_access_token) : "NULL") . ",
+ config_mail_oauth_access_token_expires_at = " . ($is_oauth ? $q($config_mail_oauth_access_token_expires_at) : "NULL") . "
+ WHERE company_id = 1
+ ";
+
+ mysqli_query($mysqli, $sql);
+
+ logAction("Settings", "Edit", "$session_name edited IMAP/OAuth mail settings");
+
+ // Friendly hint about what was saved
+ if ($config_imap_provider === '') {
+ flash_alert("IMAP monitoring disabled (provider not configured).");
+ } elseif ($config_imap_provider === 'standard_imap') {
+ flash_alert("IMAP settings updated (standard username/password).");
+ } elseif ($config_imap_provider === 'google_oauth') {
+ flash_alert("IMAP settings updated for Google Workspace (OAuth).");
+ } elseif ($config_imap_provider === 'microsoft_oauth') {
+ flash_alert("IMAP settings updated for Microsoft 365 (OAuth).");
+ } else {
+ flash_alert("IMAP settings updated.");
+ }
redirect();
-
}
if (isset($_POST['edit_mail_from_settings'])) {
diff --git a/admin/settings_mail.php b/admin/settings_mail.php
index 6d9c9af5..ec18b8bb 100644
--- a/admin/settings_mail.php
+++ b/admin/settings_mail.php
@@ -84,37 +84,56 @@ require_once "includes/inc_all_admin.php";
-
-
-
-
+
-
+
+
+
+
OAuth Settings
+
+ Configure OAuth credentials for the selected provider.
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -327,5 +401,67 @@ require_once "includes/inc_all_admin.php";
-
+(function(){
+ const sel = document.getElementById('config_imap_provider');
+ const pwdGrp = document.getElementById('imap_password_group');
+ const oauthWrap = document.getElementById('oauth_fields');
+ const standardWrap = document.getElementById('standard_fields');
+ const tenantRow = document.getElementById('tenant_row');
+ const oauthHint = document.getElementById('oauth_hint');
+ const providerHint = document.getElementById('imap_provider_hint');
+ function setDisabled(container, disabled){
+ if(!container) return;
+ container.querySelectorAll('input, select, textarea').forEach(el => {
+ el.disabled = !!disabled;
+ });
+ }
+
+ function toggleFields(){
+ if(!sel) return;
+ const v = sel.value || '';
+ const isNone = v === '';
+ const isStd = v === 'standard_imap';
+ const isG = v === 'google_oauth';
+ const isM = v === 'microsoft_oauth';
+ const isOAuth = isG || isM;
+
+ // Show/hide containers
+ if (pwdGrp) pwdGrp.style.display = isStd ? '' : 'none';
+ if (oauthWrap) oauthWrap.style.display = isOAuth ? '' : 'none';
+ if (standardWrap) standardWrap.style.display = isStd ? '' : 'none';
+ if (tenantRow) tenantRow.style.display = isM ? '' : 'none';
+
+ // Disable inputs inside hidden sections to avoid accidental submission
+ setDisabled(pwdGrp, !isStd);
+ setDisabled(standardWrap, !isStd);
+ setDisabled(oauthWrap, !isOAuth);
+
+ // Update hints
+ if (providerHint) {
+ providerHint.textContent = isNone
+ ? 'Choose a provider to reveal the relevant settings.'
+ : isStd
+ ? 'Standard IMAP: provide host, port, encryption, username, and password.'
+ : isG
+ ? 'Google Workspace OAuth: provide Client ID & Secret; paste the refresh token; username should be the mailbox address.'
+ : 'Microsoft 365 OAuth: provide Client ID, Secret & Tenant ID; paste the refresh token; username should be the mailbox address.';
+ }
+ if (oauthHint) {
+ oauthHint.textContent = isG
+ ? 'Google Workspace OAuth: Client ID & Secret from Google Cloud; Refresh token generated via OAuth consent.'
+ : isM
+ ? 'Microsoft 365 OAuth: Client ID, Secret & Tenant ID from Entra ID; Refresh token generated via OAuth consent.'
+ : 'Configure OAuth credentials for the selected provider.';
+ }
+ }
+
+ if (sel) {
+ sel.addEventListener('change', toggleFields);
+ toggleFields();
+ }
+})();
+
+
+ ($raw !== false && $code >= 200 && $code < 300), 'body' => $raw, 'code' => $code, 'err' => $err];
+}
+
+/**
+ * Get a valid access token for Google Workspace IMAP via refresh token if needed.
+ * Uses settings: config_mail_oauth_client_id / _client_secret / _refresh_token / _access_token / _access_token_expires_at
+ * Updates globals if refreshed (so later logging can reflect it if you want to persist).
+ */
+function getGoogleAccessToken(string $username): ?string {
+ // pull from global settings variables you already load
+ global $mysqli,
+ $config_mail_oauth_client_id,
+ $config_mail_oauth_client_secret,
+ $config_mail_oauth_refresh_token,
+ $config_mail_oauth_access_token,
+ $config_mail_oauth_access_token_expires_at;
+
+ // If we have a not-expired token, use it
+ if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
+ return $config_mail_oauth_access_token;
+ }
+
+ // Need to refresh?
+ if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token)) {
+ // Nothing we can do
+ return null;
+ }
+
+ $resp = httpFormPost(
+ 'https://oauth2.googleapis.com/token',
+ [
+ 'client_id' => $config_mail_oauth_client_id,
+ 'client_secret' => $config_mail_oauth_client_secret,
+ 'refresh_token' => $config_mail_oauth_refresh_token,
+ 'grant_type' => 'refresh_token',
+ ]
+ );
+
+ if (!$resp['ok']) return null;
+
+ $json = json_decode($resp['body'], true);
+ if (!is_array($json) || empty($json['access_token'])) return null;
+
+ // Calculate new expiry
+ $expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
+
+ // Update in-memory globals (and persist to DB)
+ $config_mail_oauth_access_token = $json['access_token'];
+ $config_mail_oauth_access_token_expires_at = $expires_at;
+
+ $at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
+ $exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
+ mysqli_query($mysqli, "UPDATE settings SET
+ config_mail_oauth_access_token = '{$at_esc}',
+ config_mail_oauth_access_token_expires_at = '{$exp_esc}'
+ WHERE company_id = 1
+ ");
+
+ return $config_mail_oauth_access_token;
+}
+
+/**
+ * Get a valid access token for Microsoft 365 IMAP via refresh token if needed.
+ * Uses settings: config_mail_oauth_client_id / _client_secret / _tenant_id / _refresh_token / _access_token / _access_token_expires_at
+ */
+function getMicrosoftAccessToken(string $username): ?string {
+ global $mysqli,
+ $config_mail_oauth_client_id,
+ $config_mail_oauth_client_secret,
+ $config_mail_oauth_tenant_id,
+ $config_mail_oauth_refresh_token,
+ $config_mail_oauth_access_token,
+ $config_mail_oauth_access_token_expires_at;
+
+ if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
+ return $config_mail_oauth_access_token;
+ }
+
+ if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token) || empty($config_mail_oauth_tenant_id)) {
+ return null;
+ }
+
+ $url = "https://login.microsoftonline.com/".rawurlencode($config_mail_oauth_tenant_id)."/oauth2/v2.0/token";
+
+ $resp = httpFormPost($url, [
+ 'client_id' => $config_mail_oauth_client_id,
+ 'client_secret' => $config_mail_oauth_client_secret,
+ 'refresh_token' => $config_mail_oauth_refresh_token,
+ 'grant_type' => 'refresh_token',
+ // IMAP/SMTP scopes typically included at initial consent; not needed for refresh
+ ]);
+
+ if (!$resp['ok']) return null;
+
+ $json = json_decode($resp['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));
+
+ $config_mail_oauth_access_token = $json['access_token'];
+ $config_mail_oauth_access_token_expires_at = $expires_at;
+
+ $at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
+ $exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
+ mysqli_query($mysqli, "UPDATE settings SET
+ config_mail_oauth_access_token = '{$at_esc}',
+ config_mail_oauth_access_token_expires_at = '{$exp_esc}'
+ WHERE company_id = 1
+ ");
+
+ return $config_mail_oauth_access_token;
+}
+
+// Provider from settings (may be NULL/empty to disable IMAP polling)
+$imap_provider = $config_imap_provider ?? '';
+if ($imap_provider === null) $imap_provider = '';
+
+if ($imap_provider === '') {
+ // IMAP disabled by admin: exit cleanly
+ logApp("Cron-Email-Parser", "info", "IMAP polling skipped: provider not configured.");
+ @unlink($lock_file_path);
+ exit(0);
+}
+
+/** ------------------------------------------------------------------
+ * Webklex IMAP setup (supports Standard / Google OAuth / Microsoft OAuth)
* ------------------------------------------------------------------ */
use Webklex\PHPIMAP\ClientManager;
-$validate_cert = true; // or false based on your configuration
+$validate_cert = true;
+
+// Defaults from settings (standard IMAP)
+$host = $config_imap_host;
+$port = (int)$config_imap_port;
+$encr = !empty($config_imap_encryption) ? $config_imap_encryption : null; // 'ssl'|'tls'|null
+$user = $config_imap_username;
+$pass = $config_imap_password;
+$auth = null; // 'oauth' for OAuth providers
+
+if ($imap_provider === 'google_oauth') {
+ $host = 'imap.gmail.com';
+ $port = 993;
+ $encr = 'ssl';
+ $auth = 'oauth';
+ $pass = getGoogleAccessToken($user);
+ if (empty($pass)) {
+ logApp("Cron-Email-Parser", "error", "Google OAuth: no usable access token (check refresh token/client credentials).");
+ @unlink($lock_file_path);
+ exit(1);
+ }
+} elseif ($imap_provider === 'microsoft_oauth') {
+ $host = 'outlook.office365.com';
+ $port = 993;
+ $encr = 'ssl';
+ $auth = 'oauth';
+ $pass = getMicrosoftAccessToken($user);
+ if (empty($pass)) {
+ logApp("Cron-Email-Parser", "error", "Microsoft OAuth: no usable access token (check refresh token/client credentials/tenant).");
+ @unlink($lock_file_path);
+ exit(1);
+ }
+} else {
+ // standard_imap (username/password)
+ if (empty($host) || empty($port) || empty($user)) {
+ logApp("Cron-Email-Parser", "error", "Standard IMAP: missing host/port/username.");
+ @unlink($lock_file_path);
+ exit(1);
+ }
+}
$cm = new ClientManager();
-$client = $cm->make([
- 'host' => $config_imap_host,
- 'port' => (int)$config_imap_port,
- 'encryption' => !empty($config_imap_encryption) ? $config_imap_encryption : null, // 'ssl' | 'tls' | null
- 'validate_cert' => (bool)$validate_cert,
- 'username' => $config_imap_username,
- 'password' => $config_imap_password,
- 'protocol' => 'imap'
-]);
+$client = $cm->make(array_filter([
+ 'host' => $host,
+ 'port' => $port,
+ 'encryption' => $encr, // 'ssl' | 'tls' | null
+ 'validate_cert' => (bool)$validate_cert,
+ 'username' => $user, // full mailbox address (OAuth uses user as principal)
+ 'password' => $pass, // access token when $auth === 'oauth'
+ 'authentication' => $auth, // 'oauth' or null
+ 'protocol' => 'imap',
+]));
try {
$client->connect();
} catch (\Throwable $e) {
echo "Error connecting to IMAP server: " . $e->getMessage();
- unlink($lock_file_path);
+ @unlink($lock_file_path);
exit(1);
}
diff --git a/db.sql b/db.sql
index 2e7a31ed..8e1f4443 100644
--- a/db.sql
+++ b/db.sql
@@ -1,9 +1,9 @@
/*M!999999\- enable the sandbox mode */
--- MariaDB dump 10.19 Distrib 10.11.11-MariaDB, for debian-linux-gnu (x86_64)
+-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: itflow_dev
-- ------------------------------------------------------
--- Server version 10.11.11-MariaDB-0+deb12u1
+-- Server version 10.11.14-MariaDB-0+deb12u2
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@@ -1988,6 +1988,13 @@ CREATE TABLE `settings` (
`config_smtp_password` varchar(200) DEFAULT NULL,
`config_mail_from_email` varchar(200) DEFAULT NULL,
`config_mail_from_name` varchar(200) DEFAULT NULL,
+ `config_imap_provider` enum('standard_imap','google_oauth','microsoft_oauth') DEFAULT NULL,
+ `config_mail_oauth_client_id` varchar(255) DEFAULT NULL,
+ `config_mail_oauth_client_secret` varchar(255) DEFAULT NULL,
+ `config_mail_oauth_tenant_id` varchar(255) DEFAULT NULL,
+ `config_mail_oauth_refresh_token` text DEFAULT NULL,
+ `config_mail_oauth_access_token` text DEFAULT NULL,
+ `config_mail_oauth_access_token_expires_at` datetime DEFAULT NULL,
`config_imap_host` varchar(200) DEFAULT NULL,
`config_imap_port` int(5) DEFAULT NULL,
`config_imap_encryption` varchar(200) DEFAULT NULL,
@@ -2758,4 +2765,4 @@ CREATE TABLE `vendors` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--- Dump completed on 2025-08-28 11:43:32
+-- Dump completed on 2025-09-12 15:55:31
diff --git a/includes/database_version.php b/includes/database_version.php
index 4a95c75c..b4caf4ef 100644
--- a/includes/database_version.php
+++ b/includes/database_version.php
@@ -5,4 +5,4 @@
* It is used in conjunction with database_updates.php
*/
-DEFINE("LATEST_DATABASE_VERSION", "2.3.2");
+DEFINE("LATEST_DATABASE_VERSION", "2.3.3");
diff --git a/includes/load_global_settings.php b/includes/load_global_settings.php
index 43cd1b5f..6bb95226 100644
--- a/includes/load_global_settings.php
+++ b/includes/load_global_settings.php
@@ -21,12 +21,21 @@ $config_mail_from_email = $row['config_mail_from_email'];
$config_mail_from_name = $row['config_mail_from_name'];
// Mail - IMAP
+$config_imap_provider = $row['config_imap_provider'];
$config_imap_host = $row['config_imap_host'];
$config_imap_port = intval($row['config_imap_port']);
$config_imap_encryption = $row['config_imap_encryption'];
$config_imap_username = $row['config_imap_username'];
$config_imap_password = $row['config_imap_password'];
+// Mail OAUTH2
+$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'];
+
// Defaults
$config_start_page = $row['config_start_page'] ?? 'clients.php';
$config_default_transfer_from_account = intval($row['config_default_transfer_from_account']);