プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

identy_switch の挙動について

先日、勝手にアップデートされていて困ってしまった と書いた identy_switch ですが、実はそれ以前からも設定したはずの『特殊なフォルダ』設定が勝手に変更されていたりして、どうも挙動が不安定な印象がありました。

つまり、この設定画面の『特殊なフォルダ』の部分が

いつのまにやら、変更されているという感じです。

せっかくの機会なので、 identy_switch のコードなどもちらほら見ながら挙動を追いかけてみたので、その際にわかったことをメモっておきます。

なお、使ったバージョンは以下の通りです。

  • Roundcube 1.6.7
  • identy_switch 1.0.17

identy_switch を使ううえでの注意点

まず、結論としては、挙動が不安定というのではなく、(多分)identy_switch が想定していない使い方、私のほうがしていたための現象のようです。

自分なりの解釈として、 identy_switch を使ううえでの注意点としては

  • identy_switch を利用して、アカウントを切り替えつつ Roundcube を使う場合は、複数のブラウザから同時にアクセスしない
  • Roundcube からログアウトする前に、デフォルトのアカウントに切り替える

となります。

再現方法

上記の結論を導き出す前に、最初に触れた『特殊なフォルダ』の設定がおかしくなる再現方法がわかったので、まとめておきます。なお、下記で設定しているアカウントに対して、それぞれ『特殊なフォルダ』を設定しているものとします。

  1. 1つ目のブラウザ(ブラウザ A )で Roundcube にアクセスします。この時、ログインに使ったデフォルトのアカウント(アカウント 1 )でメールが表示されます。
  2. 別のアカウント(アカウント 2 )に切り替えます。切り替えたアカウントについても、特殊なフォルダも問題なく表示されます。
  3. 2つ目のブラウザ(ブラウザ B )で Roundcube にアクセスします。この時のログインもアカウント 1 を使って行います。
  4. デフォルトのアカウントでメールが表示されるのですが、『特殊なフォルダ』が正しく表示されません
  5. ブラウザ A の Roundcube を終了させます
  6. ブラウザBの Roundcube を終了させます
  7. 再度、ブラウザA でアカウント 1 でログインすると、『特殊なフォルダ』が正しく表示されません
  8. 設定画面に移動すると、『特殊なフォルダ』の内容が変更されています

不思議ですね。

わざわざブラウザ切り替えて、こんなことするのか?と思われるかもしれませんが、 PC とスマホの両方からアクセスしていたりすると、割と頻繁に起きることになります。

あと、この状態になっている場合、メールの送信、削除などをおこなおうとすると

とエラーが表示されて、操作できなくなることがあります。なので、困りものです。

挙動の確認

なんでこうなるのか?ということを理解するためにも、いろいろと調べてみました。

保存場所

まず、 identy_switch を利用した場合、追加したアカウント毎にサーバー、ポート番号、ユーザー名、パスワードなどを設定すると思いますが、これらがどこに保存されているかを確認します。

デフォルト以外の追加したアカウントに対しては、データベースの identy_switch テーブルにアカウント毎のレコードとして格納されます。

たとえば、データベースとして SQLite を使っている場合は、 /var/roundcube/db/sqlite.db にファイルがあるので、これを開くと、

sqlite> select * from identy_switch;
+----+---------+-----+---------------+-------+----------------------+----------------------+------------------+-----------+------------+---------------+----------------+------------------+-----------+-------------+------------+-------------+-------------+
| id | user_id | iid |    label      | flags |      imap_user       |       imap_pwd       |    imap_host     | imap_port | imap_delim | newmail_check | notify_timeout |    smtp_host     | smtp_port |   drafts    |    sent    |    junk     |    trash    |
+----+---------+-----+---------------+-------+----------------------+----------------------+------------------+-----------+------------+---------------+----------------+------------------+-----------+-------------+------------+-------------+-------------+
| 1  | 1       | 2   | email 1       | 21    | user1@example.com    | password 1           | example.com      | 993       | /          | 60            | 10             | example.com      | 465       | Drafts      | Sent       | Junk E-mail | Trash       |
| 2  | 1       | 3   | email 2       | 21    | user2@test.com       | password 2           | test.com         | 993       | .          | 60            | 10             | test.com         | 465       | INBOX.Draft | INBOX.Sent | INBOX.spam  | INBOX.Trash |
| 3  | 1       | 4   | email 3       | 21    | user3@example.com    | password 3           | example.com      | 993       | .          | 60            | 10             | example.com      | 465       |             |            |             |             |

という感じで設定したものが入っていることがわかります。

