1<?php
2/**
3 * An IMAP based driver for accessing Kolab storage.
4 *
5 * PHP version 5
6 *
7 * @category Kolab
8 * @package  Kolab_Storage
9 * @author   Gunnar Wrobel <wrobel@pardus.de>
10 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
11 */
12
13/**
14 * The IMAP driver class for accessing Kolab storage.
15 *
16 * Copyright 2009-2017 Horde LLC (http://www.horde.org/)
17 *
18 * See the enclosed file COPYING for license information (LGPL). If you
19 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
20 *
21 * @category Kolab
22 * @package  Kolab_Storage
23 * @author   Gunnar Wrobel <wrobel@pardus.de>
24 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
25 */
26class Horde_Kolab_Storage_Driver_Imap
27extends Horde_Kolab_Storage_Driver_Base
28{
29    /**
30     * Create the backend driver.
31     *
32     * @return mixed The backend driver.
33     */
34    public function createBackend()
35    {
36        $config = $this->getParams();
37        $config['hostspec'] = $config['host'];
38        unset($config['host']);
39        if (isset($config['debug']) && $config['debug'] == 'STDOUT') {
40            $config['debug'] = STDOUT;
41        }
42        return new Horde_Imap_Client_Socket($config);
43    }
44
45    /**
46     * Retrieves a list of folders from the server.
47     *
48     * @return array The list of folders.
49     */
50    public function listFolders()
51    {
52        try {
53            return $this->getBackend()->listMailboxes(
54                '*', Horde_Imap_Client::MBOX_ALL, array('flat' => true));
55        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
56            throw new Horde_Kolab_Storage_Exception($e->details);
57        } catch (Horde_Imap_Client_Exception $e) {
58            throw new Horde_Kolab_Storage_Exception($e);
59        }
60    }
61
62    /**
63     * Create the specified folder.
64     *
65     * @param string $folder The folder to create.
66     *
67     * @return NULL
68     */
69    public function create($folder)
70    {
71        try {
72            return $this->getBackend()->createMailbox($folder);
73        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
74            throw new Horde_Kolab_Storage_Exception($e->details);
75        } catch (Horde_Imap_Client_Exception $e) {
76            throw new Horde_Kolab_Storage_Exception($e);
77        }
78    }
79
80    /**
81     * Delete the specified folder.
82     *
83     * @param string $folder  The folder to delete.
84     *
85     * @return NULL
86     */
87    public function delete($folder)
88    {
89        try {
90            $this->getBackend()->deleteMailbox($folder);
91        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
92            throw new Horde_Kolab_Storage_Exception($e->details);
93        } catch (Horde_Imap_Client_Exception $e) {
94            throw new Horde_Kolab_Storage_Exception($e);
95        }
96    }
97
98    /**
99     * Rename the specified folder.
100     *
101     * @param string $old  The folder to rename.
102     * @param string $new  The new name of the folder.
103     *
104     * @return NULL
105     */
106    public function rename($old, $new)
107    {
108        try {
109            $this->getBackend()->renameMailbox($old, $new);
110        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
111            throw new Horde_Kolab_Storage_Exception($e->details);
112        } catch (Horde_Imap_Client_Exception $e) {
113            throw new Horde_Kolab_Storage_Exception($e);
114        }
115    }
116
117    /**
118     * Does the backend support ACL?
119     *
120     * @return boolean True if the backend supports ACLs.
121     */
122    public function hasAclSupport()
123    {
124        try {
125            return $this->getBackend()->queryCapability('ACL');
126        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
127            throw new Horde_Kolab_Storage_Exception($e->details);
128        } catch (Horde_Imap_Client_Exception $e) {
129            throw new Horde_Kolab_Storage_Exception($e);
130        }
131    }
132
133    /**
134     * Retrieve the access rights for a folder.
135     *
136     * @param Horde_Kolab_Storage_Folder $folder The folder to retrieve the ACL for.
137     *
138     * @return An array of rights.
139     */
140    public function getAcl($folder)
141    {
142        try {
143            $acl = $this->getBackend()->getACL($folder);
144        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
145            throw new Horde_Kolab_Storage_Exception($e->details);
146        } catch (Horde_Imap_Client_Exception $e) {
147            throw new Horde_Kolab_Storage_Exception($e);
148        }
149
150        $result = array();
151        foreach ($acl as $user => $rights) {
152            $result[$user] = strval($rights);
153        }
154
155        return $result;
156    }
157
158    /**
159     * Retrieve the access rights the current user has on a folder.
160     *
161     * @param string $folder The folder to retrieve the user ACL for.
162     *
163     * @return string The user rights.
164     */
165    public function getMyAcl($folder)
166    {
167        try {
168            return strval($this->getBackend()->getMyACLRights($folder));
169        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
170            throw new Horde_Kolab_Storage_Exception($e->details);
171        } catch (Horde_Imap_Client_Exception $e) {
172            throw new Horde_Kolab_Storage_Exception($e);
173        }
174    }
175
176    /**
177     * Set the access rights for a folder.
178     *
179     * @param string $folder  The folder to act upon.
180     * @param string $user    The user to set the ACL for.
181     * @param string $acl     The ACL.
182     *
183     * @return NULL
184     */
185    public function setAcl($folder, $user, $acl)
186    {
187        try {
188            $this->getBackend()->setACL($folder, $user, array('rights' => $acl));
189        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
190            throw new Horde_Kolab_Storage_Exception($e->details);
191        } catch (Horde_Imap_Client_Exception $e) {
192            throw new Horde_Kolab_Storage_Exception($e);
193        }
194    }
195
196    /**
197     * Delete the access rights for user on a folder.
198     *
199     * @param string $folder  The folder to act upon.
200     * @param string $user    The user to delete the ACL for
201     *
202     * @return NULL
203     */
204    public function deleteAcl($folder, $user)
205    {
206        try {
207            $this->getBackend()->deleteACL($folder, $user);
208        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
209            throw new Horde_Kolab_Storage_Exception($e->details);
210        } catch (Horde_Imap_Client_Exception $e) {
211            throw new Horde_Kolab_Storage_Exception($e);
212        }
213    }
214
215    /**
216     * Retrieves the specified annotation for the complete list of folders.
217     *
218     * @param string $annotation The name of the annotation to retrieve.
219     *
220     * @return array An associative array combining the folder names as key with
221     *               the corresponding annotation value.
222     */
223    public function listAnnotation($annotation)
224    {
225        $data = array();
226
227        try {
228            foreach ($this->listFolders() as $val) {
229                if (strlen($res = $this->getAnnotation((string)$val, $annotation))) {
230                    $data[(string)$val] = $res;
231                }
232            }
233        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
234            throw new Horde_Kolab_Storage_Exception($e->details);
235        } catch (Horde_Imap_Client_Exception $e) {
236            throw new Horde_Kolab_Storage_Exception($e);
237        }
238
239        return $data;
240    }
241
242    /**
243     * Fetches the annotation from a folder.
244     *
245     * @param string $folder    The name of the folder.
246     * @param string $annotation The annotation to get.
247     *
248     * @return string The annotation value.
249     */
250    public function getAnnotation($folder, $annotation)
251    {
252        try {
253            $result = $this->getBackend()->getMetadata($folder, $annotation);
254        } catch (Exception $e) {
255            return '';
256        }
257        return isset($result[$folder][$annotation]) ? $result[$folder][$annotation] : '';
258    }
259
260    /**
261     * Sets the annotation on a folder.
262     *
263     * @param string $folder    The name of the folder.
264     * @param string $annotation The annotation to set.
265     * @param array  $value      The values to set
266     *
267     * @return NULL
268     */
269    public function setAnnotation($folder, $annotation, $value)
270    {
271        try {
272            return $this->getBackend()->setMetadata(
273                $folder, array($annotation => $value)
274            );
275        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
276            throw new Horde_Kolab_Storage_Exception($e->details);
277        } catch (Horde_Imap_Client_Exception $e) {
278            throw new Horde_Kolab_Storage_Exception($e);
279        }
280    }
281
282    /**
283     * Retrieve the namespace information for this connection.
284     *
285     * @return Horde_Kolab_Storage_Driver_Namespace The initialized namespace handler.
286     */
287    public function getNamespace()
288    {
289        if ($this->_namespace !== null) {
290            return parent::getNamespace();
291        }
292        try {
293            $this->getBackend()->login();
294            if ($this->getBackend()->queryCapability('NAMESPACE') === true) {
295                $c = array();
296                $configuration = $this->getParam('namespaces', array());
297                foreach ($this->getBackend()->getNamespaces() as $namespace) {
298                    if (in_array($namespace['name'], array_keys($configuration))) {
299                        $namespace = array_merge($namespace, $configuration[$namespace['name']]);
300                    }
301
302                    switch ($namespace['type']) {
303                    case Horde_Imap_Client::NS_PERSONAL:
304                        $namespace['type'] = Horde_Kolab_Storage_Folder_Namespace::PERSONAL;
305                        break;
306
307                    case Horde_Imap_Client::NS_OTHER:
308                        $namespace['type'] = Horde_Kolab_Storage_Folder_Namespace::OTHER;
309                        break;
310
311                    case Horde_Imap_Client::NS_SHARED:
312                        $namespace['type'] = Horde_Kolab_Storage_Folder_Namespace::SHARED;
313                        break;
314                    }
315
316                    $c[] = $namespace;
317                }
318                $this->_namespace = $this->getFactory()->createNamespace('imap', $this->getAuth(), $c);
319            }
320        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
321            throw new Horde_Kolab_Storage_Exception($e->details);
322        } catch (Horde_Imap_Client_Exception $e) {
323            throw new Horde_Kolab_Storage_Exception($e);
324        }
325
326        return parent::getNamespace();
327    }
328
329    /**
330     * Opens the given folder.
331     *
332     * @param string $folder The folder to open
333     *
334     * @return NULL
335     */
336    public function select($folder, $mode = Horde_Imap_Client::OPEN_AUTO)
337    {
338        try {
339            $this->getBackend()->openMailbox($folder, $mode);
340        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
341            throw new Horde_Kolab_Storage_Exception($e->details);
342        } catch (Horde_Imap_Client_Exception $e) {
343            throw new Horde_Kolab_Storage_Exception($e);
344        }
345    }
346
347    /**
348     * Returns the status of the current folder.
349     *
350     * @param string $folder Check the status of this folder.
351     *
352     * @return array An array that contains 'uidvalidity', 'uidnext', and
353     *               'token'.
354     */
355    public function status($folder)
356    {
357        try {
358            $status = $this->getBackend()->status(
359                $folder,
360                Horde_Imap_Client::STATUS_UIDNEXT |
361                Horde_Imap_Client::STATUS_UIDVALIDITY |
362                Horde_Imap_Client::STATUS_FORCE_REFRESH
363            );
364            $status['token'] = $this->getBackend()->getSyncToken($folder);
365        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
366            throw new Horde_Kolab_Storage_Exception($e->details);
367        } catch (Horde_Imap_Client_Exception $e) {
368            throw new Horde_Kolab_Storage_Exception($e);
369        }
370
371        return $status;
372    }
373
374    /**
375     * Synchrozine using a token provided by the IMAP client.
376     *
377     * @param string $folder  The folder to synchronize.
378     * @param string $token   The sync token provided by the IMAP client.
379     * @param array  $ids     The list of IMAP message UIDs we currently know
380     *                        about. If omitted, the server will return
381     *                        VANISHED data only if it supports QRESYNC.
382     *
383     * @return array  An array containing the following keys and values:
384     *   Horde_Kolab_Storage_Folder_Stamp_Uids::DELETED - Contains the UIDs that
385     *       have VANISHED from the IMAP server.
386     *   Horde_Kolab_Storage_Folder_Stamp_Uids::ADDED   - Contains the UIDs that
387     *       have been added to the IMAP server since the last sync.
388     */
389    public function sync($folder, $token, array $ids = array())
390    {
391        $mbox = new Horde_Imap_Client_Mailbox($folder);
392        $options = array('ids' => new Horde_Imap_Client_Ids($ids));
393        $sync_data = $this->getBackend()->sync($mbox, $token, $options);
394        if ($sync_data->flags) {
395            // Flag changes, we must check for /deleted since some Kolab clients
396            // like e.g., Kontact only flag as /deleted and do not automatially
397            // expunge.
398            $query = new Horde_Imap_Client_Search_Query();
399            $query->flag(Horde_Imap_Client::FLAG_DELETED);
400            $query->ids($sync_data->flagsuids);
401            $search_ret = $this->getBackend()->search($mbox, $query);
402            $deleted = array_merge($sync_data->vanisheduids->ids, $search_ret['match']->ids);
403        } else {
404            $deleted = $sync_data->vanisheduids->ids;
405        }
406
407        return array(
408            Horde_Kolab_Storage_Folder_Stamp_Uids::DELETED => $deleted,
409            Horde_Kolab_Storage_Folder_Stamp_Uids::ADDED => $sync_data->newmsgsuids->ids
410        );
411    }
412
413    /**
414     * Returns a stamp for the current folder status. This stamp can be used to
415     * identify changes in the folder data. This method, as opposed to
416     * self::getStamp(), uses the IMAP client's token to calculate the changes.
417     *
418     * @param string $folder Return the stamp for this folder.
419     * @param string $token  A sync token provided by the IMAP server.
420     * @param array $ids     An array of UIDs that we know about.
421     *
422     * @return Horde_Kolab_Storage_Folder_Stamp A stamp indicating the current
423     *                                          folder status.
424     */
425    public function getStampFromToken($folder, $token, array $ids)
426    {
427        // always get folder status first, then sync()
428        $status = $this->status($folder);
429        $sync = $this->sync($folder, $token, $ids);
430
431        $ids = array_diff(
432            $ids,
433            $sync[Horde_Kolab_Storage_Folder_Stamp_Uids::DELETED]
434        );
435        $ids = array_merge(
436            $ids,
437            $sync[Horde_Kolab_Storage_Folder_Stamp_Uids::ADDED]
438        );
439        return new Horde_Kolab_Storage_Folder_Stamp_Uids(
440            $status,
441            $ids
442        );
443    }
444
445    /**
446     * Returns the message ids of the messages in this folder.
447     *
448     * @param string $folder Check the status of this folder.
449     *
450     * @return array The message ids.
451     */
452    public function getUids($folder)
453    {
454        $search_query = new Horde_Imap_Client_Search_Query();
455        $search_query->flag('DELETED', false);
456        try {
457            $uidsearch = $this->getBackend()->search($folder, $search_query);
458        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
459            throw new Horde_Kolab_Storage_Exception($e->details);
460        } catch (Horde_Imap_Client_Exception $e) {
461            throw new Horde_Kolab_Storage_Exception($e);
462        }
463        $uids = $uidsearch['match'];
464
465        return $uids->ids;
466    }
467
468    /**
469     * Retrieves a complete message.
470     *
471     * @param string $folder The folder to fetch the messages from.
472     * @param array  $uid    The message UID.
473     *
474     * @return array The message encapsuled as an array that contains a
475     *               Horde_Mime_Headers and a Horde_Mime_Part object.
476     */
477    public function fetchComplete($folder, $uid)
478    {
479        $query = new Horde_Imap_Client_Fetch_Query();
480        $query->fullText();
481
482        try {
483            $ret = $this->getBackend()->fetch(
484                $folder,
485                $query,
486                array('ids' => new Horde_Imap_Client_Ids($uid))
487            );
488
489            if (!isset($ret[$uid])) {
490                throw new Horde_Kolab_Storage_Exception(
491                    sprintf(
492                        Horde_Kolab_Storage_Translation::t(
493                            "Failed fetching message %s in folder %s."
494                        ), $uid, $folder
495                    )
496                );
497            }
498
499            $msg = $ret[$uid]->getFullMsg();
500        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
501            throw new Horde_Kolab_Storage_Exception($e->details);
502        } catch (Horde_Imap_Client_Exception $e) {
503            throw new Horde_Kolab_Storage_Exception($e);
504        }
505        return array(
506            Horde_Mime_Headers::parseHeaders($msg),
507            Horde_Mime_Part::parseMessage($msg)
508        );
509    }
510
511    /**
512     * Retrieves the message headers.
513     *
514     * @param string $folder The folder to fetch the message from.
515     * @param array  $uid    The message UID.
516     *
517     * @return Horde_Mime_Headers The message headers.
518     */
519    public function fetchHeaders($folder, $uid)
520    {
521        $query = new Horde_Imap_Client_Fetch_Query();
522        $query->headerText();
523
524        try {
525            $ret = $this->getBackend()->fetch(
526                $folder,
527                $query,
528                array('ids' => new Horde_Imap_Client_Ids($uid))
529            );
530            $msg = $ret[$uid]->getHeaderText();
531        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
532            throw new Horde_Kolab_Storage_Exception($e->details);
533        } catch (Horde_Imap_Client_Exception $e) {
534            throw new Horde_Kolab_Storage_Exception($e);
535        }
536        return Horde_Mime_Headers::parseHeaders($msg);
537    }
538
539    /**
540     * Retrieves the messages for the given message ids.
541     *
542     * @param string $folder The folder to fetch the messages from.
543     * @param array  $uids                The message UIDs.
544     *
545     * @return array An array of message structures parsed into Horde_Mime_Part
546     *               instances.
547     */
548    public function fetchStructure($folder, $uids)
549    {
550        if (empty($uids)) {
551            return array();
552        }
553
554        $query = new Horde_Imap_Client_Fetch_Query();
555        $query->structure();
556
557        try {
558            $ret = $this->getBackend()->fetch(
559                $folder,
560                $query,
561                array('ids' => new Horde_Imap_Client_Ids($uids))
562            );
563
564            $out = array();
565            foreach ($ret as $key => $result) {
566                $out[$key]['structure'] = $result->getStructure();
567            }
568        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
569            throw new Horde_Kolab_Storage_Exception($e->details);
570        } catch (Horde_Imap_Client_Exception $e) {
571            throw new Horde_Kolab_Storage_Exception($e);
572        }
573
574        return $out;
575    }
576
577    /**
578     * Retrieves a bodypart for the given message ID and mime part ID.
579     *
580     * @param string $folder The folder to fetch the messages from.
581     * @param array  $uid                 The message UID.
582     * @param array  $id                  The mime part ID.
583     *
584     * @return resource  The body part, as a stream resource. The contents are
585     *                   already transfer decoded and presented as 8bit data.
586     */
587    public function fetchBodypart($folder, $uid, $id)
588    {
589        $query = new Horde_Imap_Client_Fetch_Query();
590        $query->structure();
591        $query->bodyPart($id, array('decode' => true));
592
593        try {
594            $ret = $this->getBackend()->fetch(
595                $folder,
596                $query,
597                array('ids' => new Horde_Imap_Client_Ids($uid))
598            );
599
600            // Already decoded?
601            if ($ret[$uid]->getBodyPartDecode($id)) {
602                return $ret[$uid]->getBodyPart($id, true);
603            }
604
605            // Not already decoded, let Horde_Mime do it.
606            $part = $ret[$uid]->getStructure()->getPart($id);
607            $part->setContents(
608                $ret[$uid]->getBodyPart($id, true),
609                array(
610                    'encoding' => $ret[$uid]->getBodyPartDecode($id),
611                    'usestream' => true
612                )
613            );
614            return $part->getContents(array('stream' => true));
615        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
616            throw new Horde_Kolab_Storage_Exception($e->details);
617        } catch (Horde_Imap_Client_Exception $e) {
618            throw new Horde_Kolab_Storage_Exception($e);
619        }
620    }
621
622    /**
623     * Appends a message to the given folder.
624     *
625     * @param string   $folder  The folder to append the message(s) to.
626     * @param resource $msg     The message to append.
627     *
628     * @return mixed True or the UID of the new message in case the backend
629     *               supports UIDPLUS.
630     */
631    public function appendMessage($folder, $msg)
632    {
633        try {
634            $result = $this->getBackend()
635                ->append($folder, array(array('data' => $msg)));
636        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
637            throw new Horde_Kolab_Storage_Exception($e->details);
638        } catch (Horde_Imap_Client_Exception $e) {
639            throw new Horde_Kolab_Storage_Exception($e);
640        }
641        return $result->ids[0];
642    }
643
644    /**
645     * Deletes messages from the specified folder.
646     *
647     * @param string  $folder  The folder to delete messages from.
648     * @param integer $uids    IMAP message ids.
649     *
650     * @return NULL
651     */
652    public function deleteMessages($folder, $uids)
653    {
654        try {
655            return $this->getBackend()->store($folder, array(
656                'add' => array('\\deleted'),
657                'ids' => new Horde_Imap_Client_Ids($uids)
658            ));
659        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
660            throw new Horde_Kolab_Storage_Exception($e->details);
661        } catch (Horde_Imap_Client_Exception $e) {
662            throw new Horde_Kolab_Storage_Exception($e);
663        }
664    }
665
666    /**
667     * Moves a message to a new folder.
668     *
669     * @param integer $uid         IMAP message id.
670     * @param string  $old_folder  Source folder.
671     * @param string  $new_folder  Target folder.
672     *
673     * @return NULL
674     */
675    public function moveMessage($uid, $old_folder, $new_folder)
676    {
677        $options = array('ids' => new Horde_Imap_Client_Ids($uid),
678                         'move' => true);
679        try {
680            return $this->getBackend()
681                ->copy($old_folder, $new_folder, $options);
682        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
683            throw new Horde_Kolab_Storage_Exception($e->details);
684        } catch (Horde_Imap_Client_Exception $e) {
685            throw new Horde_Kolab_Storage_Exception($e);
686        }
687    }
688
689    /**
690     * Expunges messages in the current folder.
691     *
692     * @param string $folder The folder to expunge.
693     *
694     * @return NULL
695     */
696    public function expunge($folder)
697    {
698        try {
699            return $this->getBackend()->expunge($folder);
700        } catch (Horde_Imap_Client_Exception_ServerResponse $e) {
701            throw new Horde_Kolab_Storage_Exception($e->details);
702        } catch (Horde_Imap_Client_Exception $e) {
703            throw new Horde_Kolab_Storage_Exception($e);
704        }
705    }
706}
707