<?php
/**
 * Client-side Filters (per-user GUI) v6.3
 * - AND/OR 条件 + NOT は「演算子プルダウン」で指定（含む/含まない 等）
 * - 階層フォルダ対応（UTF7-IMAP）
 * - セーフティ: 値が短すぎる条件の無効化、1ルール上限件数、ドライラン、実行確認
 */
class filters extends rcube_plugin
{
    public $task = 'mail|settings';

    private $rc;
    private $storage;

    public function init()
    {
        $this->rc      = rcmail::get_instance();
        $this->storage = $this->rc->get_storage();

        $this->register_action('plugin.filters',         array($this, 'settings_pane'));
        $this->register_action('plugin.filters-run',     array($this, 'run_filters_action'));
        $this->register_action('plugin.filters-dryrun',  array($this, 'dryrun_filters_action'));
        $this->register_action('plugin.filters-save',    array($this, 'save_settings_action'));

        if ($this->rc->task == 'settings') {
            $this->add_hook('settings_actions', array($this, 'settings_actions'));
        }

        $prefs   = $this->rc->user ? $this->rc->user->get_prefs() : array();
        $autorun = !empty($prefs['clientfilters_autorun']);
        if ($autorun && $this->rc->task == 'mail' && $this->rc->action == 'index') {
            $this->add_hook('render_page', function ($args) {
                $js = "setTimeout(function(){ rcmail.http_post('plugin.filters-run', {}); }, 1500);";
                $this->rc->output->add_script($js, 'docready');
                return $args;
            });
        }
    }

    public function settings_actions($args)
    {
        $args['actions'][] = array(
            'action' => 'plugin.filters',
            'class'  => 'filters',
            'label'  => 'Client-side Filters',
            'title'  => 'Client-side mail filters (per-user)',
            'domain' => 'filters',
        );
        return $args;
    }

    private function get_user_rules()
    {
        $rules = array();
        if ($this->rc->user) {
            $prefs = $this->rc->user->get_prefs();
            if (!empty($prefs['clientfilters_rules'])) {
                $tmp = json_decode((string)$prefs['clientfilters_rules'], true);
                if (is_array($tmp)) $rules = $tmp;
            }
        }
        if (empty($rules)) {
            $global = $this->rc->config->get('filters_rules', array());
            if (is_array($global)) {
                foreach ($global as $gr) {
                    if (is_array($gr) && isset($gr['field'])) {
                        $rules[] = array(
                            'logic'  => 'and',
                            'conds'  => array(array(
                                'field' => $gr['field'],
                                'op'    => isset($gr['op']) ? $gr['op'] : 'contains',
                                'value' => isset($gr['value']) ? $gr['value'] : '',
                                'not'   => !empty($gr['not']),
                            )),
                            'action' => isset($gr['action']) ? $gr['action'] : 'move',
                            'target' => isset($gr['target']) ? $gr['target'] : '',
                        );
                    }
                }
            }
        }
        return $rules;
    }

