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";
- +
- +
- -
-
- -
- -
-
- -
- -
-
- -
- -
-
- -
- + + + +
+ + Select your mailbox provider. OAuth options ignore the IMAP password here. + +
+
@@ -123,25 +142,80 @@ require_once "includes/inc_all_admin.php";
- +
-
+
- +
+ +
@@ -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']);