Compare commits

..
8 Commits
Author SHA1 Message Date
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 315df79178 ISSUE-1 (#2)
Co-authored-by: stuzer05 <stuzer05@gmail.com>
Reviewed-on: #2
2022-12-31 18:32:24 +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
10 changed files with 292 additions and 392 deletions

No files matched your search

+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.
+4 -9
View File
@@ -1,12 +1,7 @@
# Roundcube aliasmanager
# Roundcube alias manager extension
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 disabled, copied or deleted from Settings.
![](screenshots/2024-04-11_12-16.png)
## Config
Provides interface to create mailbox aliases. Meant to use dedicated subdomain for email aliases
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)
Aliases are mailboxes used per-service for privacy and spam control
+171 -187
View File
@@ -1,41 +1,53 @@
<?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 $db;
private $postfixadmin_db;
/**
* Initializes the plugin.
*/
public function init() {
$this->rcmail = rcube::get_instance();
$this->load_config();
$this->add_texts('localization/');
$this->include_stylesheet('assets/styles/app.css');
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');
}
$this->add_hook('settings_actions', [$this, 'hookSettingsActions']);
if ($this->rcmail->task == "mail" || $this->rcmail->task == "settings") {
$this->include_stylesheet('assets/styles/app.css');
}
$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']);
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;
}
}
}
public function hookSettingsActions($arg): array {
// add the menu item to the settings sidebar
$arg['actions'][] = [
'action' => 'plugin.aliasmanager',
'class' => 'aliasmanager',
@@ -47,215 +59,187 @@ class aliasmanager extends rcube_plugin {
return $arg;
}
public function onShowSettingsPage() {
/**
* Renders and returns the settings.html view.
* @return mixed
*/
public function settingsPageHandler() {
$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->rcmail->output->add_label("settings_menu_label");
return $this->view("elastic", "aliasmanager.settings", []);
}
/**
* Creates and outputs the settings page.
*/
public function onShowSettingsPage() {
$this->register_handler('plugin.body', [$this, 'settingsPageHandler']);
$this->rcmail->output->set_pagetitle($this->gettext('settings_menu_label'));
$this->rcmail->output->set_pagetitle($this->gettext('aliasmanager'));
$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() {
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),
$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'],
];
}
$this->respond(['alias_list' => $list]);
$this->sendResponse(true, [
'data' => [
'alias_list' => $alias_list,
],
]);
}
public function onAddAlias() {
if (!$this->guard()) {
$email = $_POST['email'] ?? null;
if (empty($email)) {
$this->sendResponse(false, [
'msg' => 'Email not set',
]);
return;
}
$label = $this->normalizeLabel((string)rcube_utils::get_input_value('email', rcube_utils::INPUT_POST));
if ($label === '') {
$this->fail('name_required');
$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;
}
$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');
$this->sendResponse(true, []);
}
public function onToggleAlias() {
if (!$this->guard()) {
$state = $_POST['state'] == 'true' ? 1 : 0;
$email = $_POST['email'];
if (empty($email)) {
$this->sendResponse(false, [
'msg' => 'Email not set',
]);
return;
}
$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;
$email_alias_domain = $this->rcmail->config->get('alias_email_domain');
$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');
$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;
}
$this->respond([]);
$this->sendResponse(true, []);
}
public function onDeleteAlias() {
if (!$this->guard()) {
$email = $_POST['email'];
if (empty($email)) {
$this->sendResponse(false, [
'msg' => 'Email not set',
]);
return;
}
$email = trim((string)rcube_utils::get_input_value('email', rcube_utils::INPUT_POST));
$email_alias_domain = $this->rcmail->config->get('alias_email_domain');
$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');
$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;
}
$this->respond([]);
$this->sendResponse(true, []);
}
/**
* Rejects requests without a valid CSRF token and with a broken configuration.
* 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
*/
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');
private function sendResponse($success, $data = [], $errorMessage = false) {
if ($this->unitTest) {
return ["success" => $success, "data" => $data, "errorMessage" => $data['errorMessage']];
}
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()) {
if (ob_get_contents()) {
@ob_end_clean();
}
header('Content-Type: application/json; charset=UTF-8');
exit(json_encode($payload));
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;
}
}
+74 -114
View File
@@ -1,136 +1,96 @@
$(function() {
const labels = rcmail.env.aliasmanager_labels || {};
function call(action, data, onSuccess) {
data = $.extend({_token: rcmail.env.request_token}, data);
function updateAliasesList() {
$.ajax({
type: 'POST',
url: rcmail.url('plugin.aliasmanager-' + action).replace(/&_unlock=[^&]*/, ''),
data: data,
dataType: 'json',
url: '/?_task=settings&_action=plugin.aliasmanager-get-alias-list',
success: function(response) {
if (response.success) {
onSuccess(response.data || {});
} else {
rcmail.display_message(response.message, 'error');
updateAliasesList();
$('[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);
}
}
},
error: function() {
rcmail.display_message(labels.list_failed, 'error');
}
dataType: 'json'
});
}
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');
$(document).on('click', '.btn-aliasmanager-add-alias', function() {
const email = $('[name=alias_new_name]').val();
if (!/[a-z0-9]/.test(email)) {
return;
}
call('add-alias', {email: name}, function() {
input.val('');
updateAliasesList();
});
}
$.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('');
$(document).on('submit', '#aliasmanager-form', function(e) {
e.preventDefault();
addAlias();
});
updateAliasesList();
}
},
dataType: 'json'
});
})
$(document).on('change', '.btn-aliasmanager-toggle-alias', function() {
call('toggle-alias', {
email: $(this).attr('data-email'),
state: $(this).is(':checked'),
}, 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'
});
})
$(document).on('click', '.btn-aliasmanager-delete-alias', function() {
if (confirm(labels.confirm_delete + '\n' + $(this).attr('data-email'))) {
call('delete-alias', {email: $(this).attr('data-email')}, updateAliasesList);
}
});
const email = $(this).attr('data-email');
$(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'
});
}
});
})
updateAliasesList();
});
// Init
updateAliasesList()
});
+1 -15
View File
@@ -5,18 +5,4 @@
.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,6 +4,4 @@ $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 -23
View File
@@ -1,25 +1,3 @@
<?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';
$labels['settings_menu_label'] = 'Alias manager';
-25
View File
@@ -1,25 +0,0 @@
<?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