    public function settings_pane()
    {
        $out = $this->rc->output;
        $out->set_pagetitle('Client-side Filters');

        $prefs     = $this->rc->user ? $this->rc->user->get_prefs() : array();
        $autorun   = !empty($prefs['clientfilters_autorun']) ? 'checked' : '';
        $rules     = $this->get_user_rules();
        if (!is_array($rules)) $rules = array();

        // Folders list
        $folders = array();
        if ($this->storage && method_exists($this->storage, 'list_folders')) {
            $list = $this->storage->list_folders();
            foreach ((array)$list as $f) $folders[] = $this->from_utf7imap($f);
        }
        sort($folders, SORT_NATURAL);

        $token = $this->rc->get_request_token();

        $fields = array('from'=>'From','reply-to'=>'Reply-To','to'=>'To','cc'=>'Cc','subject'=>'Subject','sender'=>'Sender');
        // negated operators included
        $ops = array(
            'contains'     => '含む',
            'not_contains' => '含まない（NOT）',
            'equals'       => '一致',
            'not_equals'   => '一致しない（NOT）',
            'starts'       => '前方一致',
            'not_starts'   => '前方一致でない（NOT）',
            'ends'         => '後方一致',
            'not_ends'     => '後方一致でない（NOT）',
        );

        ob_start();
        ?>
        <style>
            .filters-rule { border:1px solid #e3e3e3; border-radius:8px; padding:10px; margin:10px 0; }
            .filters-conds { width:100%; border-collapse:collapse; margin:6px 0; }
            .filters-conds th,.filters-conds td { border-bottom:1px solid #f0f0f0; padding:6px; }
            .filters-conds th { color:#555; font-weight:600; }
            .filters-actions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:8px; }
            .button { padding:6px 10px; }
            .w-100 { width:100%; }
            .muted { color:#666; font-size:12px; }
            .logic-select { margin-left: 6px; }
        </style>
        <div class="boxcontent">
          <h3>クライアント側フィルタ（ユーザーごと／AND・OR・NOTは演算子で指定）</h3>
          <form method="post" action="?_task=settings&amp;_action=plugin.filters-save&amp;_framed=1" id="filters-form" onsubmit="return filtersBeforeSave()">
            <input type="hidden" name="_token" value="<?php echo htmlspecialchars($token); ?>">

            <div id="rules-container">
            <?php $rid = 0; foreach ($rules as $r): $rid++;
                $logic  = isset($r['logic']) ? strtolower($r['logic']) : 'and';
                $conds  = isset($r['conds']) && is_array($r['conds']) ? $r['conds'] : array();
                if (empty($conds) && isset($r['field'])) {
                    $conds = array(array('field'=>$r['field'],'op'=>isset($r['op'])?$r['op']:'contains','value'=>isset($r['value'])?$r['value']:'','not'=>false));
                }
                $target = isset($r['target']) ? (string)$r['target'] : '';
                if (stripos($target, 'INBOX/') === 0)  $target = substr($target, 6);
                if (stripos($target, 'INBOX.') === 0)  $target = substr($target, 6);
            ?>
              <div class="filters-rule" data-rule="<?php echo $rid; ?>">
                <div class="muted">このルールの条件のつなぎ方：
                  <select name="rules[<?php echo $rid; ?>][logic]" class="logic-select">
                    <option value="and" <?php if ($logic!=='or') echo 'selected'; ?>>すべて満たす（AND）</option>
                    <option value="or"  <?php if ($logic==='or') echo 'selected'; ?>>いずれか満たす（OR）</option>
                  </select>
                </div>
                <table class="filters-conds">
                  <thead>
                    <tr>
                      <th style="width:20%">条件</th>
                      <th style="width:22%">演算子</th>
                      <th>値（3文字以上）</th>
                      <th style="width:6%"></th>
                    </tr>
                  </thead>
                  <tbody>
                  <?php $cid = 0; foreach ($conds as $c): $cid++;
                      $cf = isset($c['field']) ? strtolower($c['field']) : 'from';
                      $co = isset($c['op']) ? strtolower($c['op']) : 'contains';
                      $cv = isset($c['value']) ? (string)$c['value'] : '';
                      $cn = !empty($c['not']);
                      // map legacy checkbox 'not' to not_* operator for UI
                      if ($cn && strpos($co, 'not_') !== 0) $co = 'not_' . $co;
                  ?>
                    <tr data-cond="<?php echo $cid; ?>">
                      <td>
                        <select name="rules[<?php echo $rid; ?>][conds][<?php echo $cid; ?>][field]" class="w-100">
                          <?php foreach ($fields as $k=>$label): ?>
                            <option value="<?php echo $k; ?>" <?php if ($cf === $k) echo 'selected'; ?>><?php echo htmlspecialchars($label); ?></option>
                          <?php endforeach; ?>
                        </select>
                      </td>
                      <td>
                        <select name="rules[<?php echo $rid; ?>][conds][<?php echo $cid; ?>][op]" class="w-100">
                          <?php foreach ($ops as $k=>$label): ?>
                            <option value="<?php echo $k; ?>" <?php if ($co === $k) echo 'selected'; ?>><?php echo htmlspecialchars($label); ?></option>
                          <?php endforeach; ?>
                        </select>
                      </td>
                      <td><input type="text" name="rules[<?php echo $rid; ?>][conds][<?php echo $cid; ?>][value]" class="w-100" value="<?php echo htmlspecialchars($cv); ?>" placeholder="例：@example.com / 請求書"></td>
                      <td><button type="button" class="button" onclick="filtersRemoveCond(this)">－</button></td>
                    </tr>
                  <?php endforeach; ?>
                  </tbody>
                </table>
                <div class="filters-actions">
                  <button type="button" class="button" onclick="filtersAddCond(<?php echo $rid; ?>)">＋ 条件を追加</button>
                  <span class="muted">※ NOT は演算子（含まない／一致しない 等）で指定します。</span>
                </div>
                <div class="filters-actions">
                  <div style="display:flex; gap:6px; width:100%;">
                    <select name="rules[<?php echo $rid; ?>][target_select]" class="w-100">
                      <option value="">（既存フォルダから選択）</option>
                      <?php foreach ($folders as $ff): ?>
                        <option value="<?php echo htmlspecialchars($ff); ?>" <?php if ($target === $ff) echo 'selected'; ?>><?php echo htmlspecialchars($ff); ?></option>
                      <?php endforeach; ?>
                    </select>
                    <input type="text" name="rules[<?php echo $rid; ?>][target_text]" class="w-100" placeholder="新規/階層フォルダ（例：取引先/請求）" value="<?php echo htmlspecialchars($target); ?>">
                  </div>
                  <input type="hidden" name="rules[<?php echo $rid; ?>][action]" value="move">
                  <button type="button" class="button" onclick="filtersRemoveRule(this)">このルールを削除</button>
                </div>
              </div>
            <?php endforeach; ?>
            </div>

            <div class="filters-actions">
              <button type="button" class="button" onclick="filtersAddRule()">＋ ルールを追加</button>
              <label><input type="checkbox" name="autorun" value="1" <?php echo $autorun; ?>> ログイン時に自動実行</label>
              <span class="spacer"></span>
              <button type="submit" class="button">保存</button>
              <a class="button" href="?_task=settings&amp;_action=plugin.filters-dryrun&amp;_framed=1">試しに件数だけ数える（ドライラン）</a>
              <a class="button" href="?_task=settings&amp;_action=plugin.filters-run&amp;_framed=1" >今すぐフィルタを実行</a>
            </div>
            <div class="muted">※ 1ルール最大移動件数は既定で 300。超えると中止（config で変更可）。</div>
          </form>
        </div>
        <script>
        let ruleSeq = <?php echo max(0, (int)count($rules)); ?>;
        function filtersAddRule() {
          ruleSeq += 1;
          const container = document.getElementById('rules-container');
          const html = `
            <div class="filters-rule" data-rule="${ruleSeq}">
              <div class="muted">このルールの条件のつなぎ方：
                <select name="rules[${ruleSeq}][logic]" class="logic-select">
                  <option value="and" selected>すべて満たす（AND）</option>
                  <option value="or">いずれか満たす（OR）</option>
                </select>
              </div>
              <table class="filters-conds">
                <thead>
                  <tr>
                    <th style="width:20%">条件</th>
                    <th style="width:22%">演算子</th>
                    <th>値（3文字以上）</th>
                    <th style="width:6%"></th>
                  </tr>
                </thead>
                <tbody>
                  <tr data-cond="1">
                    <td>
                      <select name="rules[${ruleSeq}][conds][1][field]" class="w-100">
                        <option value="from">From</option>
                        <option value="reply-to">Reply-To</option>
                        <option value="to">To</option>
                        <option value="cc">Cc</option>
                        <option value="subject">Subject</option>
                        <option value="sender">Sender</option>
                      </select>
                    </td>
                    <td>
                      <select name="rules[${ruleSeq}][conds][1][op]" class="w-100">
                        <option value="contains">含む</option>
                        <option value="not_contains">含まない（NOT）</option>
                        <option value="equals">一致</option>
                        <option value="not_equals">一致しない（NOT）</option>
                        <option value="starts">前方一致</option>
                        <option value="not_starts">前方一致でない（NOT）</option>
                        <option value="ends">後方一致</option>
                        <option value="not_ends">後方一致でない（NOT）</option>
                      </select>
                    </td>
                    <td><input type="text" name="rules[${ruleSeq}][conds][1][value]" class="w-100" value=""></td>
                    <td><button type="button" class="button" onclick="filtersRemoveCond(this)">－</button></td>
                  </tr>
                </tbody>
              </table>
              <div class="filters-actions">
                <button type="button" class="button" onclick="filtersAddCond(${ruleSeq})">＋ 条件を追加</button>
                <span class="muted">※ NOT は演算子（含まない 等）で指定。</span>
              </div>
              <div class="filters-actions">
                <div style="display:flex; gap:6px; width:100%;">
                  <select name="rules[${ruleSeq}][target_select]" class="w-100">
                    <option value="">（既存フォルダから選択）</option>
                    <?php foreach ($folders as $ff): ?>
                      <option value="<?php echo htmlspecialchars($ff); ?>"><?php echo htmlspecialchars($ff); ?></option>
                    <?php endforeach; ?>
                  </select>
                  <input type="text" name="rules[${ruleSeq}][target_text]" class="w-100" placeholder="新規/階層フォルダ（例：取引先/請求）" value="">
                </div>
                <input type="hidden" name="rules[${ruleSeq}][action]" value="move">
                <button type="button" class="button" onclick="filtersRemoveRule(this)">このルールを削除</button>
              </div>
            </div>`;
          container.insertAdjacentHTML('beforeend', html);
        }
        function filtersAddCond(ruleId) {
          const rule = document.querySelector('.filters-rule[data-rule="'+ruleId+'"]');
          const tbody = rule.querySelector('tbody');
          const next = tbody.querySelectorAll('tr').length + 1;
          const html = `
            <tr data-cond="${next}">
              <td>
                <select name="rules[${ruleId}][conds][${next}][field]" class="w-100">
                  <option value="from">From</option>
                  <option value="reply-to">Reply-To</option>
                  <option value="to">To</option>
                  <option value="cc">Cc</option>
                  <option value="subject">Subject</option>
                  <option value="sender">Sender</option>
                </select>
              </td>
              <td>
                <select name="rules[${ruleId}][conds][${next}][op]" class="w-100">
                  <option value="contains">含む</option>
                  <option value="not_contains">含まない（NOT）</option>
                  <option value="equals">一致</option>
                  <option value="not_equals">一致しない（NOT）</option>
                  <option value="starts">前方一致</option>
                  <option value="not_starts">前方一致でない（NOT）</option>
                  <option value="ends">後方一致</option>
                  <option value="not_ends">後方一致でない（NOT）</option>
                </select>
              </td>
              <td><input type="text" name="rules[${ruleId}][conds][${next}][value]" class="w-100" value=""></td>
              <td><button type="button" class="button" onclick="filtersRemoveCond(this)">－</button></td>
            </tr>`;
          tbody.insertAdjacentHTML('beforeend', html);
        }
        function filtersRemoveCond(btn) {
          const tr = btn.closest('tr');
          tr && tr.remove();
        }
        function filtersRemoveRule(btn) {
          const wrap = btn.closest('.filters-rule');
          wrap && wrap.remove();
        }
        function filtersBeforeSave() {
          const inputs = document.querySelectorAll('input[name*="[value]"]');
          for (const i of inputs) {
            const v = i.value ? i.value.trim() : '';
            if (v && v.length > 0 && v.length < 3) {
              return confirm('3文字未満の条件があります。本当に保存しますか？');
            }
          }
          return true;
        }
        </script>
        <?php
        $html = ob_get_clean();
        echo $html;

        if (!empty($_SESSION['filters_flash'])) {
            $f = $_SESSION['filters_flash'];
            unset($_SESSION['filters_flash']);
            $this->rc->output->command('display_message', $f['text'], $f['type']);
        }

        $out->send('iframe');
    }

    public function save_settings_action()
    {
        $posted  = isset($_POST['rules']) ? $_POST['rules'] : array();
        $autorun = !empty($_POST['autorun']) ? 1 : 0;

        $rules = array();
        if (is_array($posted)) {
            foreach ($posted as $rid => $r) {
                $logic = isset($r['logic']) && strtolower((string)$r['logic']) === 'or' ? 'or' : 'and';

                $conds = array();
                if (!empty($r['conds']) && is_array($r['conds'])) {
                    foreach ($r['conds'] as $c) {
                        $field = isset($c['field']) ? strtolower(trim((string)$c['field'])) : '';
                        $op    = isset($c['op']) ? strtolower(trim((string)$c['op'])) : 'contains';
                        $val   = isset($c['value']) ? trim((string)$c['value']) : '';
                        if ($field === '' || $val === '') continue;
                        if (mb_strlen($val) < 3) continue; // safety
                        $not   = false;
                        if (strpos($op, 'not_') === 0) {
                            $not = true;
                            $op  = substr($op, 4); // remove "not_"
                            if ($op === false || $op === '') $op = 'contains';
                        }
                        $conds[] = array('field'=>$field,'op'=>$op,'value'=>$val,'not'=>$not);
                    }
                }
                $sel    = isset($r['target_select']) ? trim((string)$r['target_select']) : '';
                $txt    = isset($r['target_text']) ? trim((string)$r['target_text']) : '';
                $target = $txt !== '' ? $txt : $sel;

                if (empty($conds) || $target === '') continue;
                $rules[] = array('logic'=>$logic, 'conds'=>$conds, 'action'=>'move', 'target'=>$target);
            }
        }

        if ($this->rc->user) {
            $prefs = $this->rc->user->get_prefs();
            $prefs['clientfilters_rules']   = json_encode($rules, JSON_UNESCAPED_UNICODE);
            $prefs['clientfilters_autorun'] = $autorun ? 1 : 0;
            $this->rc->user->save_prefs($prefs);
        }

        $_SESSION['filters_flash'] = ['type' => 'confirmation', 'text' => '保存しました。'];
        $this->rc->output->redirect('?_task=settings&_action=plugin.filters&_framed=1');
    }

    public function run_filters_action()   { return $this->run_filters_common(false); }
    public function dryrun_filters_action(){ return $this->run_filters_common(true);  }

    private function run_filters_common($dryrun = false)
    {
        $type = 'confirmation';
        $msg  = '';

        try {
            $result = $this->run_filters_core($dryrun);
            if ($dryrun) $msg = sprintf("Dry-run: matched(total)=%d (no moves)", $result['matched']);
            else         $msg = sprintf("Executed: moved=%d, matched=%d, rules=%d", $result['moved'], $result['matched'], $result['rules']);
        } catch (Exception $e) {
            $type = 'error';
            $msg  = 'Filters error: ' . $e->getMessage();
        }

        $_SESSION['filters_flash'] = array('type' => $type, 'text' => $msg);
        $this->rc->output->redirect('?_task=settings&_action=plugin.filters&_framed=1');
    }

    private function run_filters_core($dryrun = false)
    {
        $rules = $this->get_user_rules();
        if (!is_array($rules) || empty($rules)) {
            return ['moved'=>0,'matched'=>0,'rules'=>0];
        }

        $storage = $this->storage ?: $this->rc->get_storage();
        if (!$storage) throw new Exception('storage unavailable');

        $mbox = 'INBOX';
        if (method_exists($storage,'set_folder')) { @ $storage->set_folder($mbox); }
        elseif (method_exists($storage,'select')) { @ $storage->select($mbox); }

        $delim = method_exists($storage,'get_hierarchy_delimiter') ? ($storage->get_hierarchy_delimiter() ?: '/') : '/';
        $debug = $this->rc->config->get('filters_debug', false);

        $max_move_per_rule = (int)$this->rc->config->get('filters_max_move_per_rule', 300);

        $moved = 0; $matched = 0;

        foreach ($rules as $rule) {
            if (!is_array($rule)) continue;
            $logic  = isset($rule['logic']) && strtolower((string)$rule['logic']) === 'or' ? 'or' : 'and';
            $conds  = isset($rule['conds']) && is_array($rule['conds']) ? $rule['conds'] : array();
            $action = isset($rule['action']) ? strtolower((string)$rule['action']) : 'move';
            $target = isset($rule['target']) ? (string)$rule['target'] : '';

            if (empty($conds) || $action !== 'move' || $target === '') continue;

            $crit = ($logic === 'or')
                ? $this->build_or_criteria($conds)
                : $this->build_and_criteria($conds);

            if ($debug && method_exists('rcube','write_log')) rcube::write_log('filters', "Search criteria: " . $crit);

            if (!method_exists($storage,'search')) continue;
            $res  = $storage->search($mbox, $crit);
            $uids = array();
            if (is_object($res) && method_exists($res,'get')) $uids = $res->get();
            elseif (is_array($res))                           $uids = $res;
            if (!is_array($uids) || empty($uids)) continue;

            $count = count($uids);
            $matched += $count;

            if ($count > $max_move_per_rule) {
                if ($debug && method_exists('rcube','write_log')) {
                    rcube::write_log('filters', "ABORT: matched={$count} > max_move_per_rule={$max_move_per_rule} (rule skipped)");
                }
                continue;
            }

            if ($dryrun) continue;

            $norm = $this->normalize_target($target, $delim);
            if ($norm === '') {
                if ($debug && method_exists('rcube','write_log')) {
                    rcube::write_log('filters', "Skip rule: empty target after normalize (raw='{$target}')");
                }
                continue;
            }
            $dest_utf8 = (strpos($norm, 'INBOX' . $delim) === 0 || $norm === 'INBOX') ? $norm : 'INBOX' . $delim . $norm;
            $dest_utf7 = $this->to_utf7imap($dest_utf8);

            if (method_exists($storage,'folder_exists') && !$storage->folder_exists($dest_utf7)) {
                if (method_exists($storage,'create_folder')) { @ $storage->create_folder($dest_utf7, true); }
            }
            if (method_exists($storage,'move_message')) {
                $ok = @ $storage->move_message($uids, $dest_utf7, $mbox);
                if ($ok) $moved += count($uids);
                if ($debug && method_exists('rcube','write_log')) {
                    rcube::write_log('filters', sprintf("Move %s messages to %s %s",
                        count($uids), $this->from_utf7imap($dest_utf7), $ok ? 'OK' : 'FAIL'));
                }
            }
        }

        if ($debug && method_exists('rcube','write_log')) rcube::write_log('filters', sprintf("Done moved=%d matched=%d", $moved, $matched));
        return ['moved'=>$moved,'matched'=>$matched,'rules'=>count($rules)];
    }

    private function build_and_criteria($conds)
    {
        $parts = array();
        foreach ($conds as $c) {
            if (!is_array($c)) continue;
            $clause = $this->build_clause_from_cond($c);
            if ($clause !== '') $parts[] = $clause;
        }
        return implode(' ', $parts);
    }

    private function build_or_criteria($conds)
    {
        $parts = array();
        foreach ($conds as $c) {
            if (!is_array($c)) continue;
            $clause = $this->build_clause_from_cond($c);
            if ($clause !== '') $parts[] = $clause;
        }
        $count = count($parts);
        if ($count === 0) return '';
        if ($count === 1) return $parts[0];
        $expr = 'OR ' . $parts[0] . ' ' . $parts[1];
        for ($i = 2; $i < $count; $i++) {
            $expr = 'OR ' . $expr . ' ' . $parts[$i];
        }
        return $expr;
    }

    private function build_clause_from_cond($c)
    {
        $field = isset($c['field']) ? strtolower((string)$c['field']) : '';
        $op    = isset($c['op']) ? strtolower((string)$c['op']) : 'contains';
        $val   = isset($c['value']) ? (string)$c['value'] : '';
        $not   = !empty($c['not']); // may be set via legacy data (kept for backward-compat)
        if (strpos($op, 'not_') === 0) { $not = true; $op = substr($op, 4) ?: 'contains'; }
        if ($field === '' || $val === '') return '';
        if (mb_strlen($val) < 3) return '';

        switch ($field) {
            case 'from':     $hdr = 'FROM'; break;
            case 'to':       $hdr = 'TO'; break;
            case 'cc':       $hdr = 'CC'; break;
            case 'subject':  $hdr = 'SUBJECT'; break;
            case 'reply-to':
            case 'replyto':
            case 'reply_to': $hdr = 'HEADER REPLY-TO'; break;
            case 'sender':   $hdr = 'HEADER SENDER'; break;
            default:         $hdr = 'HEADER ' . strtoupper($field);
        }
        // サーバ差を避けるため、equals/starts/ends も包含検索にフォールバック
        $v = str_replace('"','\"',(string)$val);
        $clause = $hdr . ' "' . $v . '"';
        if ($not) $clause = 'NOT ' . $clause;
        return $clause;
    }

    private function normalize_target($target, $delim)
    {
        $t = trim((string)$target);
        if (stripos($t, 'INBOX/') === 0)  $t = substr($t, 6);
        if (stripos($t, 'INBOX' . $delim) === 0) $t = substr($t, 6);
        if (strcasecmp($t, 'INBOX') === 0) $t = '';

        $t = str_replace(array('/', '\\'), $delim, $t);
        $dup = $delim . $delim;
        while (strpos($t, $dup) !== false) $t = str_replace($dup, $delim, $t);
        if ($t !== '' && substr($t, -strlen($delim)) === $delim) $t = substr($t, 0, -strlen($delim));
        if ($t !== '' && strpos($t, $delim) === 0) $t = substr($t, strlen($delim));
        if ($t === '' || $t === $delim) return '';
        return $t;
    }

    private function to_utf7imap($s)
    {
        if (class_exists('rcube_charset') && method_exists('rcube_charset','convert')) {
            return rcube_charset::convert((string)$s, 'UTF-8', 'UTF7-IMAP');
        }
        return (string)$s;
    }
    private function from_utf7imap($s)
    {
        if (class_exists('rcube_charset') && method_exists('rcube_charset','convert')) {
            return rcube_charset::convert((string)$s, 'UTF7-IMAP', 'UTF-8');
        }
        return (string)$s;
    }
}
