Files
itflow/admin/settings_mail.php

754 lines
50 KiB
PHP

<?php
/*
* Admin > Settings > Mail - sending (SMTP, or Google / Microsoft 365 by
* OAuth), receiving the mailbox email to ticket reads, the OAuth app both
* share, the addresses and names emails come from, tests, and a preview of
* every email ITFlow sends. Each tab's form saves on its own
* (admin/post/settings_mail.php), with the frame's save bar.
*/
require_once "includes/inc_all_admin.php";
// ---- Tiny status dot for tab labels ----------------------------------------
function renderMailStatusDot($on) {
return $on
? '<i class="fas fa-circle text-success ms-2" style="font-size:.5rem;vertical-align:middle;" title="Configured"></i>'
: '<i class="far fa-circle text-muted ms-2" style="font-size:.5rem;vertical-align:middle;" title="Not configured"></i>';
}
$smtp_on = !empty($config_smtp_provider);
$imap_on = !empty($config_imap_provider);
$oauth_needed = in_array($config_smtp_provider, ['google_oauth', 'microsoft_oauth'], true)
|| in_array($config_imap_provider, ['google_oauth', 'microsoft_oauth'], true);
// ---- Active tab -------------------------------------------------------------
// The tab lives in the URL (?tab=receiving) so it can be linked, bookmarked, survives a
// reload, and lets the POST handlers send you back to the tab you saved from
$mail_tabs = ['sending', 'receiving', 'oauth', 'from', 'tests', 'previews'];
$active_tab = isset($_GET['tab']) && in_array($_GET['tab'], $mail_tabs, true) ? $_GET['tab'] : 'sending';
// A direct link to the OAuth tab reveals it even when no OAuth provider is selected yet
if ($active_tab === 'oauth') {
$oauth_needed = true;
}
// ---- OAuth callback URI (for Entra App Registration) ------------------------
if (defined('BASE_URL') && !empty(BASE_URL)) {
$mail_oauth_callback_uri = rtrim((string) BASE_URL, '/') . '/admin/oauth_microsoft_mail_callback.php';
} else {
$mail_oauth_callback_uri = 'https://' . rtrim((string) $config_base_url, '/') . '/admin/oauth_microsoft_mail_callback.php';
}
// ---- Readiness checks (drive the Tests tab) --------------------------------
$smtp_standard_ready = !empty($config_smtp_host) && !empty($config_smtp_port)
&& !empty($config_mail_from_email) && !empty($config_mail_from_name);
$smtp_oauth_ready = in_array($config_smtp_provider, ['google_oauth', 'microsoft_oauth'], true)
&& !empty($config_mail_from_email) && !empty($config_mail_from_name)
&& !empty($config_mail_oauth_client_id) && !empty($config_mail_oauth_client_secret)
&& !empty($config_mail_oauth_refresh_token)
&& ($config_smtp_provider !== 'microsoft_oauth' || !empty($config_mail_oauth_tenant_id));
$imap_standard_ready = !empty($config_imap_username) && !empty($config_imap_password)
&& !empty($config_imap_host) && !empty($config_imap_port);
$imap_oauth_ready = in_array($config_imap_provider, ['google_oauth', 'microsoft_oauth'], true)
&& !empty($config_imap_username)
&& !empty($config_mail_oauth_client_id) && !empty($config_mail_oauth_client_secret)
&& !empty($config_mail_oauth_refresh_token)
&& ($config_imap_provider !== 'microsoft_oauth' || !empty($config_mail_oauth_tenant_id));
$oauth_provider_for_test = '';
if (in_array($config_imap_provider, ['google_oauth', 'microsoft_oauth'], true)) {
$oauth_provider_for_test = $config_imap_provider;
} elseif (in_array($config_smtp_provider, ['google_oauth', 'microsoft_oauth'], true)) {
$oauth_provider_for_test = $config_smtp_provider;
}
$oauth_has_required_fields = !empty($oauth_provider_for_test)
&& !empty($config_mail_oauth_client_id) && !empty($config_mail_oauth_client_secret)
&& !empty($config_mail_oauth_refresh_token)
&& ($oauth_provider_for_test !== 'microsoft_oauth' || !empty($config_mail_oauth_tenant_id));
$send_ready = $smtp_standard_ready || $smtp_oauth_ready;
$imap_ready = $imap_standard_ready || $imap_oauth_ready;
?>
<?= settingsPageStart('mail') ?>
<div class="card itflow-settings-card">
<div class="card-body">
<ul class="nav nav-tabs" id="mailTabs" role="tablist">
<li class="nav-item">
<a class="nav-link <?php if ($active_tab === 'sending') { echo 'active'; } ?>" href="?tab=sending" data-bs-target="#tab-sending">
<i class="fas fa-fw fa-paper-plane me-1"></i>Sending<?= renderMailStatusDot($smtp_on) ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php if ($active_tab === 'receiving') { echo 'active'; } ?>" href="?tab=receiving" data-bs-target="#tab-receiving">
<i class="fas fa-fw fa-inbox me-1"></i>Receiving<?= renderMailStatusDot($imap_on) ?>
</a>
</li>
<li class="nav-item" id="tabitem-oauth" style="<?= $oauth_needed ? '' : 'display:none;' ?>">
<a class="nav-link <?php if ($active_tab === 'oauth') { echo 'active'; } ?>" href="?tab=oauth" data-bs-target="#tab-oauth">
<i class="fas fa-fw fa-key me-1"></i>OAuth
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php if ($active_tab === 'from') { echo 'active'; } ?>" href="?tab=from" data-bs-target="#tab-from">
<i class="fas fa-fw fa-at me-1"></i>From Addresses
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php if ($active_tab === 'tests') { echo 'active'; } ?>" href="?tab=tests" data-bs-target="#tab-tests">
<i class="fas fa-fw fa-vial me-1"></i>Tests
</a>
</li>
<li class="nav-item">
<a class="nav-link <?php if ($active_tab === 'previews') { echo 'active'; } ?>" href="?tab=previews" data-bs-target="#tab-previews">
<i class="fas fa-fw fa-eye me-1"></i>Previews
</a>
</li>
</ul>
<div class="tab-content pt-4">
<!-- ============================ SENDING / SMTP ============================ -->
<div class="tab-pane fade <?php if ($active_tab === 'sending') { echo 'show active'; } ?>" id="tab-sending" role="tabpanel">
<form action="post.php" method="post" autocomplete="off" data-settings-form>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="sending">
<div class="mb-3">
<label>SMTP Provider <small class="text-muted">— outbound</small></label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-cloud"></i></span>
<select class="form-select" name="config_smtp_provider" id="config_smtp_provider">
<option value="" <?php if (empty($config_smtp_provider)) { echo 'selected'; } ?>>None (Disabled)</option>
<option value="standard_smtp" <?php if ($config_smtp_provider === 'standard_smtp') { echo 'selected'; } ?>>Standard SMTP (Username/Password)</option>
<option value="google_oauth" <?php if ($config_smtp_provider === 'google_oauth') { echo 'selected'; } ?>>Google Workspace (OAuth)</option>
<option value="microsoft_oauth" <?php if ($config_smtp_provider === 'microsoft_oauth') { echo 'selected'; } ?>>Microsoft 365 (OAuth)</option>
</select>
</div>
<small class="form-text text-muted" id="smtp_provider_hint">Choose your outbound mail provider.</small>
</div>
<div id="smtp_conn_fields">
<div class="row g-2">
<div class="mb-3 col-md-6">
<label>SMTP Host</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
<input type="text" class="form-control" name="config_smtp_host" placeholder="smtp.yourcompany.com" maxlength="200" value="<?= escapeHtml($config_smtp_host) ?>" required>
</div>
</div>
<div class="mb-3 col-md-3">
<label>Port</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
<input type="text" class="form-control numeric-only" inputmode="numeric" pattern="[0-9]*" maxlength="5" name="config_smtp_port" placeholder="587 / 465 / 25" value="<?= !empty($config_smtp_port) ? intval($config_smtp_port) : '' ?>" required>
</div>
</div>
<div class="mb-3 col-md-3">
<label>Encryption</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
<select class="form-select" name="config_smtp_encryption">
<option value="">None</option>
<option <?php if ($config_smtp_encryption == 'tls') { echo "selected"; } ?> value="tls">TLS</option>
<option <?php if ($config_smtp_encryption == 'ssl') { echo "selected"; } ?> value="ssl">SSL</option>
</select>
</div>
</div>
</div>
</div>
<div class="row g-2">
<div class="mb-3 col-md-6" id="smtp_user_group">
<label id="smtp_user_label">SMTP Username</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<input type="text" class="form-control" name="config_smtp_username" id="config_smtp_username" placeholder="usually your full email address" maxlength="200" value="<?= escapeHtml($config_smtp_username) ?>">
</div>
<small class="form-text text-muted" id="smtp_user_hint">Leave blank if no authentication is required.</small>
</div>
<div class="mb-3 col-md-6" id="smtp_pass_group">
<label>SMTP Password</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="password" class="form-control" data-toggle="password" name="config_smtp_password" placeholder="mailbox or app password" maxlength="200" value="<?= escapeHtml($config_smtp_password) ?>" autocomplete="new-password"><span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
</div>
</div>
<div class="alert alert-info" id="smtp_oauth_pointer" style="display:none;">
<div class="d-flex align-items-center justify-content-between">
<span><i class="fas fa-fw fa-info-circle me-2"></i>This provider uses OAuth — the password is ignored. Enter app credentials in the OAuth tab.</span>
<button type="button" class="btn btn-sm btn-outline-primary goto-oauth ms-3 text-nowrap"><i class="fas fa-fw fa-key me-1"></i>Open OAuth</button>
</div>
</div>
<?= settingsSaveBar('edit_mail_smtp_settings') ?>
</form>
</div>
<!-- ============================ RECEIVING / IMAP ============================ -->
<div class="tab-pane fade <?php if ($active_tab === 'receiving') { echo 'show active'; } ?>" id="tab-receiving" role="tabpanel">
<form action="post.php" method="post" autocomplete="off" data-settings-form>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="receiving">
<div class="mb-3">
<label>IMAP Provider <small class="text-muted">— inbound ticket inbox</small></label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-cloud"></i></span>
<select class="form-select" name="config_imap_provider" id="config_imap_provider">
<option value="" <?php if (empty($config_imap_provider)) { echo 'selected'; } ?>>None (Disabled)</option>
<option value="standard_imap" <?php if ($config_imap_provider === 'standard_imap') { echo 'selected'; } ?>>Standard IMAP (Username/Password)</option>
<option value="google_oauth" <?php if ($config_imap_provider === 'google_oauth') { echo 'selected'; } ?>>Google Workspace (OAuth)</option>
<option value="microsoft_oauth" <?php if ($config_imap_provider === 'microsoft_oauth') { echo 'selected'; } ?>>Microsoft 365 (OAuth)</option>
</select>
</div>
<small class="form-text text-muted" id="imap_provider_hint">Select your mailbox provider.</small>
</div>
<div id="imap_conn_fields">
<div class="row g-2">
<div class="mb-3 col-md-6">
<label>IMAP Host</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-server"></i></span>
<input type="text" class="form-control" name="config_imap_host" placeholder="imap.yourcompany.com" maxlength="200" value="<?= escapeHtml($config_imap_host) ?>">
</div>
</div>
<div class="mb-3 col-md-3">
<label>Port</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-plug"></i></span>
<input type="text" class="form-control numeric-only" inputmode="numeric" pattern="[0-9]*" maxlength="5" name="config_imap_port" placeholder="993 / 143" value="<?= !empty($config_imap_port) ? intval($config_imap_port) : '' ?>">
</div>
</div>
<div class="mb-3 col-md-3">
<label>Encryption</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-lock"></i></span>
<select class="form-select" name="config_imap_encryption">
<option value="">None</option>
<option <?php if ($config_imap_encryption == 'tls') { echo "selected"; } ?> value="tls">TLS</option>
<option <?php if ($config_imap_encryption == 'ssl') { echo "selected"; } ?> value="ssl">SSL</option>
</select>
</div>
</div>
</div>
</div>
<div class="row g-2">
<div class="mb-3 col-md-6" id="imap_user_group">
<label id="imap_user_label">IMAP Username</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-user"></i></span>
<input type="text" class="form-control" name="config_imap_username" placeholder="tickets@yourcompany.com" maxlength="200" value="<?= escapeHtml($config_imap_username) ?>" required>
</div>
<small class="form-text text-muted" id="imap_user_hint">The mailbox address to monitor for incoming tickets.</small>
</div>
<div class="mb-3 col-md-6" id="imap_pass_group">
<label>IMAP Password</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="password" class="form-control" data-toggle="password" name="config_imap_password" placeholder="mailbox or app password" maxlength="200" value="<?= escapeHtml($config_imap_password) ?>" autocomplete="new-password"><span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
</div>
</div>
<div class="alert alert-info" id="imap_oauth_pointer" style="display:none;">
<div class="d-flex align-items-center justify-content-between">
<span><i class="fas fa-fw fa-info-circle me-2"></i>This provider uses OAuth — the password is ignored. Enter app credentials in the OAuth tab.</span>
<button type="button" class="btn btn-sm btn-outline-primary goto-oauth ms-3 text-nowrap"><i class="fas fa-fw fa-key me-1"></i>Open OAuth</button>
</div>
</div>
<?= settingsSaveBar('edit_mail_imap_settings') ?>
</form>
</div>
<!-- ============================ OAUTH ============================ -->
<div class="tab-pane fade <?php if ($active_tab === 'oauth') { echo 'show active'; } ?>" id="tab-oauth" role="tabpanel">
<form action="post.php" method="post" autocomplete="off" data-settings-form>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="oauth">
<div class="alert alert-secondary" id="oauth_hint">
<i class="fas fa-fw fa-info-circle me-2"></i>These credentials are shared by any Sending or Receiving provider set to Google / Microsoft OAuth.
</div>
<div class="row g-2">
<div class="mb-3 col-md-6">
<label>OAuth Client ID</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-id-badge"></i></span>
<input type="text" class="form-control" name="config_mail_oauth_client_id" id="config_mail_oauth_client_id" placeholder="Application (client) ID" maxlength="255" value="<?= escapeHtml($config_mail_oauth_client_id ?? '') ?>">
</div>
</div>
<div class="mb-3 col-md-6">
<label>OAuth Client Secret</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="password" class="form-control" data-toggle="password" name="config_mail_oauth_client_secret" id="config_mail_oauth_client_secret" placeholder="Client secret value" maxlength="255" value="<?= escapeHtml($config_mail_oauth_client_secret ?? '') ?>" autocomplete="new-password"><span class="input-group-text"><i class="fa fa-fw fa-eye"></i></span>
</div>
</div>
</div>
<div class="mb-3" id="tenant_row" style="display:none;">
<label>Tenant ID <small class="text-muted">— Microsoft 365 only</small></label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-building"></i></span>
<input type="text" class="form-control" name="config_mail_oauth_tenant_id" placeholder="Directory (tenant) ID, e.g. 00000000-0000-0000-0000-000000000000" maxlength="255" value="<?= escapeHtml($config_mail_oauth_tenant_id ?? '') ?>">
</div>
</div>
<div class="row g-2">
<div class="mb-3 col-md-6">
<label>Refresh Token</label>
<textarea class="form-control" name="config_mail_oauth_refresh_token" rows="2" placeholder="Paste a refresh token, or use the Connect button below to fetch one"><?= escapeHtml($config_mail_oauth_refresh_token ?? '') ?></textarea>
</div>
<div class="mb-3 col-md-6">
<label>Access Token <small class="text-muted">— optional</small></label>
<textarea class="form-control" name="config_mail_oauth_access_token" rows="2" placeholder="Leave blank — auto-refreshed from the refresh token"><?= escapeHtml($config_mail_oauth_access_token ?? '') ?></textarea>
<small class="form-text text-muted">Expires at: <?= !empty($config_mail_oauth_access_token_expires_at) ? htmlspecialchars($config_mail_oauth_access_token_expires_at) : 'n/a' ?></small>
</div>
</div>
<div class="mb-3" id="ms_connect_group" style="display:none;">
<label>Microsoft OAuth Connect (Web)</label>
<div class="input-group"><span class="input-group-text"><i class="fa fa-fw fa-link"></i></span>
<input type="text" class="form-control" readonly value="<?= htmlspecialchars($mail_oauth_callback_uri) ?>">
<button type="submit" name="oauth_connect_microsoft_mail" class="btn btn-outline-primary">
<i class="fab fa-fw fa-microsoft me-2"></i>Connect Microsoft 365
</button>
</div>
<small class="form-text text-muted">Add this callback URI in your Entra App Registration, save credentials, then click Connect to store the refresh token automatically.</small>
</div>
<?= settingsSaveBar('edit_mail_oauth_settings') ?>
</form>
</div>
<!-- ============================ FROM ADDRESSES ============================ -->
<div class="tab-pane fade <?php if ($active_tab === 'from') { echo 'show active'; } ?>" id="tab-from" role="tabpanel">
<form action="post.php" method="post" autocomplete="off" data-settings-form>
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="from">
<p class="text-muted">Each From address must be allowed to send on behalf of the SMTP user.</p>
<table class="table table-sm">
<thead>
<tr>
<th style="width:26%">Purpose</th>
<th>From Email</th>
<th>From Name</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-middle">System Default<br><small class="text-muted">share links &amp; system tasks</small></td>
<td class="align-middle"><input type="email" class="form-control form-control-sm" name="config_mail_from_email" placeholder="noreply@yourcompany.com" maxlength="200" value="<?= escapeHtml($config_mail_from_email) ?>"></td>
<td class="align-middle"><input type="text" class="form-control form-control-sm" name="config_mail_from_name" placeholder="YourCompany" maxlength="200" value="<?= escapeHtml($config_mail_from_name) ?>"></td>
</tr>
<tr>
<td class="align-middle">Invoices<br><small class="text-muted">sent when emailing invoices</small></td>
<td class="align-middle"><input type="email" class="form-control form-control-sm" name="config_invoice_from_email" placeholder="billing@yourcompany.com" maxlength="200" value="<?= escapeHtml($config_invoice_from_email) ?>"></td>
<td class="align-middle"><input type="text" class="form-control form-control-sm" name="config_invoice_from_name" placeholder="YourCompany Billing" maxlength="200" value="<?= escapeHtml($config_invoice_from_name) ?>"></td>
</tr>
<tr>
<td class="align-middle">Quotes<br><small class="text-muted">sent when emailing quotes</small></td>
<td class="align-middle"><input type="email" class="form-control form-control-sm" name="config_quote_from_email" placeholder="sales@yourcompany.com" maxlength="200" value="<?= escapeHtml($config_quote_from_email) ?>"></td>
<td class="align-middle"><input type="text" class="form-control form-control-sm" name="config_quote_from_name" placeholder="YourCompany Sales" maxlength="200" value="<?= escapeHtml($config_quote_from_name) ?>"></td>
</tr>
<tr>
<td class="align-middle">Tickets<br><small class="text-muted">ticket creation &amp; client replies</small></td>
<td class="align-middle"><input type="email" class="form-control form-control-sm" name="config_ticket_from_email" placeholder="support@yourcompany.com" maxlength="200" value="<?= escapeHtml($config_ticket_from_email) ?>"></td>
<td class="align-middle"><input type="text" class="form-control form-control-sm" name="config_ticket_from_name" placeholder="YourCompany Support" maxlength="200" value="<?= escapeHtml($config_ticket_from_name) ?>"></td>
</tr>
</tbody>
</table>
<?= settingsSaveBar('edit_mail_from_settings') ?>
</form>
</div>
<!-- ============================ TESTS ============================ -->
<div class="tab-pane fade <?php if ($active_tab === 'tests') { echo 'show active'; } ?>" id="tab-tests" role="tabpanel">
<?php if (!$send_ready && !$imap_ready && !$oauth_has_required_fields) { ?>
<div class="alert alert-secondary mb-0">
<i class="fas fa-fw fa-info-circle me-2"></i>Finish configuring Sending, Receiving, or OAuth (plus at least one From address) to unlock the tests.
</div>
<?php } ?>
<?php if ($send_ready) { ?>
<div class="mb-4">
<h6 class="text-bold"><i class="fas fa-fw fa-paper-plane me-2"></i>Send a Test Email</h6>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="tests">
<div class="input-group">
<select class="form-select select2" name="test_email" required>
<option value="">- Select a From address -</option>
<?php if ($config_mail_from_email) { ?><option value="1"><?= escapeHtml($config_mail_from_name) ?> (<?= escapeHtml($config_mail_from_email) ?>)</option><?php } ?>
<?php if ($config_invoice_from_email) { ?><option value="2"><?= escapeHtml($config_invoice_from_name) ?> (<?= escapeHtml($config_invoice_from_email) ?>)</option><?php } ?>
<?php if ($config_quote_from_email) { ?><option value="3"><?= escapeHtml($config_quote_from_name) ?> (<?= escapeHtml($config_quote_from_email) ?>)</option><?php } ?>
<?php if ($config_ticket_from_email) { ?><option value="4"><?= escapeHtml($config_ticket_from_name) ?> (<?= escapeHtml($config_ticket_from_email) ?>)</option><?php } ?>
</select>
<input type="email" class="form-control" name="email_to" placeholder="recipient@example.com">
<button type="submit" name="test_email_smtp" class="btn btn-success"><i class="fas fa-fw fa-paper-plane me-2"></i>Send</button>
</div>
</form>
</div>
<?php } ?>
<?php if ($imap_ready) { ?>
<div class="mb-4">
<h6 class="text-bold"><i class="fas fa-fw fa-plug me-2"></i>Test IMAP Connection</h6>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="tests">
<button type="submit" name="test_email_imap" class="btn btn-success"><i class="fas fa-fw fa-inbox me-2"></i>Test IMAP</button>
</form>
</div>
<?php } ?>
<?php if ($oauth_has_required_fields) { ?>
<div>
<h6 class="text-bold"><i class="fas fa-fw fa-sync-alt me-2"></i>Test OAuth Token Refresh</h6>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="tab" value="tests">
<input type="hidden" name="oauth_provider" value="<?= htmlspecialchars($oauth_provider_for_test) ?>">
<p class="text-muted mb-2">Validates the refresh token and stores a new access token for <?= $oauth_provider_for_test === 'microsoft_oauth' ? 'Microsoft 365' : 'Google Workspace' ?>.</p>
<button type="submit" name="test_oauth_token_refresh" class="btn btn-success"><i class="fas fa-fw fa-sync-alt me-2"></i>Test OAuth Token Refresh</button>
</form>
</div>
<?php } ?>
</div>
<!-- ============================ PREVIEWS ============================ -->
<?php
// Every email ITFlow sends, made on sample records that are thrown away again (functions/email_preview.php)
$preview_catalog = emailPreviewCatalog();
$preview_groups = [];
foreach ($preview_catalog as $preview_key => $preview_entry) {
$preview_groups[$preview_entry['group']][$preview_key] = $preview_entry;
}
$preview_first = isset($_GET['email'], $preview_catalog[$_GET['email']]) ? (string) $_GET['email'] : (string) array_key_first($preview_catalog);
?>
<div class="tab-pane fade <?php if ($active_tab === 'previews') { echo 'show active'; } ?>" id="tab-previews" role="tabpanel">
<p class="text-body-secondary">
Every email ITFlow sends, as it goes out - your logo, your details and your From addresses. Each is made just now on sample records
(<?= escapeHtml(EMAIL_PREVIEW_PEOPLE['client']) ?>, its contact <?= escapeHtml(EMAIL_PREVIEW_PEOPLE['contact'][0]) ?> and an agent, <?= escapeHtml(EMAIL_PREVIEW_PEOPLE['agent'][0]) ?>)
that are thrown away again, so looking sends and saves nothing.
</p>
<div class="row g-3">
<div class="col-lg-4">
<div class="input-group mb-2">
<span class="input-group-text"><i class="fas fa-fw fa-search"></i></span>
<input type="search" class="form-control" placeholder="Find an email" data-preview-filter aria-label="Find an email">
</div>
<div class="list-group itflow-mail-preview-list" style="max-height: 75vh; overflow-y: auto;">
<?php foreach ($preview_groups as $preview_group => $preview_entries) { ?>
<div class="list-group-item bg-body-tertiary small fw-semibold text-uppercase text-body-secondary py-1" data-preview-group><?= escapeHtml($preview_group) ?></div>
<?php foreach ($preview_entries as $preview_key => $preview_entry) { ?>
<button type="button" class="list-group-item list-group-item-action <?= $preview_key === $preview_first ? 'active' : '' ?>" data-preview="<?= escapeHtml($preview_key) ?>"
data-preview-text="<?= escapeHtml(strtolower($preview_group . ' ' . $preview_entry['label'] . ' ' . $preview_entry['to'] . ' ' . $preview_entry['when'])) ?>">
<div class="fw-semibold"><?= escapeHtml($preview_entry['label']) ?></div>
<div class="small opacity-75"><?= escapeHtml($preview_entry['to']) ?></div>
</button>
<?php } ?>
<?php } ?>
</div>
</div>
<div class="col-lg-8">
<div class="card mb-0" id="mailPreview">
<div class="card-header">
<div class="d-flex flex-wrap align-items-start gap-2">
<div class="flex-grow-1 min-w-0">
<div class="fw-semibold text-break" data-preview-subject></div>
<div class="small text-body-secondary text-break"><span class="fw-semibold">From</span> <span data-preview-from></span></div>
<div class="small text-body-secondary text-break"><span class="fw-semibold">To</span> <span data-preview-to></span> <span data-preview-also></span></div>
<div class="small text-body-secondary"><i class="fas fa-fw fa-bolt me-1"></i><span data-preview-when></span><span class="ms-2 badge text-bg-secondary d-none" data-preview-calendar><i class="fas fa-calendar-alt me-1"></i>Calendar file attached</span></div>
</div>
<div class="d-flex gap-2 align-items-center">
<div class="btn-group btn-group-sm" role="group" aria-label="Width">
<button type="button" class="btn btn-outline-secondary active" data-preview-width="100%" title="As on a computer"><i class="fas fa-fw fa-desktop"></i></button>
<button type="button" class="btn btn-outline-secondary" data-preview-width="390px" title="As on a phone"><i class="fas fa-fw fa-mobile-alt"></i></button>
</div>
<form action="post.php" method="post" class="m-0">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="email" value="<?= escapeHtml($preview_first) ?>" data-preview-key>
<button type="submit" name="send_email_preview" class="btn btn-sm btn-primary" <?= emailReady() ? '' : 'disabled title="Set up Sending first"' ?>>
<i class="fas fa-fw fa-paper-plane me-1"></i>Send me a copy
</button>
</form>
</div>
</div>
</div>
<div class="card-body p-0 bg-body-tertiary text-center">
<div class="alert alert-warning m-3 d-none" data-preview-error></div>
<iframe title="Email preview" sandbox="allow-same-origin" data-preview-frame style="width: 100%; height: 600px; border: 0; display: block; margin: 0 auto; background: #f1f3f5;"></iframe>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Admin > Mail > Previews - each email fetched as it is picked (admin/mail_preview.php)
(function () {
var card = document.getElementById('mailPreview');
if (!card) {
return;
}
var frame = card.querySelector('[data-preview-frame]');
var csrf = <?= json_encode($_SESSION['csrf_token']) ?>;
var items = Array.from(document.querySelectorAll('[data-preview]'));
var loading = 0;
function fit() {
try {
var doc = frame.contentDocument;
if (doc && doc.documentElement) {
frame.style.height = Math.max(400, doc.documentElement.scrollHeight) + 'px';
}
} catch (e) {
// nothing to measure
}
}
frame.addEventListener('load', fit);
function show(key, push) {
items.forEach(function (item) {
item.classList.toggle('active', item.getAttribute('data-preview') === key);
});
card.querySelector('[data-preview-key]').value = key;
var url = new URL(window.location.href);
url.searchParams.set('tab', 'previews');
url.searchParams.set('email', key);
history.replaceState(history.state, '', url);
var mine = ++loading;
card.style.opacity = '0.6';
fetch('mail_preview.php?email=' + encodeURIComponent(key) + '&csrf_token=' + encodeURIComponent(csrf), { credentials: 'same-origin' })
.then(function (response) { return response.json(); })
.then(function (data) {
if (mine !== loading) {
return;
}
card.style.opacity = '';
card.querySelector('[data-preview-subject]').textContent = data.subject || data.label || '';
card.querySelector('[data-preview-from]').textContent = data.from || '-';
card.querySelector('[data-preview-to]').textContent = data.to || data.to_who || '-';
card.querySelector('[data-preview-also]').textContent = data.also && data.also.length ? '(and ' + data.also.join(', ') + ')' : '';
card.querySelector('[data-preview-when]').textContent = 'Sent when: ' + (data.when || '');
card.querySelector('[data-preview-calendar]').classList.toggle('d-none', !data.calendar);
var error = card.querySelector('[data-preview-error]');
error.textContent = data.error || '';
error.classList.toggle('d-none', !data.error);
frame.srcdoc = data.html || '';
})
.catch(function () {
card.style.opacity = '';
var error = card.querySelector('[data-preview-error]');
error.textContent = 'The preview could not be loaded.';
error.classList.remove('d-none');
});
}
items.forEach(function (item) {
item.addEventListener('click', function () {
show(item.getAttribute('data-preview'));
});
});
card.querySelectorAll('[data-preview-width]').forEach(function (button) {
button.addEventListener('click', function () {
card.querySelectorAll('[data-preview-width]').forEach(function (other) {
other.classList.toggle('active', other === button);
});
frame.style.width = button.getAttribute('data-preview-width');
setTimeout(fit, 50);
});
});
// Find an email - its group heading stays while any of its emails show
var filter = document.querySelector('[data-preview-filter]');
filter.addEventListener('input', function () {
var words = filter.value.toLowerCase().trim().split(/\s+/).filter(Boolean);
var heading = null;
var any = false;
Array.from(document.querySelector('.itflow-mail-preview-list').children).forEach(function (row) {
if (row.hasAttribute('data-preview-group')) {
if (heading) {
heading.classList.toggle('d-none', !any);
}
heading = row;
any = false;
return;
}
var text = row.getAttribute('data-preview-text');
var match = words.every(function (word) { return text.indexOf(word) !== -1; });
row.classList.toggle('d-none', !match);
any = any || match;
});
if (heading) {
heading.classList.toggle('d-none', !any);
}
});
// The first one, when the tab is opened
var started = false;
function start() {
if (!started && document.getElementById('tab-previews').classList.contains('active')) {
started = true;
show(<?= json_encode($preview_first) ?>);
}
}
document.querySelectorAll('#mailTabs .nav-link').forEach(function (link) {
link.addEventListener('click', function () {
setTimeout(start, 0);
});
});
window.addEventListener('popstate', function () {
setTimeout(start, 0);
});
start();
})();
</script>
<script>
(function () {
function setDisabled(c, d) { if (c) c.querySelectorAll('input,select,textarea').forEach(el => el.disabled = !!d); }
function show(el, v) { if (el) el.style.display = v ? '' : 'none'; }
function toggle(el, v) { show(el, v); setDisabled(el, !v); }
function val(s) { return (s && s.value) || ''; }
function isStd(v) { return v === 'standard_imap' || v === 'standard_smtp'; }
function isOauth(v) { return v === 'google_oauth' || v === 'microsoft_oauth'; }
// ---- Numeric-only inputs (ports): strip anything that isn't a digit ----
document.querySelectorAll('.numeric-only').forEach(function (el) {
el.addEventListener('input', function () { this.value = this.value.replace(/[^0-9]/g, ''); });
});
// ---- Self-contained tab controller (no dependency on the BS tab plugin) ----
// Set when the page was opened directly on the OAuth tab - stops the provider pass hiding it
const forcedOauthTab = <?= $active_tab === 'oauth' ? 'true' : 'false' ?>;
const navLinks = Array.from(document.querySelectorAll('#mailTabs .nav-link'));
const panes = ['tab-sending', 'tab-receiving', 'tab-oauth', 'tab-from', 'tab-tests', 'tab-previews']
.map(id => document.getElementById(id)).filter(Boolean);
// Server rendered the initial tab; keep the URL honest as the user clicks around
function syncTabUrl(target, push) {
const tab = target.replace('#tab-', '');
const url = new URL(window.location.href);
url.searchParams.set('tab', tab);
// pushState, not replaceState, so back/forward step through the tabs
// instead of jumping straight off the settings page
history[push ? 'pushState' : 'replaceState']({ tab: tab }, '', url);
}
function activateTab(target, push) {
syncTabUrl(target, push !== false);
navLinks.forEach(l => l.classList.toggle('active', l.getAttribute('data-bs-target') === target));
panes.forEach(p => {
const on = ('#' + p.id) === target;
p.classList.toggle('active', on);
p.classList.toggle('show', on);
});
}
window.addEventListener('popstate', function () {
const tab = new URL(window.location.href).searchParams.get('tab') || 'sending';
activateTab('#tab-' + tab, false);
});
navLinks.forEach(l => l.addEventListener('click', function (e) {
e.preventDefault();
activateTab(l.getAttribute('data-bs-target'), true);
}));
// ---- Provider-driven field visibility ----
const smtpSel = document.getElementById('config_smtp_provider');
const imapSel = document.getElementById('config_imap_provider');
const smtpConn = document.getElementById('smtp_conn_fields');
const smtpUser = document.getElementById('smtp_user_group');
const smtpPass = document.getElementById('smtp_pass_group');
const smtpPtr = document.getElementById('smtp_oauth_pointer');
const smtpHint = document.getElementById('smtp_provider_hint');
const smtpUserLb = document.getElementById('smtp_user_label');
const smtpUserHt = document.getElementById('smtp_user_hint');
const smtpUserIn = document.getElementById('config_smtp_username');
const imapConn = document.getElementById('imap_conn_fields');
const imapUser = document.getElementById('imap_user_group');
const imapPass = document.getElementById('imap_pass_group');
const imapPtr = document.getElementById('imap_oauth_pointer');
const imapHint = document.getElementById('imap_provider_hint');
const imapUserLb = document.getElementById('imap_user_label');
const imapUserHt = document.getElementById('imap_user_hint');
const oauthTabItem = document.getElementById('tabitem-oauth');
const tenantRow = document.getElementById('tenant_row');
const msConnect = document.getElementById('ms_connect_group');
const oauthHint = document.getElementById('oauth_hint');
const oauthClientId = document.getElementById('config_mail_oauth_client_id');
const oauthClientSecret = document.getElementById('config_mail_oauth_client_secret');
function render() {
const sv = val(smtpSel), iv = val(imapSel);
toggle(smtpConn, isStd(sv));
toggle(smtpUser, isStd(sv) || isOauth(sv));
toggle(smtpPass, isStd(sv));
show(smtpPtr, isOauth(sv));
if (smtpUserLb) smtpUserLb.textContent = isOauth(sv) ? 'Authenticated User Email (licensed user)' : 'SMTP Username';
if (smtpUserIn) smtpUserIn.placeholder = isOauth(sv) ? 'licensed.user@yourcompany.com' : 'usually your full email address';
if (smtpUserHt) smtpUserHt.innerHTML = isOauth(sv)
? 'The licensed user that completed the OAuth flow &mdash; <strong>not</strong> the From / shared-mailbox address. Becomes the <code>user=</code> identity in the XOAUTH2 string.'
: 'Leave blank if no authentication is required.';
if (smtpHint) smtpHint.textContent = isOauth(sv) ? 'OAuth: set the authenticated user email here; app credentials live in the OAuth tab.'
: isStd(sv) ? 'Standard: host, port, encryption, username & password.' : 'Disabled.';
toggle(imapConn, isStd(iv));
toggle(imapUser, isStd(iv) || isOauth(iv));
toggle(imapPass, isStd(iv));
show(imapPtr, isOauth(iv));
if (imapUserLb) imapUserLb.textContent = isOauth(iv) ? 'Mailbox Email (monitored inbox)' : 'IMAP Username';
if (imapUserHt) imapUserHt.textContent = isOauth(iv)
? 'The mailbox you monitor for tickets (the account the refresh token was issued for).'
: 'The mailbox address to monitor for incoming tickets.';
if (imapHint) imapHint.textContent = isOauth(iv) ? 'OAuth: set the mailbox here; app credentials live in the OAuth tab.'
: isStd(iv) ? 'Standard: host, port, encryption, username & password.' : 'Disabled.';
const anyOauth = isOauth(sv) || isOauth(iv);
const anyMs = sv === 'microsoft_oauth' || iv === 'microsoft_oauth';
show(oauthTabItem, anyOauth || forcedOauthTab);
toggle(tenantRow, anyMs);
toggle(msConnect, anyMs);
if (oauthClientId) oauthClientId.placeholder = anyMs
? 'Application (client) ID, e.g. 00000000-0000-0000-0000-000000000000'
: 'xxxxxxxxxxxx.apps.googleusercontent.com';
if (oauthClientSecret) oauthClientSecret.placeholder = anyMs ? 'Entra client secret value' : 'Google client secret';
if (oauthHint) oauthHint.innerHTML = anyMs
? '<i class="fas fa-fw fa-info-circle me-2"></i>Microsoft 365: Client ID / Secret / Tenant from Entra ID; refresh token via the Connect button below.'
: anyOauth ? '<i class="fas fa-fw fa-info-circle me-2"></i>Google Workspace: Client ID / Secret from Google Cloud; refresh token obtained via the consent flow.'
: '<i class="fas fa-fw fa-info-circle me-2"></i>These credentials are shared by any Sending or Receiving provider set to Google / Microsoft OAuth.';
if (!anyOauth && !forcedOauthTab) {
const oauthLink = document.querySelector('#mailTabs .nav-link[data-bs-target="#tab-oauth"]');
if (oauthLink && oauthLink.classList.contains('active')) activateTab('#tab-sending');
}
}
if (smtpSel) smtpSel.addEventListener('change', render);
// Undo changes puts the provider back - and with it the fields it shows
document.querySelectorAll('#mailTabs ~ .tab-content form[data-settings-form]').forEach(f => f.addEventListener('settings:reset', render));
if (imapSel) imapSel.addEventListener('change', render);
document.querySelectorAll('.goto-oauth').forEach(b => b.addEventListener('click', () => activateTab('#tab-oauth')));
render();
})();
</script>
<?php require_once "../includes/footer.php"; ?>