Compare commits
12
Commits
d022b17f07
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0795119846
|
||
|
|
86e2db4717
|
||
|
|
f753a8134e | ||
|
|
e73e484718 | ||
|
|
3566ab30c7 | ||
|
|
83329c8ef7 | ||
|
|
a185391f9d | ||
|
|
315df79178 | ||
|
|
47114cfa3b | ||
|
|
870d190bc3 | ||
|
|
9985bcd306 | ||
|
|
d28d0de142 |
No files matched your search
@@ -1 +1,2 @@
|
||||
.idea
|
||||
config.inc.php
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2024 Illya Marchenko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Roundcube alias manager extension
|
||||
|
||||

|
||||
|
||||
Provides interface to create mailbox aliases. Meant to use dedicated subdomain for email aliases
|
||||
|
||||
Aliases are mailboxes used per-service for privacy and spam control
|
||||
|
||||
Lets a user create per-service email aliases (`<service>-<random>@<alias domain>`) that forward to their mailbox. Aliases are stored in the PostfixAdmin `alias` table and can be searched, disabled, copied or deleted from Settings.
|
||||
|
||||
## Config
|
||||
|
||||
Copy `config.inc.php.dist` to `config.inc.php`:
|
||||
|
||||
- `postfixadmin_db_dsn` - PostfixAdmin database DSN
|
||||
- `alias_email_domain` - domain aliases are created in
|
||||
- `alias_email_hash_len` - random suffix length (default 7, minimum 4)
|
||||
- `alias_max_per_user` - alias limit per user (default 100, 0 = unlimited)
|
||||
+189
-170
@@ -1,49 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Per-service email alias manager backed by the PostfixAdmin `alias` table.
|
||||
*
|
||||
* Config (see config.inc.php.dist):
|
||||
* - postfixadmin_db_dsn DSN of the PostfixAdmin database
|
||||
* - alias_email_domain domain the aliases are created in
|
||||
* - alias_email_hash_len length of the random suffix (default 7)
|
||||
* - alias_max_per_user maximum number of aliases per user (default 100, 0 = unlimited)
|
||||
*/
|
||||
class aliasmanager extends rcube_plugin {
|
||||
|
||||
const LABEL_MAX_LEN = 40;
|
||||
|
||||
public $task = 'settings';
|
||||
|
||||
private $rcmail;
|
||||
|
||||
private $postfixadmin_db;
|
||||
private $db;
|
||||
|
||||
/**
|
||||
* Initializes the plugin.
|
||||
*/
|
||||
public function init() {
|
||||
$this->rcmail = rcube::get_instance();
|
||||
$this->load_config();
|
||||
|
||||
if ($dsn = $this->rcmail->config->get('postfixadmin_db_dsn')) {
|
||||
$this->postfixadmin_db = rcube_db::factory($dsn, '', false);
|
||||
} else {
|
||||
throw new \Exception('cannot connect ot postfix db');
|
||||
}
|
||||
|
||||
if ($this->rcmail->task == "settings") {
|
||||
$this->add_hook("settings_actions", [$this, "hookSettingsActions"]);
|
||||
$this->register_action('plugin.aliasmanager', [$this, "onShowSettingsPage"]);
|
||||
|
||||
$this->add_texts('localization/');
|
||||
|
||||
switch ($this->rcmail->action) {
|
||||
case 'plugin.aliasmanager-get-alias-list':
|
||||
$this->onGetAliasList();
|
||||
break;
|
||||
case 'plugin.aliasmanager-add-alias':
|
||||
$this->onAddAlias();
|
||||
break;
|
||||
case 'plugin.aliasmanager-toggle-alias':
|
||||
$this->onToggleAlias();
|
||||
break;
|
||||
case 'plugin.aliasmanager-delete-alias':
|
||||
$this->onDeleteAlias();
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->include_stylesheet('assets/styles/app.css');
|
||||
|
||||
$this->add_hook('settings_actions', [$this, 'hookSettingsActions']);
|
||||
|
||||
$this->register_action('plugin.aliasmanager', [$this, 'onShowSettingsPage']);
|
||||
$this->register_action('plugin.aliasmanager-get-alias-list', [$this, 'onGetAliasList']);
|
||||
$this->register_action('plugin.aliasmanager-add-alias', [$this, 'onAddAlias']);
|
||||
$this->register_action('plugin.aliasmanager-toggle-alias', [$this, 'onToggleAlias']);
|
||||
$this->register_action('plugin.aliasmanager-delete-alias', [$this, 'onDeleteAlias']);
|
||||
}
|
||||
|
||||
public function hookSettingsActions($arg): array {
|
||||
// add the menu item to the settings sidebar
|
||||
$arg['actions'][] = [
|
||||
'action' => 'plugin.aliasmanager',
|
||||
'class' => 'aliasmanager',
|
||||
@@ -55,188 +47,215 @@ class aliasmanager extends rcube_plugin {
|
||||
return $arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders and returns the settings.html view.
|
||||
* @return mixed
|
||||
*/
|
||||
public function settingsPageHandler() {
|
||||
$this->include_script('assets/scripts/app.js');
|
||||
$this->include_stylesheet('assets/styles/app.css');
|
||||
|
||||
$this->rcmail->output->add_label("settings_menu_label");
|
||||
|
||||
return $this->view("elastic", "aliasmanager.settings", []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and outputs the settings page.
|
||||
*/
|
||||
public function onShowSettingsPage() {
|
||||
$this->include_script('assets/scripts/app.js');
|
||||
|
||||
$labels = [];
|
||||
foreach (['add_failed', 'toggle_failed', 'delete_failed', 'list_failed', 'name_required', 'confirm_delete', 'copied', 'copy', 'delete', 'empty', 'no_matches'] as $key) {
|
||||
$labels[$key] = $this->gettext($key);
|
||||
}
|
||||
$this->rcmail->output->set_env('aliasmanager_labels', $labels);
|
||||
|
||||
$this->register_handler('plugin.body', [$this, 'settingsPageHandler']);
|
||||
$this->rcmail->output->set_pagetitle($this->gettext('aliasmanager'));
|
||||
$this->rcmail->output->set_pagetitle($this->gettext('settings_menu_label'));
|
||||
$this->rcmail->output->send('plugin');
|
||||
}
|
||||
|
||||
public function settingsPageHandler() {
|
||||
$html = file_get_contents(__DIR__.'/skins/elastic/templates/settings.html');
|
||||
|
||||
return preg_replace_callback('/\[\+([a-z_]+)\+\]/', function ($m) {
|
||||
return rcube::Q($this->gettext($m[1]));
|
||||
}, $html);
|
||||
}
|
||||
|
||||
public function onGetAliasList() {
|
||||
$alias_list = [];
|
||||
$result = $this->postfixadmin_db->query('SELECT address as email, goto as users, active FROM alias WHERE address != goto AND goto = ? AND domain = ?', $this->rcmail->user->get_username(), $this->rcmail->config->get('alias_email_domain'));
|
||||
foreach ($result as $row) {
|
||||
$alias_list[] = [
|
||||
'email' => $row['email'],
|
||||
'active' => $row['active'],
|
||||
if (!$this->guard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->db()->query(
|
||||
'SELECT address, active, created FROM alias WHERE address != goto AND goto = ? AND domain = ? ORDER BY created DESC, address',
|
||||
$this->username(),
|
||||
$this->aliasDomain()
|
||||
);
|
||||
if ($this->db()->is_error($result)) {
|
||||
$this->fail('list_failed');
|
||||
}
|
||||
|
||||
$list = [];
|
||||
while ($row = $this->db()->fetch_assoc($result)) {
|
||||
$list[] = [
|
||||
'email' => $row['address'],
|
||||
'active' => (int)$row['active'] === 1,
|
||||
'created' => substr($row['created'], 0, 10),
|
||||
];
|
||||
}
|
||||
|
||||
$this->sendResponse(true, [
|
||||
'data' => [
|
||||
'alias_list' => $alias_list,
|
||||
],
|
||||
]);
|
||||
$this->respond(['alias_list' => $list]);
|
||||
}
|
||||
|
||||
public function onAddAlias() {
|
||||
$email = $_POST['email'] ?? null;
|
||||
if (empty($email)) {
|
||||
$this->sendResponse(false, [
|
||||
'msg' => 'Email not set',
|
||||
]);
|
||||
if (!$this->guard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email_alias_domain = $this->rcmail->config->get('alias_email_domain');
|
||||
|
||||
$email = $email.'-'.$this->generateRandomEmailAliasHash($this->rcmail->config->get('alias_email_hash_len', 7)).'@'.$email_alias_domain;
|
||||
|
||||
$error = $this->postfixadmin_db->query('INSERT INTO alias (address, goto, domain, created, modified, active) VALUES (?, ?, ?, NOW(), NOW(), 1)', [$email, $this->rcmail->user->get_username(), $email_alias_domain]);
|
||||
if (!$error) {
|
||||
$this->sendResponse(false, [
|
||||
'msg' => 'failed to add alias', // $this->postfixadmin_db->is_error()
|
||||
]);
|
||||
return;
|
||||
$label = $this->normalizeLabel((string)rcube_utils::get_input_value('email', rcube_utils::INPUT_POST));
|
||||
if ($label === '') {
|
||||
$this->fail('name_required');
|
||||
}
|
||||
|
||||
$this->sendResponse(true, []);
|
||||
$domain = $this->aliasDomain();
|
||||
|
||||
$max = (int)$this->rcmail->config->get('alias_max_per_user', 100);
|
||||
if ($max > 0 && $this->countAliases() >= $max) {
|
||||
$this->fail('limit_reached');
|
||||
}
|
||||
|
||||
$hash_len = max(4, (int)$this->rcmail->config->get('alias_email_hash_len', 7));
|
||||
|
||||
// retry on the (unlikely) address collision
|
||||
for ($attempt = 0; $attempt < 5; $attempt++) {
|
||||
$email = $label.'-'.$this->randomHash($hash_len).'@'.$domain;
|
||||
|
||||
$exists = $this->db()->query('SELECT 1 FROM alias WHERE address = ?', $email);
|
||||
if ($this->db()->fetch_array($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->db()->query(
|
||||
'INSERT INTO alias (address, goto, domain, created, modified, active) VALUES (?, ?, ?, NOW(), NOW(), 1)',
|
||||
$email,
|
||||
$this->username(),
|
||||
$domain
|
||||
);
|
||||
if ($this->db()->affected_rows($result) < 1) {
|
||||
$this->fail('add_failed');
|
||||
}
|
||||
|
||||
$this->respond(['email' => $email]);
|
||||
}
|
||||
|
||||
$this->fail('add_failed');
|
||||
}
|
||||
|
||||
public function onToggleAlias() {
|
||||
$state = $_POST['state'] == 'true' ? 1 : 0;
|
||||
$email = $_POST['email'];
|
||||
|
||||
if (empty($email)) {
|
||||
$this->sendResponse(false, [
|
||||
'msg' => 'Email not set',
|
||||
]);
|
||||
if (!$this->guard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email_alias_domain = $this->rcmail->config->get('alias_email_domain');
|
||||
$email = trim((string)rcube_utils::get_input_value('email', rcube_utils::INPUT_POST));
|
||||
$state = rcube_utils::get_input_value('state', rcube_utils::INPUT_POST) === 'true' ? 1 : 0;
|
||||
|
||||
$error = $this->postfixadmin_db->query('UPDATE alias SET active = ? WHERE address = ? AND goto = ? AND domain = ?', [$state, $email, $this->rcmail->user->get_username(), $email_alias_domain]);
|
||||
if (!$error) {
|
||||
$this->sendResponse(false, [
|
||||
'msg' => 'failed to toggle alias', // $this->postfixadmin_db->is_error()
|
||||
]);
|
||||
return;
|
||||
$result = $this->db()->query(
|
||||
'UPDATE alias SET active = ?, modified = NOW() WHERE address = ? AND goto = ? AND domain = ? AND address != goto',
|
||||
$state,
|
||||
$email,
|
||||
$this->username(),
|
||||
$this->aliasDomain()
|
||||
);
|
||||
if ($this->db()->is_error($result)) {
|
||||
$this->fail('toggle_failed');
|
||||
}
|
||||
|
||||
$this->sendResponse(true, []);
|
||||
$this->respond([]);
|
||||
}
|
||||
|
||||
public function onDeleteAlias() {
|
||||
$email = $_POST['email'];
|
||||
if (empty($email)) {
|
||||
$this->sendResponse(false, [
|
||||
'msg' => 'Email not set',
|
||||
]);
|
||||
if (!$this->guard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email_alias_domain = $this->rcmail->config->get('alias_email_domain');
|
||||
$email = trim((string)rcube_utils::get_input_value('email', rcube_utils::INPUT_POST));
|
||||
|
||||
$error = $this->postfixadmin_db->query('DELETE FROM alias WHERE address = ? AND goto = ? AND domain = ?', [$email, $this->rcmail->user->get_username(), $email_alias_domain]);
|
||||
if (!$error) {
|
||||
$this->sendResponse(false, [
|
||||
'msg' => 'failed to toggle alias', // $this->postfixadmin_db->is_error()
|
||||
]);
|
||||
return;
|
||||
$result = $this->db()->query(
|
||||
'DELETE FROM alias WHERE address = ? AND goto = ? AND domain = ? AND address != goto',
|
||||
$email,
|
||||
$this->username(),
|
||||
$this->aliasDomain()
|
||||
);
|
||||
if ($this->db()->is_error($result) || $this->db()->affected_rows($result) < 1) {
|
||||
$this->fail('delete_failed');
|
||||
}
|
||||
|
||||
$this->sendResponse(true, []);
|
||||
$this->respond([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends ajax response in json format.
|
||||
*
|
||||
* IMPORTANT: When sending an error with an error message, use this format:
|
||||
* sendResponse(true, array('success' => false, 'errorMessage' => $message, 'other data'...)
|
||||
* This is because the standard way of setting $success and $errorMessage won't work properly with non-English
|
||||
* character sets (when the error is sent using http/1.0 500)
|
||||
*
|
||||
* @param bool $success
|
||||
* @param array $data
|
||||
* Rejects requests without a valid CSRF token and with a broken configuration.
|
||||
*/
|
||||
private function sendResponse($success, $data = [], $errorMessage = false) {
|
||||
if ($this->unitTest) {
|
||||
return ["success" => $success, "data" => $data, "errorMessage" => $data['errorMessage']];
|
||||
private function guard(): bool {
|
||||
if (!$this->rcmail->check_request(rcube_utils::INPUT_POST)) {
|
||||
$this->fail('request_invalid');
|
||||
}
|
||||
if ($this->aliasDomain() === '' || !$this->db()) {
|
||||
$this->fail('not_configured');
|
||||
}
|
||||
|
||||
if (ob_get_contents()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private function db() {
|
||||
if ($this->db === null) {
|
||||
$dsn = $this->rcmail->config->get('postfixadmin_db_dsn');
|
||||
$this->db = $dsn ? rcube_db::factory($dsn, '', false) : false;
|
||||
}
|
||||
|
||||
return $this->db;
|
||||
}
|
||||
|
||||
private function username(): string {
|
||||
return $this->rcmail->user->get_username();
|
||||
}
|
||||
|
||||
private function aliasDomain(): string {
|
||||
return (string)$this->rcmail->config->get('alias_email_domain', '');
|
||||
}
|
||||
|
||||
private function countAliases(): int {
|
||||
$result = $this->db()->query('SELECT COUNT(*) FROM alias WHERE address != goto AND goto = ? AND domain = ?', $this->username(), $this->aliasDomain());
|
||||
$row = $this->db()->fetch_array($result);
|
||||
|
||||
return (int)($row[0] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces user input to a safe email local part: [a-z0-9._-], at most LABEL_MAX_LEN chars.
|
||||
*/
|
||||
private function normalizeLabel(string $input): string {
|
||||
$label = strtolower(trim($input));
|
||||
$label = preg_replace('/[^a-z0-9._-]+/', '-', $label);
|
||||
$label = preg_replace('/([._-])\1+/', '$1', $label);
|
||||
$label = substr($label, 0, self::LABEL_MAX_LEN);
|
||||
|
||||
return trim($label, '._-');
|
||||
}
|
||||
|
||||
private function randomHash(int $length): string {
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
$max = strlen($characters) - 1;
|
||||
$hash = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$hash .= $characters[random_int(0, $max)];
|
||||
}
|
||||
|
||||
return $hash;
|
||||
}
|
||||
|
||||
private function respond(array $data) {
|
||||
$this->send(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
private function fail(string $label) {
|
||||
$this->send(['success' => false, 'message' => $this->gettext($label)]);
|
||||
}
|
||||
|
||||
private function send(array $payload) {
|
||||
if (ob_get_length()) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
|
||||
if (!is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
if (!isset($data['success'])) {
|
||||
$data['success'] = (bool)$success;
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
exit(json_encode($data));
|
||||
}
|
||||
|
||||
if (empty($errorMessage)) {
|
||||
$errorMessage = empty($data['errorMessage']) ? "Server error" : $data['errorMessage'];
|
||||
}
|
||||
|
||||
exit(@header("HTTP/1.0 500 ".$errorMessage));
|
||||
}
|
||||
|
||||
private function view($skin, $view, $data = false) {
|
||||
if (empty($data) || !is_array($data)) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$parts = explode(".", $view);
|
||||
$plugin = $parts[0];
|
||||
|
||||
unset($parts[0]);
|
||||
$html = file_get_contents(__DIR__."/../$plugin/skins/$skin/templates/".implode(".", $parts).".html");
|
||||
|
||||
while (($i = strrpos($html, "[+")) !== false && ($j = strrpos($html, "+]")) !== false) {
|
||||
$html = substr_replace($html, xrc()->gettext(substr($html, $i + 2, $j - $i - 2)), $i, $j - $i + 2);
|
||||
}
|
||||
|
||||
// replace our custom tags that can contain html tags
|
||||
foreach ($data as $key => $val) {
|
||||
if (is_string($val)) {
|
||||
$html = str_replace("[~".$key."~]", $val, $html);
|
||||
} else if (is_array($val)) {
|
||||
$html = str_replace("[~".$key."~]", @json_encode($val), $html);
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function generateRandomEmailAliasHash($length = 10) {
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
$charactersLength = strlen($characters);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[rand(0, $charactersLength - 1)];
|
||||
}
|
||||
return $randomString;
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
exit(json_encode($payload));
|
||||
}
|
||||
}
|
||||
+113
-73
@@ -1,96 +1,136 @@
|
||||
$(function() {
|
||||
function updateAliasesList() {
|
||||
const labels = rcmail.env.aliasmanager_labels || {};
|
||||
|
||||
function call(action, data, onSuccess) {
|
||||
data = $.extend({_token: rcmail.env.request_token}, data);
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/?_task=settings&_action=plugin.aliasmanager-get-alias-list',
|
||||
url: rcmail.url('plugin.aliasmanager-' + action).replace(/&_unlock=[^&]*/, ''),
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('[name=alias_new_name]').val('');
|
||||
|
||||
// Clear table
|
||||
$('.alias-list').html('');
|
||||
|
||||
// Make template
|
||||
let template = $('template#alias-list-row').html();
|
||||
|
||||
for (let i = 0; i < response.data.alias_list.length; i++) {
|
||||
const row = response.data.alias_list[i];
|
||||
|
||||
let tpl = template.replaceAll('{i}', i);
|
||||
tpl = tpl.replaceAll('{email}', row.email);
|
||||
tpl = tpl.replaceAll('{active}', row.active == 1);
|
||||
tpl = tpl.replaceAll('{checked}', row.active == 1 ? 'checked' : '');
|
||||
$('.alias-list').append(tpl);
|
||||
}
|
||||
onSuccess(response.data || {});
|
||||
} else {
|
||||
rcmail.display_message(response.message, 'error');
|
||||
updateAliasesList();
|
||||
}
|
||||
},
|
||||
dataType: 'json'
|
||||
error: function() {
|
||||
rcmail.display_message(labels.list_failed, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '.btn-aliasmanager-add-alias', function() {
|
||||
const email = $('[name=alias_new_name]').val();
|
||||
if (!/[a-z0-9]/.test(email)) {
|
||||
function renderRow(row, i) {
|
||||
const id = 'toggle-active-' + i;
|
||||
|
||||
const toggle = $('<input type="checkbox" class="form-check-input custom-control-input btn-aliasmanager-toggle-alias">')
|
||||
.attr('id', id)
|
||||
.attr('data-email', row.email)
|
||||
.prop('checked', row.active);
|
||||
|
||||
const copy = $('<button type="button" class="button btn-sm btn-secondary btn-aliasmanager-copy-alias"></button>')
|
||||
.attr('data-email', row.email)
|
||||
.text(labels.copy);
|
||||
|
||||
const del = $('<button type="button" class="button btn-sm btn-danger btn-aliasmanager-delete-alias"></button>')
|
||||
.attr('data-email', row.email)
|
||||
.text(labels.delete);
|
||||
|
||||
return $('<tr class="form-group"></tr>')
|
||||
.append($('<td></td>').append($('<span class="alias-email"></span>').text(row.email)))
|
||||
.append($('<td></td>').text(row.created))
|
||||
.append($('<td></td>').append(
|
||||
$('<div class="custom-control custom-switch"></div>')
|
||||
.append(toggle)
|
||||
.append($('<label class="custom-control-label"></label>').attr('for', id))
|
||||
))
|
||||
.append($('<td></td>').append(copy, ' ', del));
|
||||
}
|
||||
|
||||
function updateAliasesList() {
|
||||
call('get-alias-list', {}, function(data) {
|
||||
const list = $('.alias-list').empty();
|
||||
|
||||
data.alias_list.forEach(function(row, i) {
|
||||
list.append(renderRow(row, i));
|
||||
});
|
||||
|
||||
list.append($('<tr class="alias-empty-row"><td colspan="4" class="alias-empty"></td></tr>'));
|
||||
applyFilter();
|
||||
});
|
||||
}
|
||||
|
||||
// Hides rows that do not match the search query, shows a placeholder row when nothing is left
|
||||
function applyFilter() {
|
||||
const query = $('#alias-search').val().trim().toLowerCase();
|
||||
const rows = $('.alias-list tr').not('.alias-empty-row');
|
||||
let visible = 0;
|
||||
|
||||
rows.each(function() {
|
||||
const match = !query || $(this).find('.alias-email').text().toLowerCase().indexOf(query) !== -1;
|
||||
$(this).toggle(match);
|
||||
if (match) {
|
||||
visible++;
|
||||
}
|
||||
});
|
||||
|
||||
$('.alias-empty-row')
|
||||
.toggle(visible === 0)
|
||||
.find('td')
|
||||
.text(rows.length ? labels.no_matches : labels.empty);
|
||||
}
|
||||
|
||||
let searchTimer;
|
||||
$(document).on('input', '#alias-search', function() {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(applyFilter, 250);
|
||||
});
|
||||
|
||||
function addAlias() {
|
||||
const input = $('[name=alias_new_name]');
|
||||
const name = input.val().trim();
|
||||
|
||||
if (!/[a-z0-9]/i.test(name)) {
|
||||
rcmail.display_message(labels.name_required, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/?_task=settings&_action=plugin.aliasmanager-add-alias',
|
||||
data: {
|
||||
email: email,
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('[name=alias_new_name]').val('');
|
||||
|
||||
call('add-alias', {email: name}, function() {
|
||||
input.val('');
|
||||
updateAliasesList();
|
||||
}
|
||||
},
|
||||
dataType: 'json'
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$(document).on('submit', '#aliasmanager-form', function(e) {
|
||||
e.preventDefault();
|
||||
addAlias();
|
||||
});
|
||||
|
||||
$(document).on('change', '.btn-aliasmanager-toggle-alias', function() {
|
||||
const email = $(this).attr('data-email');
|
||||
const isOn = $(this).is(':checked');
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/?_task=settings&_action=plugin.aliasmanager-toggle-alias',
|
||||
data: {
|
||||
state: isOn,
|
||||
email: email,
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
updateAliasesList();
|
||||
}
|
||||
},
|
||||
dataType: 'json'
|
||||
call('toggle-alias', {
|
||||
email: $(this).attr('data-email'),
|
||||
state: $(this).is(':checked'),
|
||||
}, function() {});
|
||||
});
|
||||
})
|
||||
|
||||
$(document).on('click', '.btn-aliasmanager-delete-alias', function() {
|
||||
const email = $(this).attr('data-email');
|
||||
if (confirm(labels.confirm_delete + '\n' + $(this).attr('data-email'))) {
|
||||
call('delete-alias', {email: $(this).attr('data-email')}, updateAliasesList);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-aliasmanager-copy-alias', function() {
|
||||
const text = $(this).attr('data-email');
|
||||
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
rcmail.display_message(labels.copied, 'confirmation');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (confirm('You really want to delete ' + email + '?')) {
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/?_task=settings&_action=plugin.aliasmanager-delete-alias',
|
||||
data: {
|
||||
email: email,
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
updateAliasesList();
|
||||
}
|
||||
},
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
// Init
|
||||
updateAliasesList()
|
||||
});
|
||||
@@ -1,3 +1,22 @@
|
||||
.scroller {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.aliasmanager :before {
|
||||
font-size: 1.1em !important;
|
||||
content: "\f02c" !important;
|
||||
}
|
||||
|
||||
.aliasmanager-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aliasmanager-table .alias-empty {
|
||||
text-align: center;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.aliasmanager-search {
|
||||
margin: 1rem 0;
|
||||
max-width: 30rem;
|
||||
}
|
||||
@@ -4,4 +4,6 @@ $config['alias_email_domain'] = 'social.mail.example.com';
|
||||
|
||||
$config['alias_email_hash_len'] = 7;
|
||||
|
||||
$config['alias_max_per_user'] = 100;
|
||||
|
||||
$config['postfixadmin_db_dsn'] = 'mysql://user:pass@host/db';
|
||||
@@ -1,3 +1,25 @@
|
||||
<?php
|
||||
|
||||
$labels['settings_menu_label'] = 'Alias manager';
|
||||
$labels['new_alias_for'] = 'New alias for';
|
||||
$labels['service_name'] = 'Service name..';
|
||||
$labels['add'] = 'Add';
|
||||
$labels['alias'] = 'Alias';
|
||||
$labels['created'] = 'Created';
|
||||
$labels['enabled'] = 'Enabled';
|
||||
$labels['action'] = 'Action';
|
||||
$labels['delete'] = 'Delete';
|
||||
$labels['copy'] = 'Copy';
|
||||
$labels['copied'] = 'Copied to clipboard';
|
||||
$labels['empty'] = 'No aliases yet';
|
||||
$labels['confirm_delete'] = 'Delete this alias? Mail sent to it will no longer be delivered.';
|
||||
$labels['name_required'] = 'Enter a service name (letters and digits)';
|
||||
$labels['limit_reached'] = 'Alias limit reached';
|
||||
$labels['add_failed'] = 'Failed to add alias';
|
||||
$labels['toggle_failed'] = 'Failed to change alias state';
|
||||
$labels['delete_failed'] = 'Failed to delete alias';
|
||||
$labels['list_failed'] = 'Failed to load aliases';
|
||||
$labels['request_invalid'] = 'Invalid request, reload the page';
|
||||
$labels['not_configured'] = 'Alias manager is not configured';
|
||||
$labels['search'] = 'Search aliases..';
|
||||
$labels['no_matches'] = 'No matching aliases';
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
$labels['settings_menu_label'] = 'Алиасы';
|
||||
$labels['new_alias_for'] = 'Новый алиас для';
|
||||
$labels['service_name'] = 'Название сервиса..';
|
||||
$labels['add'] = 'Добавить';
|
||||
$labels['alias'] = 'Алиас';
|
||||
$labels['created'] = 'Создан';
|
||||
$labels['enabled'] = 'Включён';
|
||||
$labels['action'] = 'Действие';
|
||||
$labels['delete'] = 'Удалить';
|
||||
$labels['copy'] = 'Копировать';
|
||||
$labels['copied'] = 'Скопировано в буфер обмена';
|
||||
$labels['empty'] = 'Алиасов пока нет';
|
||||
$labels['confirm_delete'] = 'Удалить алиас? Письма на него перестанут доставляться.';
|
||||
$labels['name_required'] = 'Введите название сервиса (буквы и цифры)';
|
||||
$labels['limit_reached'] = 'Достигнут лимит алиасов';
|
||||
$labels['add_failed'] = 'Не удалось добавить алиас';
|
||||
$labels['toggle_failed'] = 'Не удалось изменить состояние алиаса';
|
||||
$labels['delete_failed'] = 'Не удалось удалить алиас';
|
||||
$labels['list_failed'] = 'Не удалось загрузить алиасы';
|
||||
$labels['request_invalid'] = 'Некорректный запрос, перезагрузите страницу';
|
||||
$labels['not_configured'] = 'Менеджер алиасов не настроен';
|
||||
$labels['search'] = 'Поиск алиасов..';
|
||||
$labels['no_matches'] = 'Ничего не найдено';
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -1,50 +1,33 @@
|
||||
<div id='aliasmanager-settings' class="scroller">
|
||||
<div id="aliasmanager-settings" class="scroller">
|
||||
<div class="boxcontent formcontent">
|
||||
<form id="password-form" name="password-form" method="post" action="./?_task=settings&_action=plugin.password-save">
|
||||
<form id="aliasmanager-form" onsubmit="return false">
|
||||
<table class="propform">
|
||||
<tbody>
|
||||
<tr class="form-group row">
|
||||
<td class="title col-sm-2">
|
||||
<label for="alias-new-name" class="col-form-label">New alias for</label>
|
||||
<label for="alias-new-name" class="col-form-label">[+new_alias_for+]</label>
|
||||
</td>
|
||||
<td class="col-sm-2">
|
||||
<input id="alias-new-name" size="60" required name="alias_new_name" class="form-control" placeholder="Service name.." type="text">
|
||||
<input id="alias-new-name" size="40" maxlength="40" required name="alias_new_name" class="form-control" placeholder="[+service_name+]" type="text">
|
||||
</td>
|
||||
<td class="col-sm-1">
|
||||
<button class="button submit btn btn-primary btn-aliasmanager-add-alias" type="button">Add</button>
|
||||
<button class="button submit btn btn-primary btn-aliasmanager-add-alias" type="submit">[+add+]</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="form-group row">
|
||||
<table>
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="search" id="alias-search" class="form-control aliasmanager-search" placeholder="[+search+]" autocomplete="off">
|
||||
<table class="records-table aliasmanager-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alias</th>
|
||||
<th>Enabled</th>
|
||||
<th>Action</th>
|
||||
<th>[+alias+]</th>
|
||||
<th>[+created+]</th>
|
||||
<th>[+enabled+]</th>
|
||||
<th>[+action+]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="alias-list"></tbody>
|
||||
</table>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="alias-list-row">
|
||||
<tr class="form-group">
|
||||
<td>
|
||||
<span>{email}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" id="toggle-active-{i}" value="{active}" {checked} data-email="{email}" class="form-check-input custom-control-input btn-aliasmanager-toggle-alias">
|
||||
<label for="toggle-active-{i}" class="custom-control-label" title=""></label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="button btn-sm btn-danger btn-aliasmanager-delete-alias" data-email="{email}" type="button">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
Reference in new issue
Block a user