Compare commits

...
12 Commits
Author SHA1 Message Date
stuzer05 0795119846 Merge origin/master 2026-10-01 16:09:31 +03:00
stuzer05 86e2db4717 Harden aliasmanager, add live search, copy, ru_RU localization 2026-10-01 16:06:58 +03:00
stuzer05 f753a8134e Add LICENSE 2024-04-11 12:21:02 +03:00
stuzer05 e73e484718 Add README.md 2024-04-11 12:20:30 +03:00
stuzer05 3566ab30c7 Upload files to "screenshots" 2024-04-11 12:17:04 +03:00
stuzer05 83329c8ef7 ISSUE-1 (#3)
Co-authored-by: stuzer05 <stuzer05@gmail.com>
Reviewed-on: #3
2022-12-31 18:36:12 +02:00
stuzer05 a185391f9d Commit 2022-12-31 18:35:57 +02:00
stuzer05 315df79178 ISSUE-1 (#2)
Co-authored-by: stuzer05 <stuzer05@gmail.com>
Reviewed-on: #2
2022-12-31 18:32:24 +02:00
stuzer05 47114cfa3b Commit 2022-12-31 18:32:10 +02:00
stuzer05 870d190bc3 Update 'skins/elastic/templates/settings.html' 2022-11-09 16:06:05 +02:00
stuzer05 9985bcd306 Update 'skins/elastic/templates/settings.html' 2022-11-09 16:05:08 +02:00
stuzer05 d28d0de142 Update 'skins/elastic/templates/settings.html' 2022-11-09 16:03:09 +02:00
11 changed files with 407 additions and 271 deletions

No files matched your search

+1
View File
@@ -1 +1,2 @@
.idea
config.inc.php
+7
View File
@@ -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.
+18
View File
@@ -0,0 +1,18 @@
# Roundcube alias manager extension
![](screenshots/2024-04-11_12-16.png)
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
View File
@@ -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));
}
}
+111 -71
View File
@@ -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('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();
if (confirm(labels.confirm_delete + '\n' + $(this).attr('data-email'))) {
call('delete-alias', {email: $(this).attr('data-email')}, updateAliasesList);
}
},
dataType: 'json'
});
$(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');
});
}
})
});
// Init
updateAliasesList()
updateAliasesList();
});
+19
View File
@@ -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;
}
+2
View File
@@ -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';
+22
View File
@@ -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';
+25
View File
@@ -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

+13 -30
View File
@@ -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>