1<?php
2/**
3 * Copyright 2008-2017 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (GPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/gpl.
7 *
8 * @category  Horde
9 * @copyright 2008-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   IMP
12 */
13
14/**
15 * Provides common functions for interaction with IMAP/POP3 servers via the
16 * Horde_Imap_Client package.
17 *
18 * @author    Michael Slusarz <slusarz@horde.org>
19 * @category  Horde
20 * @copyright 2008-2017 Horde LLC
21 * @license   http://www.horde.org/licenses/gpl GPL
22 * @package   IMP
23 *
24 * @property-read boolean $changed  If true, this object has changed.
25 * @property-read Horde_Imap_Client_Base $client_ob  The IMAP client object.
26 * @property-read IMP_Imap_Config $config  Base backend config settings.
27 * @property-read boolean $init  Has the IMAP object been initialized?
28 * @property-read integer $max_compose_bodysize  The maximum size (in bytes)
29 *                                               of the compose message body.
30 * @property-read integer $max_compose_recipients  The maximum number of
31 *                                                 recipients to send to per
32 *                                                 compose message.
33 * @property-read integer $max_compose_timelimit  The maximum number of
34 *                                                recipients to send to in the
35 *                                                configured timelimit.
36 * @property-read integer $max_create_mboxes  The maximum number of mailboxes
37 *                                            a user can create.
38 * @property-read string $server_key  Server key used to login.
39 * @property-read string $thread_algo  The threading algorithm to use.
40 * @property-read Horde_Imap_Client_Url $url  A URL object.
41 */
42class IMP_Imap implements Serializable
43{
44    /* Access constants. */
45    const ACCESS_FOLDERS = 1;
46    const ACCESS_SEARCH = 2;
47    const ACCESS_FLAGS = 3;
48    const ACCESS_UNSEEN = 4;
49    const ACCESS_TRASH = 5;
50    const ACCESS_CREATEMBOX = 6;
51    const ACCESS_CREATEMBOX_MAX = 7;
52    const ACCESS_COMPOSE_BODYSIZE = 13;
53    const ACCESS_COMPOSE_RECIPIENTS = 8;
54    const ACCESS_COMPOSE_TIMELIMIT = 9;
55    const ACCESS_ACL = 10;
56    const ACCESS_DRAFTS = 11;
57    const ACCESS_REMOTE = 12;
58    const ACCESS_IMPORT = 14;
59    const ACCESS_SORT = 15;
60
61    /* Default namespace. */
62    const NS_DEFAULT = "\0default";
63
64    /**
65     * Cached backend configuration.
66     *
67     * @var array
68     */
69    static protected $_backends = array();
70
71    /**
72     * Has this object changed?
73     *
74     * @var boolean
75     */
76    protected $_changed = false;
77
78    /**
79     * Backend config.
80     *
81     * @var IMP_Imap_Config
82     */
83    protected $_config;
84
85    /**
86     * Object identifier.
87     *
88     * @var string
89     */
90    protected $_id;
91
92    /**
93     * The IMAP client object.
94     *
95     * @var Horde_Imap_Client_Base
96     */
97    protected $_ob;
98
99    /**
100     * Temporary data cache (destroyed at end of request).
101     *
102     * @var array
103     */
104    protected $_temp = array();
105
106    /**
107     * Constructor.
108     *
109     * @param string $id  Object identifier.
110     */
111    public function __construct($id)
112    {
113        $this->_id = strval($id);
114    }
115
116    /**
117     */
118    public function __get($key)
119    {
120        switch ($key) {
121        case 'changed':
122            return $this->_changed;
123
124        case 'client_ob':
125            return $this->init
126                ? $this->_ob
127                : null;
128
129        case 'config':
130            return isset($this->_config)
131                ? $this->_config
132                : new Horde_Support_Stub();
133
134        case 'init':
135            return isset($this->_ob);
136
137        case 'max_compose_bodysize':
138        case 'max_compose_recipients':
139        case 'max_compose_timelimit':
140            $perm = $GLOBALS['injector']->getInstance('Horde_Perms')->getPermissions('imp:' . str_replace('max_compose', 'max', $key), $GLOBALS['registry']->getAuth());
141            return intval($perm[0]);
142
143        case 'max_create_mboxes':
144            $perm = $GLOBALS['injector']->getInstance('Horde_Perms')->getPermissions('imp:' . $this->_getPerm($key), $GLOBALS['registry']->getAuth());
145            return intval($perm[0]);
146
147        case 'server_key':
148            return $this->init
149                ? $this->_ob->getParam('imp:backend')
150                : null;
151
152        case 'thread_algo':
153            if (!$this->init) {
154                return 'ORDEREDSUBJECT';
155            }
156
157            if ($thread = $this->_ob->getParam('imp:thread_algo')) {
158                return $thread;
159            }
160
161            $thread = $this->config->thread;
162            $thread_cap = $this->queryCapability('THREAD');
163            if (!in_array($thread, is_array($thread_cap) ? $thread_cap : array())) {
164                $thread = 'ORDEREDSUBJECT';
165            }
166
167            $this->_ob->setParam('imp:thread_algo', $thread);
168            $this->_changed = true;
169
170            return $thread;
171
172        case 'url':
173            $url = new Horde_Imap_Client_Url();
174            if ($this->init) {
175                $url->hostspec = $this->getParam('hostspec');
176                $url->port = $this->getParam('port');
177                $url->protocol = $this->isImap() ? 'imap' : 'pop';
178            }
179            return $url;
180        }
181    }
182
183    /**
184     */
185    public function __toString()
186    {
187        return $this->_id;
188    }
189
190    /**
191     * Get the full permission name for a permission.
192     *
193     * @param string $perm  The permission.
194     *
195     * @return string  The full (backend-specific) permission name.
196     */
197    protected function _getPerm($perm)
198    {
199        return 'backends:' . ($this->init ? $this->server_key . ':' : '') . $perm;
200    }
201
202    /**
203     * Determine if this is a connection to an IMAP server.
204     *
205     * @return boolean  True if connected to IMAP server.
206     */
207    public function isImap()
208    {
209        return ($this->init &&
210                ($this->_ob instanceof Horde_Imap_Client_Socket));
211    }
212
213    /**
214     * Determine if this is a connection to an IMAP server.
215     *
216     * @return boolean  True if connected to IMAP server.
217     */
218    public function isPop3()
219    {
220        return ($this->init &&
221                ($this->_ob instanceof Horde_Imap_Client_Socket_Pop3));
222    }
223
224    /**
225     * Create the base Horde_Imap_Client object (from an entry in
226     * backends.php).
227     *
228     * @param string $username  The username to authenticate with.
229     * @param string $password  The password to authenticate with.
230     * @param string $skey      Create a new object using this server key.
231     *
232     * @return Horde_Imap_Client_Base  Client object.
233     * @throws IMP_Imap_Exception
234     */
235    public function createBaseImapObject($username, $password, $skey)
236    {
237        if ($this->init) {
238            return $this->client_ob;
239        }
240
241        if (($config = $this->loadServerConfig($skey)) === false) {
242            $error = new IMP_Imap_Exception('Could not load server configuration.');
243            Horde::log($error);
244            throw $error;
245        }
246
247        $imap_config = array(
248            'hostspec' => $config->hostspec,
249            'id' => $config->id,
250            'password' => new IMP_Imap_Password($password),
251            'port' => $config->port,
252            'secure' => (($secure = $config->secure) ? $secure : false),
253            'username' => $username,
254            // IMP specific config
255            'imp:backend' => $skey
256        );
257
258        /* Needed here to set config information in createImapObject(). */
259        $this->_config = $config;
260
261        try {
262            return $this->createImapObject($imap_config, ($config->protocol == 'imap'));
263        } catch (IMP_Imap_Exception $e) {
264            unset($this->_config);
265            throw $e;
266        }
267    }
268
269    /**
270     * Create a Horde_Imap_Client object.
271     *
272     * @param array $config  The IMAP configuration.
273     * @param boolean $imap  True if IMAP connection, false if POP3.
274     *
275     * @return Horde_Imap_Client_Base  Client object.
276     * @throws IMP_Imap_Exception
277     */
278    public function createImapObject($config, $imap = true)
279    {
280        if ($this->init) {
281            return $this->_ob;
282        }
283
284        $sconfig = $this->config;
285        $config = array_merge(array(
286            'cache' => $sconfig->cache_params,
287            'capability_ignore' => $sconfig->capability_ignore,
288            'comparator' => $sconfig->comparator,
289            'debug' => $sconfig->debug,
290            'debug_literal' => $sconfig->debug_raw,
291            'lang' => $sconfig->lang,
292            'timeout' => $sconfig->timeout,
293            // 'imp:login' - Set in __call()
294        ), $config);
295
296        try {
297            $this->_ob = $imap
298                ? new Horde_Imap_Client_Socket($config)
299                : new Horde_Imap_Client_Socket_Pop3($config);
300            return $this->_ob;
301        } catch (Horde_Imap_Client_Exception $e) {
302            Horde::log($e->raw_msg);
303            throw new IMP_Imap_Exception($e);
304        }
305    }
306
307    /**
308     * Perform post-login tasks.
309     */
310    public function doPostLoginTasks()
311    {
312        global $prefs;
313
314        switch ($this->_config->protocol) {
315        case 'imap':
316            /* Overwrite default special mailbox names. */
317            foreach ($this->_config->special_mboxes as $key => $val) {
318                if ($key != IMP_Mailbox::MBOX_USERSPECIAL) {
319                    $prefs->setValue($key, $val, array(
320                        'force' => true,
321                        'nosave' => true
322                    ));
323                }
324            }
325            break;
326
327        case 'pop':
328            /* Turn some options off if we are working with POP3. */
329            foreach (array('newmail_notify', 'save_sent_mail') as $val) {
330                $prefs->setValue($val, false, array(
331                    'force' => true,
332                    'nosave' => true
333                ));
334                $prefs->setLocked($val, true);
335            }
336            $prefs->setLocked(IMP_Mailbox::MBOX_DRAFTS, true);
337            $prefs->setLocked(IMP_Mailbox::MBOX_SENT, true);
338            $prefs->setLocked(IMP_Mailbox::MBOX_SPAM, true);
339            $prefs->setLocked(IMP_Mailbox::MBOX_TEMPLATES, true);
340            $prefs->setLocked(IMP_Mailbox::MBOX_TRASH, true);
341            break;
342        }
343
344        $this->updateFetchIgnore();
345    }
346
347    /**
348     * Update the list of mailboxes to ignore when caching FETCH data in the
349     * IMAP client object.
350     */
351    public function updateFetchIgnore()
352    {
353        if ($this->isImap()) {
354            $special = IMP_Mailbox::getSpecialMailboxes();
355            $cache = $this->_ob->getParam('cache');
356            $cache['fetch_ignore'] = array_filter(array(
357                strval($special[IMP_Mailbox::SPECIAL_SPAM]),
358                strval($special[IMP_Mailbox::SPECIAL_TRASH])
359            ));
360            $this->_ob->setParam('cache', $cache);
361        }
362    }
363
364    /**
365     * Checks access rights for a server.
366     *
367     * @param integer $right  Access right.
368     *
369     * @return boolean  Does the access right exist?
370     */
371    public function access($right)
372    {
373        global $injector;
374
375        if (!$this->init) {
376            return false;
377        }
378
379        switch ($right) {
380        case self::ACCESS_ACL:
381            return ($this->config->acl && $this->queryCapability('ACL'));
382
383        case self::ACCESS_CREATEMBOX:
384            return ($this->isImap() &&
385                    $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('create_mboxes')));
386
387        case self::ACCESS_CREATEMBOX_MAX:
388            return ($this->isImap() &&
389                    $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('max_create_mboxes')));
390
391        case self::ACCESS_DRAFTS:
392        case self::ACCESS_FLAGS:
393        case self::ACCESS_IMPORT:
394        case self::ACCESS_SEARCH:
395        case self::ACCESS_UNSEEN:
396            return $this->isImap();
397
398        case self::ACCESS_FOLDERS:
399        case self::ACCESS_TRASH:
400            return ($this->isImap() &&
401                    $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('allow_folders')));
402
403        case self::ACCESS_REMOTE:
404            return $injector->getInstance('Horde_Core_Perms')->hasAppPermission($this->_getPerm('allow_remote'));
405
406        case self::ACCESS_SORT:
407            return ($this->isImap() &&
408                    ($this->config->sort_force || $this->_ob->queryCapability('SORT')));
409        }
410
411        return false;
412    }
413
414    /**
415     * Checks compose access rights for a server.
416     *
417     * @param integer $right  Access right.
418     * @param integer $data   Data required to check the rights:
419     * <pre>
420     *   - ACCESS_COMPOSE_BODYSIZE
421     *     The size of the body data.
422     *
423     *   - ACCESS_COMPOSE_RECIPIENTS
424     *   - ACCESS_COMPOSE_TIMELIMIT
425     *     The number of e-mail recipients.
426     * </pre>
427     *
428     * @return boolean  Is the access allowed?
429     */
430    public function accessCompose($right, $data)
431    {
432        switch ($right) {
433        case self::ACCESS_COMPOSE_BODYSIZE:
434            $perm_name = 'max_bodysize';
435            break;
436
437        case self::ACCESS_COMPOSE_RECIPIENTS:
438            $perm_name = 'max_recipients';
439            break;
440
441        case self::ACCESS_COMPOSE_TIMELIMIT:
442            $perm_name = 'max_timelimit';
443            break;
444
445        default:
446            return false;
447        }
448
449        return $GLOBALS['injector']->getInstance('Horde_Core_Perms')->hasAppPermission(
450            $perm_name,
451            array(
452                'opts' => array(
453                    'value' => $data
454                )
455            )
456        );
457    }
458
459    /**
460     * Get namespace info for a full mailbox path.
461     *
462     * @param string $mailbox    The mailbox path. (self:NS_DEFAULT will
463     *                           return the default personal namespace.)
464     * @param boolean $personal  If true, will return empty namespace only
465     *                           if it is a personal namespace.
466     *
467     * @return mixed  The namespace info for the mailbox path or null if the
468     *                path doesn't exist.
469     */
470    public function getNamespace($mailbox, $personal = false)
471    {
472        if ($this->isImap()) {
473            $ns = $this->getNamespaces();
474            if ($mailbox !== self::NS_DEFAULT) {
475                return $ns->getNamespace($mailbox, $personal);
476            }
477
478            foreach ($ns as $val) {
479                if ($val->type === $val::NS_PERSONAL) {
480                    return $val;
481                }
482            }
483        }
484
485        return null;
486    }
487
488    /**
489     * Return the cache ID for this mailbox.
490     *
491     * @param string $mailbox  The mailbox name (UTF-8).
492     * @param array $addl      Local IMP metadata to add to the cache ID.
493     *
494     * @return string  The cache ID.
495     */
496    public function getCacheId($mailbox, array $addl = array())
497    {
498        return $this->getSyncToken($mailbox) .
499            (empty($addl) ? '' : ('|' . implode('|', $addl)));
500    }
501
502    /**
503     * Parses the cache ID for this mailbox.
504     *
505     * @param string $id  Cache ID generated by getCacheId().
506     *
507     * @return array  Two element array:
508     *   - date: (integer) Date information (day of year), if embedded in
509     *           cache ID.
510     *   - token: (string) Mailbox sync token.
511     */
512    public function parseCacheId($id)
513    {
514        $out = array('date' => null);
515
516        if ((($pos = strrpos($id, '|')) !== false) &&
517            (substr($id, $pos + 1, 1) == 'D')) {
518            $out['date'] = substr($id, $pos + 2);
519        }
520
521        $out['token'] = (($pos = strpos($id, '|')) === false)
522            ? $id
523            : substr($id, 0, $pos);
524
525        return $out;
526    }
527
528    /**
529     * Returns a list of messages, split into slices based on the total
530     * message size.
531     *
532     * @param string $mbox                IMAP mailbox.
533     * @param Horde_Imap_Client_Ids $ids  ID list.
534     * @param integer $size               Maximum size of a slice.
535     *
536     * @return array  An array of Horde_Imap_Client_Ids objects.
537     */
538    public function getSlices(
539        $mbox, Horde_Imap_Client_Ids $ids, $size = 5242880
540    )
541    {
542        $imp_imap = IMP_Mailbox::get($mbox)->imp_imap;
543
544        $query = new Horde_Imap_Client_Fetch_Query();
545        $query->size();
546
547        try {
548            $res = $imp_imap->fetch($mbox, $query, array(
549                'ids' => $ids,
550                'nocache' => true
551            ));
552        } catch (IMP_Imap_Exception $e) {
553            return array();
554        }
555
556        $curr = $slices = array();
557        $curr_size = 0;
558
559        foreach ($res as $key => $val) {
560            $curr_size += $val->getSize();
561            if ($curr_size > $size) {
562                $slices[] = $imp_imap->getIdsOb($curr, $ids->sequence);
563                $curr = array();
564            }
565            $curr[] = $key;
566        }
567
568        $slices[] = $imp_imap->getIdsOb($curr, $ids->sequence);
569
570        return $slices;
571    }
572
573    /**
574     * Handle status() calls. This call may hit multiple servers.
575     *
576     * @see Horde_Imap_Client_Base#status()
577     */
578    protected function _status($args)
579    {
580        global $injector;
581
582        $accounts = $mboxes = $out = array();
583        $imap_factory = $injector->getInstance('IMP_Factory_Imap');
584
585        foreach (IMP_Mailbox::get($args[0]) as $val) {
586            if ($raccount = $val->remote_account) {
587                $accounts[strval($raccount)] = $raccount;
588            }
589            $mboxes[strval($raccount)][] = $val;
590        }
591
592        foreach ($mboxes as $key => $val) {
593            $imap = $imap_factory->create($key);
594            if ($imap->init) {
595                foreach (call_user_func_array(array($imap, 'impStatus'), array($val) + $args) as $key2 => $val2) {
596                    $out[isset($accounts[$key]) ? $accounts[$key]->mailbox($key2) : $key2] = $val2;
597                }
598            }
599        }
600
601        return $out;
602    }
603
604    /**
605     * All other calls to this class are routed to the underlying
606     * Horde_Imap_Client_Base object.
607     *
608     * @param string $method  Method name.
609     * @param array $params   Method parameters.
610     *
611     * @return mixed  The return from the requested method.
612     * @throws BadMethodCallException
613     * @throws IMP_Imap_Exception
614     */
615    public function __call($method, $params)
616    {
617        global $injector;
618
619        if (!$this->init) {
620            /* Fallback for these methods. */
621            switch ($method) {
622            case 'getIdsOb':
623                $ob = new Horde_Imap_Client_Ids();
624                call_user_func_array(array($ob, 'add'), $params);
625                return $ob;
626            }
627
628            throw new Horde_Exception_AuthenticationFailure(
629                'IMP is marked as authenticated, but no credentials can be found in the session.',
630                Horde_Auth::REASON_SESSION
631            );
632        }
633
634        switch ($method) {
635        case 'append':
636        case 'createMailbox':
637        case 'deleteMailbox':
638        case 'expunge':
639        case 'fetch':
640        case 'getACL':
641        case 'getMetadata':
642        case 'getMyACLRights':
643        case 'getQuota':
644        case 'getQuotaRoot':
645        case 'getSyncToken':
646        case 'setMetadata':
647        case 'setQuota':
648        case 'store':
649        case 'subscribeMailbox':
650        case 'sync':
651        case 'thread':
652            // Horde_Imap_Client_Mailbox: these calls all have the mailbox as
653            // their first parameter.
654            $params[0] = IMP_Mailbox::getImapMboxOb($params[0]);
655            break;
656
657        case 'copy':
658        case 'renameMailbox':
659            // These calls may hit multiple servers.
660            $source = IMP_Mailbox::get($params[0]);
661            $dest = IMP_Mailbox::get($params[1]);
662            if ($source->remote_account != $dest->remote_account) {
663                return call_user_func_array(array($this, '_' . $method), $params);
664            }
665
666            // Horde_Imap_Client_Mailbox: these calls all have the mailbox as
667            // their first two parameters.
668            $params[0] = $source->imap_mbox_ob;
669            $params[1] = $dest->imap_mbox_ob;
670            break;
671
672        case 'getNamespaces':
673            if (isset($this->_temp['ns'])) {
674                return $this->_temp['ns'];
675            }
676            $nsconfig = $this->config->namespace;
677            $params[0] = is_null($nsconfig) ? array() : $nsconfig;
678            $params[1] = array('ob_return' => true);
679            break;
680
681        case 'impStatus':
682            /* Internal method: allows status call with array of mailboxes,
683             * guaranteeing they are all on this server. */
684            $params[0] = IMP_Mailbox::getImapMboxOb($params[0]);
685            $method = 'status';
686            break;
687
688        case 'openMailbox':
689            $mbox = IMP_Mailbox::get($params[0]);
690            if ($mbox->search) {
691                /* Can't open a search mailbox. */
692                return;
693            }
694            $params[0] = $mbox->imap_mbox_ob;
695            break;
696
697        case 'search':
698            $params = call_user_func_array(array($this, '_search'), $params);
699            break;
700
701        case 'status':
702            if (is_array($params[0])) {
703                return $this->_status($params);
704            }
705            $params[0] = IMP_Mailbox::getImapMboxOb($params[0]);
706            break;
707
708        default:
709            if (!method_exists($this->_ob, $method)) {
710                throw new BadMethodCallException(
711                    sprintf('%s: Invalid method call "%s".', __CLASS__, $method)
712                );
713            }
714            break;
715        }
716
717        try {
718            $result = call_user_func_array(array($this->_ob, $method), $params);
719        } catch (Horde_Imap_Client_Exception $e) {
720            $error = new IMP_Imap_Exception($e);
721
722            if (!$error->authError()) {
723                switch ($method) {
724                case 'getNamespaces':
725                    return new Horde_Imap_Client_Namespace_List();
726                }
727            }
728
729            Horde::log(
730                new Exception(
731                    sprintf('[%s] %s', $method, $e->raw_msg),
732                    $e->getCode(),
733                    $e
734                ),
735                'WARN'
736            );
737
738            throw $error;
739        }
740
741        /* Special handling for various methods. */
742        switch ($method) {
743        case 'createMailbox':
744        case 'deleteMailbox':
745        case 'renameMailbox':
746            $injector->getInstance('IMP_Mailbox_SessionCache')->expire(
747                null,
748                // Mailbox is first parameter.
749                IMP_Mailbox::get($params[0])
750            );
751            break;
752
753        case 'getNamespaces':
754            $this->_temp['ns'] = $result;
755            break;
756
757        case 'login':
758            if (!$this->_ob->getParam('imp:login')) {
759                /* Check for POP3 UIDL support. */
760                if ($this->isPop3() && !$this->queryCapability('UIDL')) {
761                    Horde::log(
762                        sprintf(
763                            'The POP3 server does not support the REQUIRED UIDL capability. [server key: %s]',
764                            $this->server_key
765                        ),
766                        'CRIT'
767                    );
768                    throw new Horde_Exception_AuthenticationFailure(
769                        _("The mail server is not currently avaliable."),
770                        Horde_Auth::REASON_MESSAGE
771                    );
772                }
773
774                $this->_ob->setParam('imp:login', true);
775                $this->_changed = true;
776            }
777            break;
778
779        case 'setACL':
780            $injector->getInstance('IMP_Mailbox_SessionCache')->expire(
781                IMP_Mailbox_SessionCache::CACHE_ACL,
782                IMP_Mailbox::get($params[0])
783            );
784            break;
785        }
786
787        return $result;
788    }
789
790    /**
791     * Prepares an IMAP search query.  Needed because certain configuration
792     * parameters may need to be dynamically altered before passed to the
793     * Imap_Client object.
794     *
795     * @param string $mailbox                        The mailbox to search.
796     * @param Horde_Imap_Client_Search_Query $query  The search query object.
797     * @param array $opts                            Additional options.
798     *
799     * @return array  Parameters to use in the search() call.
800     */
801    protected function _search($mailbox, $query = null, array $opts = array())
802    {
803        $mailbox = IMP_Mailbox::get($mailbox);
804
805        if (!empty($opts['sort']) && $mailbox->access_sort) {
806            /* If doing a from/to search, use display sorting if possible.
807             * Although there is a fallback to a PHP-based display sort, for
808             * performance reasons only do a display sort if it is supported
809             * on the server. */
810            foreach ($opts['sort'] as $key => $val) {
811                switch ($val) {
812                case Horde_Imap_Client::SORT_FROM:
813                    $opts['sort'][$key] = Horde_Imap_Client::SORT_DISPLAYFROM_FALLBACK;
814                    break;
815
816                case Horde_Imap_Client::SORT_TO:
817                    $opts['sort'][$key] = Horde_Imap_Client::SORT_DISPLAYTO_FALLBACK;
818                    break;
819                }
820            }
821        }
822
823        if (!is_null($query)) {
824            $query->charset('UTF-8', false);
825        }
826
827        return array($mailbox->imap_mbox_ob, $query, $opts);
828    }
829
830    /**
831     * Handle copy() calls that hit multiple servers.
832     *
833     * @see Horde_Imap_Client_Base#copy()
834     */
835    protected function _copy()
836    {
837        global $injector;
838
839        $args = func_get_args();
840        $imap_factory = $injector->getInstance('IMP_Factory_Imap');
841        $source_imap = $imap_factory->create($args[0]);
842        $dest_imap = $imap_factory->create($args[1]);
843
844        $create = !empty($args[2]['create']);
845        $ids = isset($args[2]['ids'])
846            ? $args[2]['ids']
847            : $source_imap->getIdsOb(Horde_Imap_Client_Ids::ALL);
848        $move = !empty($args[2]['move']);
849        $retval = true;
850
851        $query = new Horde_Imap_Client_Fetch_Query();
852        $query->fullText(array(
853            'peek' => true
854        ));
855
856        foreach ($this->getSlices($args[0], $ids) as $val) {
857            try {
858                $res = $source_imap->fetch($args[0], $query, array(
859                    'ids' => $val,
860                    'nocache' => true
861                ));
862
863                $append = array();
864                foreach ($res as $msg) {
865                    $append[] = array(
866                        'data' => $msg->getFullMsg(true)
867                    );
868                }
869
870                $dest_imap->append($args[1], $append, array(
871                    'create' => $create
872                ));
873
874                if ($move) {
875                    $source_imap->expunge($args[0], array(
876                        'delete' => true,
877                        'ids' => $val
878                    ));
879                }
880            } catch (IMP_Imap_Exception $e) {
881                $retval = false;
882            }
883        }
884
885        return $retval;
886    }
887
888    /**
889     * Handle copy() calls. This call may hit multiple servers, so
890     * need to handle separately from other IMAP calls.
891     *
892     * @see Horde_Imap_Client_Base#renameMailbox()
893     */
894    protected function _renameMailbox()
895    {
896        $args = func_get_args();
897        $source = IMP_Mailbox::get($args[0]);
898
899        if ($source->create() && $this->copy($source, $args[1])) {
900            $source->delete();
901        } else {
902            throw new IMP_Imap_Exception(_("Could not move all messages between mailboxes, so the original mailbox was not removed."));
903        }
904    }
905
906    /* Static methods. */
907
908    /**
909     * Loads the IMP server configuration from backends.php.
910     *
911     * @param string $server  Returns this labeled entry only.
912     *
913     * @return mixed  If $server is set return this entry; else, return the
914     *                entire servers array. Returns false on error.
915     */
916    static public function loadServerConfig($server = null)
917    {
918        global $registry;
919
920        if (empty(self::$_backends)) {
921            try {
922                $s = $registry->loadConfigFile('backends.php', 'servers', 'imp')->config['servers'];
923            } catch (Horde_Exception $e) {
924                Horde::log($e, 'ERR');
925                return false;
926            }
927
928            foreach ($s as $key => $val) {
929                if (empty($val['disabled'])) {
930                    self::$_backends[$key] = new IMP_Imap_Config($val);
931                }
932            }
933        }
934
935        return is_null($server)
936            ? self::$_backends
937            : (isset(self::$_backends[$server]) ? self::$_backends[$server] : false);
938    }
939
940    /* Serializable methods. */
941
942    /**
943     */
944    public function serialize()
945    {
946        return $GLOBALS['injector']->getInstance('Horde_Pack')->pack(
947            array(
948                $this->_ob,
949                $this->_id,
950                $this->_config
951            ),
952            array(
953                'compression' => false,
954                'phpob' => true
955            )
956        );
957    }
958
959    /**
960     */
961    public function unserialize($data)
962    {
963        list(
964            $this->_ob,
965            $this->_id,
966            $this->_config
967        ) = $GLOBALS['injector']->getInstance('Horde_Pack')->unpack($data);
968    }
969
970}
971