From 6ea4aee0a73ab6e52029c891b593f1392ba930e1 Mon Sep 17 00:00:00 2001 From: Laurent Dinclaux Date: Tue, 10 Feb 2026 11:05:01 +1100 Subject: [PATCH] Add managesieve (Sieve) support for remote accounts When switching identities, the managesieve plugin still connected to the default sieve server. Hook into managesieve_connect to redirect the sieve connection to the remote account's server. Adds sieve_host, sieve_port and sieve_auth columns to the database, a Sieve section to the identity settings form, preconfig support for sieve_host, and localized labels for all 7 languages. --- SQL/mysql.initial.sql | 9 ++++++ SQL/mysql/2026021001.sql | 15 +++++++++ SQL/postgres.initial.sql | 9 ++++++ SQL/postgres/2026021001.sql | 13 ++++++++ SQL/sqlite.initial.sql | 9 ++++++ SQL/sqlite/2026021001.sql | 13 ++++++++ config.inc.php.dist | 5 +++ ident_switch-form.js | 3 ++ ident_switch.php | 18 +++++++++++ lib/IdentSwitchForm.php | 61 ++++++++++++++++++++++++++++++++++-- lib/IdentSwitchPreconfig.php | 16 ++++++++++ lib/IdentSwitchSwitcher.php | 45 ++++++++++++++++++++++++++ localization/de_DE.inc | 19 +++++++++++ localization/en_US.inc | 19 +++++++++++ localization/fr_FR.inc | 19 +++++++++++ localization/it_IT.inc | 19 +++++++++++ localization/nl_NL.inc | 19 +++++++++++ localization/ru_RU.inc | 19 +++++++++++ localization/sl_SI.inc | 19 +++++++++++ 19 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 SQL/mysql/2026021001.sql create mode 100644 SQL/postgres/2026021001.sql create mode 100644 SQL/sqlite/2026021001.sql diff --git a/SQL/mysql.initial.sql b/SQL/mysql.initial.sql index 99370ba..4cc5f1d 100644 --- a/SQL/mysql.initial.sql +++ b/SQL/mysql.initial.sql @@ -37,6 +37,15 @@ CREATE TABLE IF NOT EXISTS `ident_switch` smallint NOT NULL DEFAULT 1, + `sieve_host` + varchar(64), + `sieve_port` + int + CHECK(`sieve_port` > 0 AND `sieve_port` <= 65535), + `sieve_auth` + smallint + NOT NULL + DEFAULT 1, `drafts_mbox` varchar(64), `sent_mbox` diff --git a/SQL/mysql/2026021001.sql b/SQL/mysql/2026021001.sql new file mode 100644 index 0000000..6113d76 --- /dev/null +++ b/SQL/mysql/2026021001.sql @@ -0,0 +1,15 @@ +ALTER TABLE `ident_switch` + ADD `sieve_host` varchar(64) AFTER `smtp_auth`; + +ALTER TABLE `ident_switch` + ADD `sieve_port` + int + CHECK(`sieve_port` > 0 AND `sieve_port` <= 65535) + AFTER `sieve_host`; + +ALTER TABLE `ident_switch` + ADD `sieve_auth` + smallint + NOT NULL + DEFAULT 1 + AFTER `sieve_port`; diff --git a/SQL/postgres.initial.sql b/SQL/postgres.initial.sql index d810bbc..6e5f36b 100644 --- a/SQL/postgres.initial.sql +++ b/SQL/postgres.initial.sql @@ -38,6 +38,15 @@ CREATE TABLE ident_switch smallint NOT NULL DEFAULT(1), + sieve_host + varchar(64), + sieve_port + integer + CHECK(sieve_port > 0 AND sieve_port <= 65535), + sieve_auth + smallint + NOT NULL + DEFAULT(1), drafts_mbox varchar(64), sent_mbox diff --git a/SQL/postgres/2026021001.sql b/SQL/postgres/2026021001.sql new file mode 100644 index 0000000..e1e235a --- /dev/null +++ b/SQL/postgres/2026021001.sql @@ -0,0 +1,13 @@ +ALTER TABLE ident_switch + ADD sieve_host varchar(64); + +ALTER TABLE ident_switch + ADD sieve_port + integer + CHECK(sieve_port > 0 AND sieve_port <= 65535); + +ALTER TABLE ident_switch + ADD sieve_auth + smallint + NOT NULL + DEFAULT(1); diff --git a/SQL/sqlite.initial.sql b/SQL/sqlite.initial.sql index 3b5b051..168c0e1 100644 --- a/SQL/sqlite.initial.sql +++ b/SQL/sqlite.initial.sql @@ -38,6 +38,15 @@ CREATE TABLE ident_switch smallint NOT NULL DEFAULT 1, + sieve_host + varchar(64), + sieve_port + integer + CHECK(sieve_port > 0 AND sieve_port <= 65535), + sieve_auth + smallint + NOT NULL + DEFAULT 1, drafts_mbox varchar(64), sent_mbox diff --git a/SQL/sqlite/2026021001.sql b/SQL/sqlite/2026021001.sql new file mode 100644 index 0000000..75a35ad --- /dev/null +++ b/SQL/sqlite/2026021001.sql @@ -0,0 +1,13 @@ +ALTER TABLE ident_switch + ADD sieve_host varchar(64); + +ALTER TABLE ident_switch + ADD sieve_port + integer + CHECK(sieve_port > 0 AND sieve_port <= 65535); + +ALTER TABLE ident_switch + ADD sieve_auth + smallint + NOT NULL + DEFAULT 1; diff --git a/config.inc.php.dist b/config.inc.php.dist index 483d2b6..1305586 100644 --- a/config.inc.php.dist +++ b/config.inc.php.dist @@ -30,6 +30,11 @@ $config['ident_switch.preconfig'] = [ // Falls back to imap_host if not specified. 'smtp_host' => 'tls://mail.domain.tld:587', + // Sieve (managesieve) connection: scheme://host:port + // Schemes: tls:// for STARTTLS (port 4190), ssl:// for implicit TLS. + // Optional. Only needed if the managesieve plugin is installed. + 'sieve_host' => 'tls://mail.domain.tld:4190', + // Login name, can be 'email' (full address from identity), 'mbox' (only mailbox part). // Any other value is treated as 'not specified' (default). 'user' => 'email', diff --git a/ident_switch-form.js b/ident_switch-form.js index 9a98574..e1492d1 100644 --- a/ident_switch-form.js +++ b/ident_switch-form.js @@ -26,6 +26,9 @@ function plugin_switchIdent_processPreconfig() { $("INPUT[name='_ident_switch.form.smtp.host']").prop("disabled", true); $("INPUT[name='_ident_switch.form.smtp.port']").prop("disabled", true); + + $("INPUT[name='_ident_switch.form.sieve.host']").prop("disabled", true); + $("INPUT[name='_ident_switch.form.sieve.port']").prop("disabled", true); } if (2 == disVal) { $("INPUT[name='_ident_switch.form.imap.username']").prop("disabled", true); diff --git a/ident_switch.php b/ident_switch.php index 4475161..da24bbd 100644 --- a/ident_switch.php +++ b/ident_switch.php @@ -41,6 +41,12 @@ class ident_switch extends rcube_plugin /** @var int SMTP authentication: no authentication required. */ public const SMTP_AUTH_NONE = 2; + /** @var int Sieve authentication: use same credentials as IMAP. */ + public const SIEVE_AUTH_IMAP = 1; + + /** @var int Sieve authentication: no authentication required. */ + public const SIEVE_AUTH_NONE = 2; + private IdentSwitchForm $form; private IdentSwitchSwitcher $switcher; private IdentSwitchPreconfig $preconfig; @@ -57,6 +63,7 @@ class ident_switch extends rcube_plugin $this->add_hook('startup', [$this, 'on_startup']); $this->add_hook('render_page', [$this, 'on_render_page']); $this->add_hook('smtp_connect', [$this, 'on_smtp_connect']); + $this->add_hook('managesieve_connect', [$this, 'on_managesieve_connect']); $this->add_hook('identity_form', [$this, 'on_identity_form']); $this->add_hook('identity_update', [$this, 'on_identity_update']); $this->add_hook('identity_create', [$this, 'on_identity_create']); @@ -205,6 +212,17 @@ class ident_switch extends rcube_plugin return $this->switcher->configure_smtp($args); } + /** + * Handle managesieve_connect hook: configure Sieve settings for the active account. + * + * @param array $args Hook arguments containing Sieve connection parameters. + * @return array Modified hook arguments with updated Sieve settings. + */ + public function on_managesieve_connect(array $args): array + { + return $this->switcher->configure_managesieve($args); + } + /** * Handle identity_form hook: add plugin-specific fields to the identity editor. * diff --git a/lib/IdentSwitchForm.php b/lib/IdentSwitchForm.php index 840fdaa..5b209a5 100644 --- a/lib/IdentSwitchForm.php +++ b/lib/IdentSwitchForm.php @@ -79,11 +79,32 @@ class IdentSwitchForm ]; } + /** + * Build the Sieve (managesieve) form fields for identity settings. + * + * @param array $record Identity record data used for default auth type selection. + * @return array Form field definitions for Sieve host, port, and auth type. + */ + public function get_sieve_fields(array &$record): array + { + $prefix = 'ident_switch.form.sieve.'; + + $authType = new html_select(['name' => "_{$prefix}auth"]); + $authType->add($this->plugin->gettext('form.sieve.auth.imap'), ident_switch::SIEVE_AUTH_IMAP); + $authType->add($this->plugin->gettext('form.sieve.auth.none'), ident_switch::SIEVE_AUTH_NONE); + + return [ + $prefix . 'host' => ['type' => 'text', 'size' => 64, 'placeholder' => 'localhost'], + $prefix . 'port' => ['type' => 'text', 'size' => 5, 'placeholder' => 4190], + $prefix . 'auth' => ['value' => $authType->show([$record['ident_switch.form.sieve.auth'] ?? null])], + ]; + } + /** * Handle identity_form hook: add plugin-specific fields to the identity editor. * * Loads existing account data from the database (if editing) or applies - * preconfigured settings, then adds Common/IMAP/SMTP sections to the form. + * preconfigured settings, then adds Common/IMAP/SMTP/Sieve sections to the form. * * @param array $args Hook arguments containing 'record' with identity data. * @param IdentSwitchPreconfig $preconfig Preconfiguration handler. @@ -126,6 +147,9 @@ class IdentSwitchForm 'smtp_host' => 'smtp.host', 'smtp_port' => 'smtp.port', 'smtp_auth' => 'smtp.auth', + 'sieve_host' => 'sieve.host', + 'sieve_port' => 'sieve.port', + 'sieve_auth' => 'sieve.auth', ]; foreach ($row as $k => $v) { if (isset($dbToForm[$k])) { @@ -161,6 +185,10 @@ class IdentSwitchForm 'name' => $this->plugin->gettext('form.smtp.caption'), 'content' => $this->get_smtp_fields($record), ]; + $args['form']['ident_switch.sieve'] = [ + 'name' => $this->plugin->gettext('form.sieve.caption'), + 'content' => $this->get_sieve_fields($record), + ]; return $args; } @@ -315,6 +343,8 @@ class IdentSwitchForm 'ident_switch.form.imap.username' => 'imap.user', 'ident_switch.form.smtp.host' => 'smtp.host', 'ident_switch.form.smtp.port' => 'smtp.port', + 'ident_switch.form.sieve.host' => 'sieve.host', + 'ident_switch.form.sieve.port' => 'sieve.port', ]; foreach ($map as $recordKey => $dataKey) { @@ -397,6 +427,28 @@ class IdentSwitchForm return $retVal; } + $retVal['sieve.host'] = self::get_field_value('sieve', 'host'); + if (strlen($retVal['sieve.host'] ?? '') > 64) { + $retVal['err'] = 'host.long'; + return $retVal; + } + + $retVal['sieve.port'] = self::get_field_value('sieve', 'port'); + if ($retVal['sieve.port'] && !ctype_digit($retVal['sieve.port'])) { + $retVal['err'] = 'port.num'; + return $retVal; + } + if ($retVal['sieve.port'] && ($retVal['sieve.port'] <= 0 || $retVal['sieve.port'] > 65535)) { + $retVal['err'] = 'port.range'; + return $retVal; + } + + $retVal['sieve.auth'] = self::get_field_value('sieve', 'auth'); + if (!ctype_digit($retVal['sieve.auth'] ?? '')) { + $retVal['err'] = 'auth.num'; + return $retVal; + } + // Get also password $retVal['imap.pass'] = self::get_field_value('imap', 'password', false, true); @@ -453,13 +505,13 @@ class IdentSwitchForm // Record already exists, will update it $sql = 'UPDATE ' . $rc->db->table_name(ident_switch::TABLE) . - ' SET flags = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?, smtp_host = ?, smtp_port = ?, smtp_auth = ?, user_id = ?, iid = ?' . + ' SET flags = ?, label = ?, imap_host = ?, imap_port = ?, imap_delimiter = ?, username = ?, password = ?, smtp_host = ?, smtp_port = ?, smtp_auth = ?, sieve_host = ?, sieve_port = ?, sieve_auth = ?, user_id = ?, iid = ?' . ' WHERE id = ?'; } elseif ($data['flags'] & ident_switch::DB_ENABLED) { // No record exists, create new one $sql = 'INSERT INTO ' . $rc->db->table_name(ident_switch::TABLE) . - '(flags, label, imap_host, imap_port, imap_delimiter, username, password, smtp_host, smtp_port, smtp_auth, user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + '(flags, label, imap_host, imap_port, imap_delimiter, username, password, smtp_host, smtp_port, smtp_auth, sieve_host, sieve_port, sieve_auth, user_id, iid) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; } else { return false; } @@ -481,6 +533,9 @@ class IdentSwitchForm $data['smtp.host'], $data['smtp.port'], $data['smtp.auth'], + $data['sieve.host'], + $data['sieve.port'], + $data['sieve.auth'], $rc->user->ID, $data['id'], $r['id'] ?? null diff --git a/lib/IdentSwitchPreconfig.php b/lib/IdentSwitchPreconfig.php index 215cb12..241625d 100644 --- a/lib/IdentSwitchPreconfig.php +++ b/lib/IdentSwitchPreconfig.php @@ -110,6 +110,22 @@ class IdentSwitchPreconfig $record['ident_switch.form.smtp.port'] = !empty($urlArr['port']) ? intval($urlArr['port']) : ''; } + // Sieve: use sieve_host only (no fallback — sieve is optional) + $sieveUrl = $cfg['sieve_host'] ?? ''; + if (!empty($sieveUrl)) { + $urlArr = parse_url($sieveUrl); + $host = !empty($urlArr['host']) ? rcube::Q($urlArr['host'], 'url') : ''; + $scheme = strtolower($urlArr['scheme'] ?? ''); + + if ($scheme === 'tls' || $scheme === 'ssl') { + $record['ident_switch.form.sieve.host'] = $scheme . '://' . $host; + } else { + $record['ident_switch.form.sieve.host'] = $host; + } + + $record['ident_switch.form.sieve.port'] = !empty($urlArr['port']) ? intval($urlArr['port']) : ''; + } + $loginSet = false; if (!empty($cfg['user'])) { match (strtoupper($cfg['user'])) { diff --git a/lib/IdentSwitchSwitcher.php b/lib/IdentSwitchSwitcher.php index c60dda6..e5f4aa4 100644 --- a/lib/IdentSwitchSwitcher.php +++ b/lib/IdentSwitchSwitcher.php @@ -194,6 +194,51 @@ class IdentSwitchSwitcher return $args; } + /** + * Handle managesieve_connect hook: configure Sieve settings for the active account. + * + * Loads Sieve host, port, and credentials from the database + * for the currently selected identity. + * + * @param array $args Hook arguments containing Sieve connection parameters. + * @return array Modified hook arguments with updated Sieve settings. + */ + public function configure_managesieve(array $args): array + { + $iid = $_SESSION['iid' . ident_switch::MY_POSTFIX] ?? null; + if (!is_numeric($iid) || $iid == -1) { + return $args; + } + + $rc = rcmail::get_instance(); + + $sql = 'SELECT sieve_host, sieve_port, sieve_auth, username, password FROM ' . $rc->db->table_name(ident_switch::TABLE) . ' WHERE iid = ? AND user_id = ?'; + $q = $rc->db->query($sql, $iid, $rc->user->ID); + $r = $rc->db->fetch_assoc($q); + if (is_array($r) && !empty($r['sieve_host'])) { + if (!$r['username']) { + $sql = 'SELECT email FROM ' . $rc->db->table_name('identities') . ' WHERE identity_id = ?'; + $q = $rc->db->query($sql, $iid); + $rIid = $rc->db->fetch_assoc($q); + $r['username'] = $rIid['email']; + } + + $sieveHost = $r['sieve_host']; + $sievePort = $r['sieve_port'] ?: 4190; + $args['host'] = $sieveHost . ':' . $sievePort; + + if ($r['sieve_auth'] == ident_switch::SIEVE_AUTH_IMAP) { + $args['user'] = $r['username']; + $args['password'] = $rc->decrypt($r['password']); + } else { + $args['user'] = ''; + $args['password'] = ''; + } + } + + return $args; + } + /** * Handle preferences_list hook: customize special folders form for remote accounts. * diff --git a/localization/de_DE.inc b/localization/de_DE.inc index 2ce1eee..f9c7039 100644 --- a/localization/de_DE.inc +++ b/localization/de_DE.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'Wie IMAP'; $labels['form.smtp.auth.none'] = 'Keine'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Servername'; + +// Port +$labels['form.sieve.port'] = 'Port'; + +// Authorization +$labels['form.sieve.auth'] = 'Autorisierung'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'Wie IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'Keine'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max). diff --git a/localization/en_US.inc b/localization/en_US.inc index f3d9add..59d42de 100644 --- a/localization/en_US.inc +++ b/localization/en_US.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'As IMAP'; $labels['form.smtp.auth.none'] = 'None'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Server host name'; + +// Port +$labels['form.sieve.port'] = 'Port'; + +// Authorization +$labels['form.sieve.auth'] = 'Authorization'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'As IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'None'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max). diff --git a/localization/fr_FR.inc b/localization/fr_FR.inc index 5ba46b4..5057b92 100644 --- a/localization/fr_FR.inc +++ b/localization/fr_FR.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'Identique à IMAP'; $labels['form.smtp.auth.none'] = 'Aucune'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Serveur'; + +// Port +$labels['form.sieve.port'] = 'Port'; + +// Authorization +$labels['form.sieve.auth'] = 'Autorisation'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'Identique à IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'Aucune'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max). diff --git a/localization/it_IT.inc b/localization/it_IT.inc index fdc1880..ed52c15 100644 --- a/localization/it_IT.inc +++ b/localization/it_IT.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'Come IMAP'; $labels['form.smtp.auth.none'] = 'Nessuna'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Nome del server'; + +// Port +$labels['form.sieve.port'] = 'Porta'; + +// Authorization +$labels['form.sieve.auth'] = 'Autorizzazione'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'Come IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'Nessuna'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max). diff --git a/localization/nl_NL.inc b/localization/nl_NL.inc index 55c0992..63d943f 100644 --- a/localization/nl_NL.inc +++ b/localization/nl_NL.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'Zoals IMAP'; $labels['form.smtp.auth.none'] = 'Geen'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Servernaam'; + +// Port +$labels['form.sieve.port'] = 'Poortnummer'; + +// Authorization +$labels['form.sieve.auth'] = 'Autorisatie'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'Zoals IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'Geen'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max). diff --git a/localization/ru_RU.inc b/localization/ru_RU.inc index 8bb69e5..86a3144 100644 --- a/localization/ru_RU.inc +++ b/localization/ru_RU.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'Как IMAP'; $labels['form.smtp.auth.none'] = 'Нет'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Адрес сервера'; + +// Port +$labels['form.sieve.port'] = 'Порт'; + +// Authorization +$labels['form.sieve.auth'] = 'Авторизация'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'Как IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'Нет'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max). diff --git a/localization/sl_SI.inc b/localization/sl_SI.inc index e6c63e5..d868931 100644 --- a/localization/sl_SI.inc +++ b/localization/sl_SI.inc @@ -64,6 +64,25 @@ $labels['form.smtp.auth.imap'] = 'Tako kot IMAP'; $labels['form.smtp.auth.none'] = 'Brez'; +// Sieve +$labels['form.sieve.caption'] = 'Sieve'; + +// Server host name +$labels['form.sieve.host'] = 'Ime oz. naslov Sieve strežnika'; + +// Port +$labels['form.sieve.port'] = 'Vrata'; + +// Authorization +$labels['form.sieve.auth'] = 'Avtorizacija'; + +// As IMAP +$labels['form.sieve.auth.imap'] = 'Tako kot IMAP'; + +// None +$labels['form.sieve.auth.none'] = 'Brez'; + + // Errors // Value in \'Server host name\' field is too long (64 chars max).