Implement IMAP email-to-ticket gateway
This commit is contained in:
1 parent
b7e5884457
commit
6f25869345
33 files changed
+5619
-5
No files matched your search
@@ -0,0 +1,192 @@
|
||||
{% extends request.ajax ? 'layout_blank.html.twig' : 'layout_default.html.twig' %}
|
||||
{% block meta_title %}{{ 'Mail Gateway Settings'|trans }}{% endblock %}
|
||||
{% set active_menu = 'system' %}
|
||||
{% set result = admin.mailgateway_settings_get({}) %}
|
||||
{% set params = result.settings %}
|
||||
|
||||
{% block content %}
|
||||
{% if result.notifications is defined %}
|
||||
<div id="mg-notification-banner" data-ready="{{ result.notifications.ready ? '1' : '0' }}" class="alert {{ result.notifications.ready ? 'alert-success' : 'alert-warning' }}" {% if result.notifications.ready %}hidden{% endif %}>
|
||||
{% if result.notifications.ready %}
|
||||
{{ 'Ticket acknowledgement and enabled staff notification subjects are configured for email replies.'|trans }}
|
||||
{% else %}
|
||||
<p>{{ 'Ticket notifications need configuration. Enabling mailbox importing will configure them automatically, or use Advanced below.'|trans }}</p>
|
||||
<ul>{% for error in result.notifications.errors %}<li>{{ error }}</li>{% endfor %}</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if result.dependencies is defined and not result.dependencies.installed %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
{% if result.dependencies.available %}
|
||||
<p>{{ 'Install the required mail library before testing connections or enabling imports.'|trans }}</p>
|
||||
<form action="{{ 'mailgateway/install_dependencies'|api_url(role: 'admin') }}" {{ fb_api_form({message: 'Mail Gateway dependencies installed'|trans, reload: true, timeoutMs: result.dependencies.request_timeout_ms|default(90000), loading: {button: 'Installing dependencies…'|trans}, preventNavigation: true}) }}>
|
||||
<button class="btn btn-primary" type="submit">{{ 'Install dependencies'|trans }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{{ 'Automatic installation is unavailable on this server. Run composer install from the Mailgateway directory using SSH.'|trans }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary mb-3" type="button" onclick="document.getElementById('mg-new').hidden = false; this.hidden = true">{{ 'Add mailbox'|trans }}</button>
|
||||
{% set mailboxes = result.mailboxes is defined ? result.mailboxes : {'default': result.settings} %}
|
||||
{% set newMailbox = result.settings|merge({'password_configured': false, 'enabled': false, 'mailbox': '', 'username': '', 'test_passed': false, 'test_result': null}) %}
|
||||
{% for mailboxId, params in mailboxes|merge({'new': newMailbox}) %}
|
||||
{% set canEnable = result.dependencies.installed|default(false) and params.test_passed|default(false) %}
|
||||
{% set disabledReason = not result.dependencies.installed|default(false) ? 'Install dependencies before enabling mailbox importing.'|trans : 'Save this mailbox, then run Test Connection and Create Folders successfully before enabling importing. Connection, password, or folder changes require another test.'|trans %}
|
||||
<div class="card mb-3" id="mg-{{ mailboxId }}" {% if mailboxId == 'new' %}hidden{% endif %}>
|
||||
<div class="card-header"><h3 class="card-title">{{ mailboxId == 'new' ? 'New mailbox'|trans : params.mailbox }}</h3></div>
|
||||
<form method="post" action="{{ 'mailgateway/settings_save'|api_url(role: 'admin') }}" {{ fb_api_form({message: 'Mail Gateway settings saved'|trans, reload: true}) }}>
|
||||
<input type="hidden" name="mailbox_id" value="{{ mailboxId }}">
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">{{ 'Each enabled mailbox routes new tickets to its selected helpdesk. The existing FOSSBilling cron polls mailboxes at the interval defined in the module configuration file.'|trans }}</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-check form-switch" id="mg-enable-label-{{ mailboxId }}" {% if not canEnable %}title="{{ disabledReason }}"{% endif %}>
|
||||
<input type="hidden" name="enabled" value="0">
|
||||
<input class="form-check-input" id="mg-enabled-{{ mailboxId }}" type="checkbox" name="enabled" value="1" aria-describedby="mg-enable-hint-{{ mailboxId }}" {% if params.enabled and canEnable %}checked{% endif %} {% if not canEnable %}disabled{% endif %}>
|
||||
<span class="form-check-label">{{ 'Enable mailbox importing'|trans }}</span>
|
||||
</label>
|
||||
<small class="form-hint" id="mg-enable-hint-{{ mailboxId }}">{% if not canEnable %}{{ disabledReason }}{% endif %}</small>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8"><label class="form-label" for="mg-{{ mailboxId }}-host">{{ 'IMAP host'|trans }}</label><input class="form-control" id="mg-{{ mailboxId }}-host" name="host" value="{{ params.host }}" maxlength="255" required></div>
|
||||
<div class="col-md-4"><label class="form-label" for="mg-{{ mailboxId }}-port">{{ 'Port'|trans }}</label><input class="form-control" id="mg-{{ mailboxId }}-port" name="port" type="number" min="1" max="65535" value="{{ params.port }}" required></div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="mg-{{ mailboxId }}-encryption">{{ 'Connection security'|trans }}</label>
|
||||
<select class="form-select" id="mg-{{ mailboxId }}-encryption" name="encryption"><option value="ssl" {% if params.encryption == 'ssl' %}selected{% endif %}>{{ 'Implicit TLS (usually port 993)'|trans }}</option><option value="tls" {% if params.encryption == 'tls' %}selected{% endif %}>{{ 'STARTTLS (usually port 143)'|trans }}</option></select>
|
||||
<small class="form-hint">{{ 'TLS certificate validation is always enabled.'|trans }}</small>
|
||||
</div>
|
||||
<div class="col-md-6"><label class="form-label" for="mg-{{ mailboxId }}-mailbox">{{ 'Mailbox email address'|trans }}</label><input class="form-control" id="mg-{{ mailboxId }}-mailbox" name="mailbox" type="email" value="{{ params.mailbox }}" maxlength="255" required></div>
|
||||
<div class="col-md-6"><label class="form-label" for="mg-{{ mailboxId }}-username">{{ 'IMAP username'|trans }}</label><input class="form-control" id="mg-{{ mailboxId }}-username" name="username" value="{{ params.username }}" maxlength="255" autocomplete="off" required></div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="mg-{{ mailboxId }}-password">{{ 'Mailbox password'|trans }}</label>
|
||||
<input class="form-control" id="mg-{{ mailboxId }}-password" name="password" type="password" autocomplete="new-password" maxlength="4096">
|
||||
<small class="form-hint">{% if params.password_configured %}{{ 'A password is saved. Leave blank to keep it.'|trans }}{% else %}{{ 'No password is saved.'|trans }}{% endif %}</small>
|
||||
<label class="form-check mt-2"><input class="form-check-input" name="clear_password" type="checkbox" value="1"><span class="form-check-label">{{ 'Clear the saved password'|trans }}</span></label>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="mg-{{ mailboxId }}-helpdesk">{{ 'Support helpdesk'|trans }}</label>
|
||||
<select class="form-select" id="mg-{{ mailboxId }}-helpdesk" name="support_helpdesk_id" required>
|
||||
<option value="">{{ 'Choose a helpdesk'|trans }}</option>
|
||||
{% for id, name in result.helpdesks %}<option value="{{ id }}" {% if params.support_helpdesk_id == id or (params.support_helpdesk_id is null and name == 'General') %}selected{% endif %}>{{ name }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% for field, label in {'inbox_folder': 'Inbox folder', 'processed_folder': 'Processed folder', 'ignored_folder': 'Ignored folder', 'failed_folder': 'Failed folder'} %}
|
||||
<div class="col-md-6"><label class="form-label" for="mg-{{ mailboxId }}-{{ field }}">{{ label|trans }}</label><input class="form-control" id="mg-{{ mailboxId }}-{{ field }}" name="{{ field }}" value="{{ attribute(params, field) }}" maxlength="255" required></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="form-hint mt-3">{{ 'Only the configured inbox is imported. Keep spam in a separate folder. Enabled mailboxes are imported automatically by FOSSBilling cron.'|trans }}</p>
|
||||
</div>
|
||||
<div class="card-footer"><button class="btn btn-primary" type="submit">{{ 'Save settings'|trans }}</button></div>
|
||||
</form>
|
||||
{% if mailboxId != 'new' %}
|
||||
<div class="card-footer">
|
||||
<form action="{{ 'mailgateway/mailbox_delete'|api_url(role: 'admin') }}" {{ fb_api_form({message: 'Mailbox removed'|trans, reload: true}) }} data-confirm="{{ 'Remove the saved configuration for %mailbox%? Emails, folders, tickets, and message tracking will be kept.'|trans|replace({'%mailbox%': params.mailbox}) }}" onsubmit="if (!confirm(this.dataset.confirm)) { event.preventDefault(); event.stopImmediatePropagation(); return false; }">
|
||||
<input type="hidden" name="mailbox_id" value="{{ mailboxId }}">
|
||||
<button class="btn btn-outline-danger" type="submit">{{ 'Delete mailbox'|trans }}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button class="btn btn-secondary" type="button" onclick="mailgatewayTest('{{ mailboxId }}')" {% if not result.dependencies.installed|default(false) %}disabled{% endif %}>{{ 'Test Connection and Create Folders'|trans }}</button>
|
||||
<small class="text-secondary">{{ 'Tests saved settings and creates missing destination folders. Save changes first. No mail is imported or moved.'|trans }}</small>
|
||||
<pre class="mt-2 mb-0" style="white-space: pre-wrap" id="mg-result-{{ mailboxId }}" role="status">{% if params.test_result|default(null) %}{{ params.test_result.connected ? 'IMAP login succeeded.'|trans : 'IMAP login failed.'|trans }}
|
||||
{% if params.test_result.move_supported is defined and params.test_result.move_supported %}{{ 'Native MOVE support: available.'|trans }}
|
||||
{% endif %}{% for folder in params.test_result.folders %}{{ folder.path }}: {{ folder.error ?: (folder.created ? 'created'|trans : 'found'|trans) }}
|
||||
{% endfor %}{% for error in params.test_result.errors %}{{ error }}
|
||||
{% endfor %}{{ params.test_result.success ? 'Test passed. You can enable importing and save.'|trans : 'Importing remains disabled until all checks pass.'|trans }}{% endif %}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<details class="card mb-3">
|
||||
<summary class="card-header">{{ 'Advanced'|trans }}</summary>
|
||||
<div class="card-body">
|
||||
<p class="text-secondary">{{ 'Maintenance actions preserve saved mailbox settings and existing ticket data.'|trans }}</p>
|
||||
{% if result.notifications is defined %}
|
||||
<div class="mb-4">
|
||||
<h4>{{ 'Ticket notifications'|trans }}</h4>
|
||||
{% if result.notifications.ready %}
|
||||
<p class="text-success">{{ 'Ticket acknowledgement and enabled staff notification subjects are configured for email replies.'|trans }}</p>
|
||||
{% else %}
|
||||
<ul>{% for error in result.notifications.errors %}<li>{{ error }}</li>{% endfor %}</ul>
|
||||
{% endif %}
|
||||
<p>{{ 'Setup enables the ticket acknowledgement and adds ticket numbers to its subject and enabled staff notification subjects. Existing wording is kept.'|trans }}</p>
|
||||
<form action="{{ 'mailgateway/notifications_setup'|api_url(role: 'admin') }}" {{ fb_api_form({message: 'Ticket notifications configured'|trans, reload: true}) }}>
|
||||
<button class="btn btn-secondary" type="submit">{{ 'Configure ticket notifications'|trans }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
<h4>{{ 'Composer dependencies'|trans }}</h4>
|
||||
{% if result.dependencies.installed|default(false) %}
|
||||
<p class="text-success">{{ 'Mail Gateway dependencies are installed.'|trans }}</p>
|
||||
{% else %}
|
||||
<p>{{ 'Mail Gateway dependencies are not installed.'|trans }}</p>
|
||||
{% endif %}
|
||||
<p>{{ 'Run composer install again using the module lock file, even when dependencies are already installed.'|trans }}</p>
|
||||
{% if result.dependencies is defined and result.dependencies.available %}
|
||||
<form action="{{ 'mailgateway/install_dependencies'|api_url(role: 'admin') }}" {{ fb_api_form({message: 'Composer install completed'|trans, reload: true, timeoutMs: result.dependencies.request_timeout_ms|default(90000), loading: {button: 'Running Composer…'|trans}, preventNavigation: true}) }}>
|
||||
<input type="hidden" name="force" value="1">
|
||||
<button class="btn btn-secondary" type="submit">{{ 'Run composer install'|trans }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{{ 'Web installation is unavailable. Run composer install from the Mailgateway directory using SSH.'|trans }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ 'Module table setup'|trans }}</h4>
|
||||
<p>{{ 'Runs the same setup as php tools/setup.php. Creates missing module tables and adds the default mismatch email template if absent. Existing records and templates are kept. Composer runs separately.'|trans }}</p>
|
||||
<form action="{{ 'mailgateway/setup'|api_url(role: 'admin') }}" {{ fb_api_form({message: 'Mail Gateway table setup completed'|trans, reload: true, loading: {button: 'Running setup…'|trans}}) }}>
|
||||
<button class="btn btn-secondary" type="submit">{{ 'Run table setup'|trans }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<script>
|
||||
(function() {
|
||||
const banner = document.getElementById('mg-notification-banner');
|
||||
if (!banner) return;
|
||||
try {
|
||||
const key = 'mailgateway.notificationErrors:' + location.pathname;
|
||||
if (banner.dataset.ready === '0') {
|
||||
sessionStorage.setItem(key, '1');
|
||||
} else {
|
||||
banner.hidden = sessionStorage.getItem(key) !== '1';
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
} catch (error) { /* The persistent status remains available in Advanced. */ }
|
||||
})();
|
||||
document.querySelectorAll('input[name="mailbox_id"]').forEach(function(hidden) {
|
||||
const form = hidden.form;
|
||||
const id = hidden.value;
|
||||
form.addEventListener('input', function(event) {
|
||||
if (!['host', 'port', 'encryption', 'username', 'password', 'clear_password', 'mailbox', 'inbox_folder', 'processed_folder', 'ignored_folder', 'failed_folder'].includes(event.target.name)) return;
|
||||
const checkbox = document.getElementById('mg-enabled-' + id);
|
||||
checkbox.disabled = true;
|
||||
checkbox.checked = false;
|
||||
const reason = 'Save the changed mailbox settings, then run Test Connection and Create Folders again.';
|
||||
document.getElementById('mg-enable-label-' + id).title = reason;
|
||||
document.getElementById('mg-enable-hint-' + id).textContent = reason;
|
||||
});
|
||||
});
|
||||
function mailgatewayTest(id) {
|
||||
const output = document.getElementById('mg-result-' + id);
|
||||
output.textContent = 'Testing connection and creating missing destination folders…';
|
||||
FOSSBilling.api.admin.post('mailgateway/test_connection', {mailbox_id: id}, function(result) {
|
||||
output.textContent = (result.connected ? 'IMAP login succeeded.' : 'IMAP login failed.') + '\n'
|
||||
+ (result.move_supported ? 'Native MOVE support: available.\n' : '')
|
||||
+ result.folders.map(function(folder) {
|
||||
return folder.path + ': ' + (folder.error || (folder.created ? 'created' : (folder.exists ? 'found' : 'MISSING')));
|
||||
}).concat(result.errors).join('\n') + '\n'
|
||||
+ (result.success ? 'Test passed. You can enable importing and save.' : 'Importing remains disabled until all checks pass.');
|
||||
const checkbox = document.getElementById('mg-enabled-' + id);
|
||||
checkbox.disabled = !result.success;
|
||||
if (!result.success) checkbox.checked = false;
|
||||
const reason = result.success ? '' : 'Fix the reported issues, then run Test Connection and Create Folders again.';
|
||||
document.getElementById('mg-enable-label-' + id).title = reason;
|
||||
document.getElementById('mg-enable-hint-' + id).textContent = reason;
|
||||
}, function(error) { output.textContent = error.message || 'Connection test or folder creation failed.'; });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user