+34 -17
View File
@@ -1,33 +1,50 @@
<div id="aliasmanager-settings" class="scroller">
<div id='aliasmanager-settings' class="scroller">
<div class="boxcontent formcontent">
<form id="aliasmanager-form" onsubmit="return false">
<form id="password-form" name="password-form" method="post" action="./?_task=settings&_action=plugin.password-save">
<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="40" maxlength="40" required name="alias_new_name" class="form-control" placeholder="[+service_name+]" type="text">
<input id="alias-new-name" size="60" 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="submit">[+add+]</button>
<button class="button submit btn btn-primary btn-aliasmanager-add-alias" type="button">Add</button>
</td>
</tr>
</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>[+created+]</th>
<th>[+enabled+]</th>
<th>[+action+]</th>
<tr class="form-group row">
<table>
<thead>
<tr>
<th>Alias</th>
<th>Enabled</th>
<th>Action</th>
</tr>
</thead>
<tbody class="alias-list"></tbody>
</table>
</tr>
</thead>
<tbody class="alias-list"></tbody>
</tbody>
</table>
</form>
</div>
</div>
<template id="alias-list-row">
<tr class="form-group">
<td>
<input style="background: transparent;border: none;outline: none;width: 100%; cursor: pointer; font-family: monospace;" readonly type="text" value="{email}" onclick="this.select();">
</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>