Files
itflow/admin/settings_mail.php
johnnyq 9457442804 Emails stage 3: every email now goes through emailQueue and the shared layout - the client portal invite sends a link to set a password (or to sign in with Microsoft) instead of the password itself, offered on New Contact as well as Edit through portalMailInvite, with Forgot password, password changed and two-step sign-in emails through portalMailPasswordReset and portalMailAccountChange; shared link and code emails with the note quoted and the whole link printed under the button, replacing shareMailHtml and shareQueueMail; calendar invitations, updates and cancellations with when, repeats, where, the description and a join button; Email on the contacts and clients lists in the layout from one of the configured mailboxes with an optional greeting and send time, leaving out archived contacts and contacts without an address, each address once, nothing queued without mail set up, through emailBulkSend and emailBulkPost with a shared bulk_email_fields window; Admin > Mail > Previews shows all 52 emails built by their real senders on sample records inside a rolled-back transaction with emailQueue capturing and custom actions off, with computer and phone widths and Send me a copy; emailHtml gains content and button show_url, emailQueue gains queued_at and always so billing still queues as before, plus emailMailboxes and emailTestSend; ticketMailCreated, ticketMailReply, ticketMailWatchersAdded, ticketMailTaskApproval, ticketMailClosedReply, ticketSlaAlert and billingMailCardSaved moved into functions; fixes Email on the contacts list queuing mail to contacts with no address, the clients list including archived contacts, and both taking any From address posted; changelog updated
2026-10-17 23:26:25 -04:00

747 lines
49 KiB
PHP

<?php
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;
?>
<div class="card card-dark">
<div class="card-header py-3">
<h3 class="card-title"><i class="fas fa-fw fa-envelope me-2"></i>Mail Configuration</h3>
</div>
<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">
<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>
<hr>
<button type="submit" name="edit_mail_smtp_settings" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save Sending Settings</button>
</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">
<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>
<hr>
<button type="submit" name="edit_mail_imap_settings" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save Receiving Settings</button>
</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">
<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>
<hr>
<button type="submit" name="edit_mail_oauth_settings" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save OAuth Credentials</button>
</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">
<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>
<button type="submit" name="edit_mail_from_settings" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save From Addresses</button>
</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);
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"; ?>