Harden aliasmanager, add live search, copy, ru_RU localization
This commit is contained in:
8 files changed
+395
-281
No files matched your search
+187
-171
@@ -1,53 +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();
|
||||
$this->add_texts('localization/');
|
||||
|
||||
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->include_stylesheet('assets/styles/app.css');
|
||||
|
||||
if ($this->rcmail->task == "mail" || $this->rcmail->task == "settings") {
|
||||
$this->include_stylesheet('assets/styles/app.css');
|
||||
}
|
||||
$this->add_hook('settings_actions', [$this, 'hookSettingsActions']);
|
||||
|
||||
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->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',
|
||||
@@ -59,187 +47,215 @@ class aliasmanager extends rcube_plugin {
|
||||
return $arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders and returns the settings.html view.
|
||||
* @return mixed
|
||||
*/
|
||||
public function settingsPageHandler() {
|
||||
public function onShowSettingsPage() {
|
||||
$this->include_script('assets/scripts/app.js');
|
||||
|
||||
$this->rcmail->output->add_label("settings_menu_label");
|
||||
$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);
|
||||
|
||||
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('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));
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
exit(json_encode($payload));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user