この時、気をつけたいのが、デフォルトのユーザーに関する情報はこのテーブルには含まれていない、ということです。では、デフォルトユーザーの設定情報はどこにあるのかと調べてみると、

sqlite> select * from users;
+---------+-----------------------+------------------+---------------------+---------------------+---------------------+----------------------+----------+--------------------------------------------------------------+
| user_id |       username        |    mail_host     |       created       |     last_login      |    failed_login     | failed_login_counter | language |                         preferences                          |
+---------+-----------------------+------------------+---------------------+---------------------+---------------------+----------------------+----------+--------------------------------------------------------------+
| 1       | default@test.com      | test.com         | 2023-08-02 23:17:40 | 2024-09-03 02:26:49 | 2024-04-28 12:25:45 | 1                    | ja_JP    | a:26:{s:21:"show_real_foldernames";b:0;s:20:"lock_special_fo |
|         |                       |                  |                     |                     |                     |                      |          | lders";b:1;s:11:"drafts_mbox";s:11:"INBOX.Draft";s:9:"sent_m |
|         |                       |                  |                     |                     |                     |                      |          | box";s:10:"INBOX.Sent";s:9:"junk_mbox";s:10:"INBOX.spam";s:1 |
|         |                       |                  |                     |                     |                     |                      |          | 0:"trash_mbox";s:11:"INBOX.Trash";s:12:"archive_mbox";s:0:"" |
|         |                       |                  |                     |                     |                     |                      |          | ;s:12:"archive_type";s:0:"";s:17:"collapsed_folders";s:3216: |

という感じで、 users テーブルの preferences フィールドに含まれていることがわかります。上記の例だと draft_mbox, sent_mbox, junk_mbox, trash_mbox といった特殊なフォルダに対する設定と思われるものが見られます。

この preferences フィールドの内容と書き方については調べてないのですが、どうもデフォルトユーザーに関する設定をすべて含んでいるようです。

ちなみに、追加したアカウントに関する情報は、

sqlite> select * from identities;
+-------------+---------+---------------------+-----+----------+------+--------------+-----------------------------+----------+-----+--------------------------------+----------------+
| identity_id | user_id |       changed       | del | standard | name | organization |           email             | reply-to | bcc |           signature            | html_signature |
+-------------+---------+---------------------+-----+----------+------+--------------+-----------------------------+----------+-----+--------------------------------+----------------+
| 1           | 1       | 2023-09-05 00:48:56 | 0   | 1        | mor  |              | default@test.com            |          |     | --                             | 0              |
|             |         |                     |     |          |      |              |                             |          |     | 森ソフト                       |                |
|             |         |                     |     |          |      |              |                             |          |     | 代表 森 純一                 |                |
+-------------+---------+---------------------+-----+----------+------+--------------+-----------------------------+----------+-----+--------------------------------+----------------+
| 2           | 1       | 2024-08-27 06:17:44 | 0   | 0        |      |              | user1@example.com           |          |     | --------------- | 0              |
|             |         |                     |     |          |      |              |                             |          |     | 森 純一                       |                |
|             |         |                     |     |          |      |              |                             |          |     | --------------- |                |
+-------------+---------+---------------------+-----+----------+------+--------------+-----------------------------+----------+-----+--------------------------------+----------------+
| 3           | 1       | 2024-07-19 12:56:58 | 0   | 0        |      |              | user2@test.comm             |          |     |                                | 0              |

のように、 identities テーブルに保存されています。また、 identy_switch テーブルの iid フィールドは、この identities テーブルの identity_id を参照する外部キーになっています。

似た種類の情報(同じ設定画面内の情報)が異なるテーブルに書き込まれていたり、デフォルトユーザーと追加したアカウントのユーザーについて保存されるテーブルが異なるなど、ちょっと直観的ではない格納方法になっています。

このあたりは、もともと Roundcube の作りとしては 1 ログインユーザーのメールボックスを表示するという機能に対して、プラグインであとから対応しようとした結果なので、致し方ないのかなーと思われますね。

アカウント切り替え時の動作

データの保存場所がわかったら、次は identy_switch プラグインの動作を追いかけてみます。 Roundcube のプラグインの基本的な構成については、こちらの Github の Wiki ページに詳しく載っているので、最初にこちらをざっと読んでおくと、全容がつかみやすいかと思います。

identy_switch.php を見てみると、 最初に呼ばれる init 関数内で

<?php
class identy_switch extends identy_switch_prefs
{
        /**
         *      Initialize Plugin
         *
         *      {@inheritDoc}
         *      @see rcube_plugin::init()
         */
        function init(): void
        {
                $rc = rcmail::get_instance();

                // Identy switch hooks and actions
                $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('template_object_composeheaders', [ $this, 'on_object_composeheaders' ]);
                $this->register_action('identy_switch_do',        [ $this, 'identy_switch_do_switch' ]);

のように、 identy_switch_do アクションが指定されています。アクションが指定された際に呼ばれる関数 identy_switch_do_switch を見てみると、

<?php
        /**
         *      Perform identity switch
         */
        function identy_switch_do_switch(): void
        {
                $rc = rcmail::get_instance();
(略)
                // Get new account
                $iid = rcube_utils::get_input_value('identy_switch_iid', rcube_utils::INPUT_POST);
                $rec = self::get($iid);

                if ($iid == -1)
                        $this->write_log('Switching mailbox back to default identity "'.$rec['imap_user'].'"');
                else
                        $this->write_log('Switching mailbox to identity "'.$rec['imap_user'].'"');

という感じで、切り替え先のアカウントのid ($iid) を使って、切り替え処理を行っているようです。 $rec というのが切り替え先のアカウントに関する設定情報を持っているようで、

<?php
                $_SESSION['_name']                              = $rec['label'];
                $_SESSION['username']                   = $rec['imap_user'];
                $_SESSION['password']                   = $rec['imap_pwd'];
(略)
                $_SESSION['unseen']                             = $rec['unseen'];
                self::set(null, 'iid', $iid);

のようにセッション変数に対して、設定値を書き込んでいます。

一方、『特殊なフォルダ』については、ユーザーの preferences と思われるものを取得して、それを更新後、保存しているようです。

<?php
                $prefs = $rc->user->get_prefs();

                // Set special folder
                $prefs['show_real_foldernames'] = $rec['flags'] & self::SHOW_REAL_FOLDER ? true : false;
                $prefs['lock_special_folders'] = $rec['flags'] & self::LOCK_SPECIAL_FOLDER ? true : false;
                foreach (rcube_storage::$folder_types as $mbox)
                        $prefs[$mbox.'_mbox'] = $rec[$mbox];
(略)
                // Set notification
                foreach ([      self::NOTIFY_BASIC              => 'basic',
                                        self::NOTIFY_DESKTOP    => 'desktop',
                                        self::NOTIFY_SOUND              => 'sound' ] as $k => $v)
                        if ($rec['flags'] & $k)
                                $prefs['newmail_notifier_'.$v] = '1';
                $prefs['newmail_notifier_timeout'] = $rec['notify_timeout'];
            $rc->user->save_prefs($prefs);

なるほど。

切り替えたアカウントの情報は、ログインユーザーの preferences を書き換えることで、もともとログインユーザーに対してのみ動作している Roundcube を複数のユーザーアカウントを使い、メールボックスをみられるようにしている、ということなんですね。

じゃあ、最初に保存していたデフォルトユーザーの設定はどこに行ったんだろうか?と思って追いかけると、 render_page フックというフックで呼び出されるように定義された関数 on_render_page 内に保存処理がありました。

<?php
        /**
         *      Dispatch action
         *
         *      @param array $args
         *      @return array
         */
        function on_render_page(array $args): array
        {
                $rc = rcmail::get_instance();

                switch ($rc->task)
                {
                case 'mail':

                        $this->add_texts('localization');

                        // Add onclick() handler to make sure selection menu will be closed
                        #$args['content'] = str_replace('<body', '<body onclick="identy_switch_toggle_menu(true)" ', $args['content']);

                        // First call?
                        if (!self::get(null, 'iid'))
                        {

self::get(null, 'iid') でカレントユーザーの iid を取得しているようで、これがない場合に、以下の処理をデフォルトユーザー ( iid = -1 ) に対して行っているようです。

<?php
                                // load configuration
                                $this->load_config();
                                foreach ($rc->config->get('identy_switch.config', []) as $k => $v)
                                {
                                        if ($k == 'logging')
                                                self::set('config', $k, $v, false);
                                        if ($k == 'check')
                                                self::set('config', $k, $v, true);
                                        if ($k == 'interval')
                                                self::set('config', $k, $v, 30);
                                        if ($k == 'retries')
                                                self::set('config', $k, $v, 10);
                                }
                                self::set('config', 'language', $_SESSION['language']);

                                // Set default user
                                self::set(null, 'iid', -1);

                                // Collect data for default identity
                                $i = $rc->user->get_identity();
                                self::set(-1, 'label', $i['name']);
                                self::set(-1, 'flags', self::ENABLED);

                                // Swap IMAP data
                                self::set(-1, 'imap_user', $_SESSION['username']);
                                self::set(-1, 'imap_pwd', $_SESSION['password']);
(略)

render_page フックは、html を返す前に呼び出されるフックのようです。このタイミングで、上記関数が呼ばれることで、セッション変数に含まれているデフォルトユーザーの情報が保存されるということのようです。

これらの処理で使っている self::get や self::set という関数は、親クラスの identy_switch_prefs (rcube_plugin クラスを継承)で定義されています。これらの処理を見てみると、セッション変数に連想配列(identy_switch という名称)を追加して、その内部に各種データを持たせるようにしています。

identy_switch.php の冒頭のコメントに、これに関するコメントがあるので、参考にするといいかと思います。

<?php
declare(strict_types=1);

/*
 *      Identy switch RoundCube Bundle
 *
 *      @copyright      (c) 2024 Forian Daeumling, Germany. All right reserved
 *      @license        LGPL-3.0-or-later
 */

/**
 *
 *      Data structure
 *
 *      config                          Configuration data
 *              logging                 Allow logging to 'logs/identy_switch.log'
 *              check                   Allow new mail checking
 *              interval                Specify interval for checking of new mails
 *              retries                 Specify no. of retries for reading data from mail server
 *              language                Language used
 *              cache                   All session variables used by identy switch
 *              data                    Unseen exchange data file
 *              fp                              File pointer
 *      iid                                     Active identity (-1 = default user)
 *      [n]                                     Cached identity data
 *              label                   Label
 *              flags                   Flags
 *              imap_user               IMAP user
 *              imap_pwd                IMAP password
 *              imap_host               IMAP host
 *              imap_port               IMAP port
 *              smtp_host               SMTP host
 *              smtp_port               SMTP port
 *              notify_timeout  Notification timeout
 *              newmail_check   New mail check interval
 *              drafts                  Draft folder name
 *              sent                    Sent folder name
 *              junk                    Junk folder name
 *              trash                   Trash folder name
 *              unseen                  # of unseen messages
 *              checked_last    Last time checked
 *              notify                  Notify user flag
 *
 */

require_once INSTALL_PATH.'plugins/identy_switch/identy_switch_prefs.php';
require_once INSTALL_PATH.'plugins/identy_switch/identy_switch_newmails.php';

class identy_switch extends identy_switch_prefs
{

挙動のまとめ

ということで、最初に書いた挙動になるポイントは、データの保持にセッション変数を使っているということになりそうです。

通常の場合のアカウント切り替え処理の場合は

  1. Roundcube にログインする
  2. デフォルトユーザーの設定情報が、セッション変数に保存される
  3. アカウントを切り替えると、データベースのテーブルよりそのアカウントの設定内容が取得され、ユーザーの設定情報(preferences)を上書きする
  4. ユーザーの設定情報を元に処理が行われる
  5. デフォルトユーザーに戻すと、セッション変数に保存された内容が復元される

となっているようです。

最初に述べた挙動の場合は、ブラウザ B でアクセスした時点で、ログインユーザーのpreferences フィールドは、アカウント 2 のものに書き換えられているため、これを取得しても、特殊なフォルダ名が異なるために、正しく表示されなかったのだと思われます。

そのうえで、デフォルトユーザーに戻すことなく、 Roundcube を終了することで、ユーザーの preferences の内容がアカウント 2 の設定を反映したままとなり、次にログインした際に、設定内容が変更されてしまっていたのだと思われます。

あと、上記でメールの送信時等にエラーが起きるとも書きましたが、これもメールを保存する際のフォルダ名が正しくない状態になっている、ということにより、起きているんじゃないかと推測できます。

ということは、もっと簡単に、ブラウザ A のみを使っても問題を再現できますね。実際にやってみると、

  1. ブラウザ A で Roundcube にログインする
  2. 別のアカウントに切り替える
  3. デフォルトユーザーに戻さずに、ログアウトする
  4. 再度 Roundcube にログインする
  5. 『特殊なフォルダ』が正しく表示されない

うーん、この手順で再現できてしまいました。

まとめ

処理を追いかける際、たぶんこうだろうな、と推測の部分も随分と入っているので、上記の内容は正確さに欠けると思います。とはいえ、このプラグインを使う方のご参考になるといいなと思います(identy_switch 1.0.17 時点の情報です)。

上記のどこかにも書きましたが、元々 Roundcube が 1 つのログインアカウントに対してのみ動作するようになっているものを、プラグインで対応するためにこういった工夫が入っているんだと思います。とはいえ、この仕様をちゃんと理解していないと、なかなか変な挙動だな、となってしまいそうです。

できることなら、もうちょっと切り替え操作に耐性があるような形で改良されるといいなと思います。

  • 2024/9/6 メール操作のエラーに関する記述を追記