1<?php
2/**
3 * Copyright 2012-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 2012-2017 Horde LLC
10 * @license   http://www.horde.org/licenses/gpl GPL
11 * @package   IMP
12 */
13
14/**
15 * Defines AJAX actions used exclusively in the IMP dynamic view.
16 *
17 * @author    Michael Slusarz <slusarz@horde.org>
18 * @category  Horde
19 * @copyright 2012-2017 Horde LLC
20 * @license   http://www.horde.org/licenses/gpl GPL
21 * @package   IMP
22 */
23class IMP_Ajax_Application_Handler_Dynamic
24extends Horde_Core_Ajax_Application_Handler
25{
26    /**
27     * The list of actions that require readonly access to the session.
28     *
29     * @var array
30     */
31    protected $_readOnly = array(
32        'html2Text', 'text2Html'
33    );
34
35    /**
36     * AJAX action: Check access rights for creation of a submailbox.
37     *
38     * Variables used:
39     *   - mbox: (string) The name of the mailbox to check (base64url
40     *           encoded).
41     *
42     * @return object  Object with the following properties:
43     *   - result: (boolean) True if submailboxes can be created.
44     */
45    public function createMailboxPrepare()
46    {
47        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
48        $ret = new stdClass;
49        $ret->result = true;
50
51        if (!$mbox->access_creatembox) {
52            $GLOBALS['notification']->push(sprintf(_("You may not create child mailboxes in \"%s\"."), $mbox->display), 'horde.error');
53            $ret->result = false;
54        }
55
56        return $ret;
57    }
58
59    /**
60     * AJAX action: Create a mailbox.
61     *
62     * Variables used:
63     *   - mbox: (string) The name of the new mailbox.
64     *   - parent: (string) The parent mailbox (base64url encoded).
65     *
66     * @return boolean  True on success, false on failure.
67     */
68    public function createMailbox()
69    {
70        if (!isset($this->vars->mbox)) {
71            return false;
72        }
73
74        $result = false;
75
76        $parent = isset($this->vars->parent)
77            ? IMP_Mailbox::formFrom($this->vars->parent)
78            : IMP_Mailbox::get(IMP_Ftree::BASE_ELT);
79        $new_mbox = $parent->createMailboxName($this->vars->mbox);
80
81        if ($new_mbox->exists) {
82            $GLOBALS['notification']->push(sprintf(_("Mailbox \"%s\" already exists."), $new_mbox->display), 'horde.warning');
83        } elseif ($new_mbox->create()) {
84            $result = true;
85        }
86
87        return $result;
88    }
89
90    /**
91     * AJAX action: Check access rights for deletion/rename of mailbox.
92     *
93     * Variables used:
94     *   - mbox: (string) The name of the mailbox to check (base64url
95     *           encoded).
96     *   - type: (string) Either 'delete' or 'rename'.
97     *
98     * @return object  Object with the following properties:
99     *   - result: (boolean) True if mailbox can be deleted/renamed.
100     */
101    public function deleteMailboxPrepare()
102    {
103        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
104        $ret = new stdClass;
105
106        if ($mbox->access_deletembox) {
107            $ret->result = true;
108            return $ret;
109        }
110
111        switch ($this->vars->type) {
112        case 'delete':
113            $GLOBALS['notification']->push(sprintf(_("You may not delete \"%s\"."), $mbox->display), 'horde.error');
114            break;
115
116        case 'rename':
117            $GLOBALS['notification']->push(sprintf(_("You may not rename \"%s\"."), $mbox->display), 'horde.error');
118            break;
119        }
120
121        $ret->result = false;
122        return $ret;
123    }
124
125    /**
126     * AJAX action: Delete a mailbox.
127     *
128     * Variables used:
129     *   - container: (boolean) True if base element is a container.
130     *   - mbox: (string) The full mailbox name to delete (base64url encoded).
131     *   - subfolders: (boolean) Delete all subfolders?
132     *
133     * @return boolean  True on success, false on failure.
134     */
135    public function deleteMailbox()
136    {
137        return ($this->vars->mbox && IMP_Mailbox::formFrom($this->vars->mbox)->delete(array(
138            'subfolders' => !empty($this->vars->subfolders),
139            'subfolders_only' => !empty($this->vars->container)
140        )));
141    }
142
143    /**
144     * AJAX action: Rename a mailbox.
145     *
146     * Variables used:
147     *   - new_name: (string) New mailbox name (child node) (UTF-8).
148     *   - new_parent: (string) New parent name (UTF-8; base64url encoded).
149     *                 If not present, uses old parent.
150     *   - old_name: (string) Full name of old mailbox (base64url encoded).
151     *
152     * @return boolean  True on success, false on failure.
153     */
154    public function renameMailbox()
155    {
156        if (!$this->vars->old_name || !$this->vars->new_name) {
157            return false;
158        }
159
160        $old_name = IMP_Mailbox::formFrom($this->vars->old_name);
161        if (isset($this->vars->new_parent)) {
162            $parent = strlen($this->vars->new_parent)
163                ? IMP_Mailbox::formFrom($this->vars->new_parent)
164                : IMP_Mailbox::get(IMP_Ftree::BASE_ELT);
165        } else {
166            $parent = IMP_Mailbox::get($old_name->parent);
167        }
168
169        if ($parent) {
170            $new_name = $parent->createMailboxName($this->vars->new_name);
171
172            if (($old_name != $new_name) && $old_name->rename($new_name)) {
173                $this->_base->queue->setMailboxOpt('switch', $new_name->form_to);
174                return true;
175            }
176        }
177
178        return false;
179    }
180
181    /**
182     * AJAX action: Check access rights for a mailbox, and provide number of
183     * messages to be emptied.
184     *
185     * Variables used:
186     *   - mbox: (string) The name of the mailbox to check (base64url
187     *           encoded).
188     *
189     * @return object  Object with the following properties:
190     *   - result: (integer) The number of messages to be deleted.
191     */
192    public function emptyMailboxPrepare()
193    {
194        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
195        $res = new stdClass;
196        $res->result = 0;
197
198        if (!$mbox->access_empty) {
199            $GLOBALS['notification']->push(sprintf(_("The mailbox \"%s\" may not be emptied."), $mbox->display), 'horde.error');
200        } else {
201            $poll_info = $mbox->poll_info;
202            if (!($res->result = $poll_info->msgs)) {
203                $GLOBALS['notification']->push(sprintf(_("The mailbox \"%s\" is already empty."), $mbox->display), 'horde.message');
204            }
205        }
206
207        return $res;
208    }
209
210    /**
211     * AJAX action: Empty a mailbox.
212     *
213     * Variables used:
214     *   - mbox: (string) The full mailbox name to empty (base64url encoded).
215     *
216     * @return boolean  True on success, false on failure.
217     */
218    public function emptyMailbox()
219    {
220        if (!$this->vars->mbox) {
221            return false;
222        }
223
224        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
225
226        $GLOBALS['injector']->getInstance('IMP_Message')->emptyMailbox(array($mbox));
227
228        $this->_base->queue->poll($mbox);
229
230        $vp = new IMP_Ajax_Application_Viewport($mbox);
231        $vp->data_reset = 1;
232        $vp->rowlist_reset = 1;
233        $this->_base->addTask('viewport', $vp);
234
235        return true;
236    }
237
238    /**
239     * AJAX action: Flag all messages in a mailbox.
240     *
241     * Variables used:
242     *   - add: (integer) Add the flags?
243     *   - flags: (string) The IMAP flags to add/remove (JSON serialized
244     *            array).
245     *   - mbox: (string) The full mailbox name (base64url encoded).
246     *
247     * @return boolean  True on success, false on failure.
248     */
249    public function flagAll()
250    {
251        $flags = json_decode($this->vars->flags);
252        if (!$this->vars->mbox || empty($flags)) {
253            return false;
254        }
255
256        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
257
258        if (!$GLOBALS['injector']->getInstance('IMP_Message')->flagAllInMailbox($flags, array($mbox), $this->vars->add)) {
259            return false;
260        }
261
262        $this->_base->queue->poll($mbox);
263
264        return true;
265    }
266
267    /**
268     * AJAX action: List mailboxes.
269     *
270     * Variables used:
271     *   - all: (integer) 1 to show all mailboxes.
272     *   - base: (string) The base mailbox.
273     *   - expall: (boolean) 1 to expand all (requires 'all').
274     *   - initial: (string) 1 to indicate the initial request for mailbox
275     *              list.
276     *   - mboxes: (string) The list of mailboxes to process (JSON encoded
277     *             array; mailboxes are base64url encoded).
278     *   - reload: (integer) 1 to force reload of mailboxes.
279     *   - unsub: (integer) 1 to show unsubscribed mailboxes.
280     *
281     * @return boolean  True.
282     */
283    public function listMailboxes()
284    {
285        global $injector, $prefs, $session;
286
287        $ftree = $injector->getInstance('IMP_Ftree');
288        $iterator = new AppendIterator();
289
290        /* This might be a long running operation. */
291        if ($this->vars->initial) {
292            $session->close();
293            $ftree->eltdiff->clear();
294
295            /* @todo: Correctly handle unsubscribed mailboxes in ftree. */
296            if ($ftree->unsubscribed_loaded && !$this->vars->reload) {
297                $ftree->init();
298            }
299        }
300
301        if ($this->vars->reload) {
302            $ftree->init();
303        }
304
305        $filter = new IMP_Ftree_IteratorFilter($ftree);
306        if ($this->vars->unsub) {
307            $ftree->loadUnsubscribed();
308            $filter->remove($filter::UNSUB);
309        }
310
311        if (isset($this->vars->base)) {
312            $this->_base->queue->setMailboxOpt('base', $this->vars->base);
313        }
314
315        if ($this->vars->all) {
316            $this->_base->queue->setMailboxOpt('all', 1);
317            $iterator->append($filter);
318            if ($this->vars->expall) {
319                $this->vars->action = 'expand';
320                $this->_base->callAction('toggleMailboxes');
321            }
322        } elseif ($this->vars->initial || $this->vars->reload) {
323            $special = new ArrayIterator();
324            $special->append($ftree['INBOX']);
325
326            /* Add special mailboxes explicitly to the initial folder list,
327             * since they are ALWAYS displayed, may appear outside of the
328             * folder slice requested, and need to be sorted logically. */
329            $s_elts = array();
330            foreach (IMP_Mailbox::getSpecialMailboxesSort() as $val) {
331                if (isset($ftree[$val])) {
332                    $special->append($val);
333                    $s_elts[] = $ftree[$val];
334                }
335            }
336            $iterator->append($special);
337
338            /* Go through and find any parent elements that contain only
339             * special mailbox children - this need to be suppressed in
340             * display. */
341            $filter2 = clone $filter;
342            $filter2->add(array($filter2::CONTAINERS, $filter2::SPECIALMBOXES));
343            $no_children = array();
344
345            foreach (array_unique($s_elts) as $val) {
346                while (($val = $val->parent) && !$val->base_elt) {
347                    $filter2->iterator = new IMP_Ftree_Iterator($val);
348                    foreach ($filter2 as $val) {
349                        /* If we found at least one viewable mailbox, this
350                         * element needs its children to be displayed. */
351                        break 2;
352                    }
353                    $no_children[] = strval($val);
354                }
355            }
356
357            if (!empty($no_children)) {
358                $this->_base->queue->ftreeCallback = function($id, $ob) use ($no_children) {
359                    if (in_array($id, $no_children)) {
360                        unset($ob->ch);
361                    }
362                };
363            }
364
365            /* Add regular mailboxes. */
366            $no_mbox = false;
367
368            switch ($prefs->getValue('nav_expanded')) {
369            case IMP_Ftree_Prefs_Expanded::NO:
370                $filter->add($filter::CHILDREN);
371                break;
372
373            case IMP_Ftree_Prefs_Expanded::YES:
374                $this->_base->queue->setMailboxOpt('expand', 1);
375                $no_mbox = true;
376                break;
377
378            case IMP_Ftree_Prefs_Expanded::LAST:
379                $filter->add($filter::EXPANDED);
380                $this->_base->queue->setMailboxOpt('expand', 1);
381                break;
382            }
383
384            $filter->mboxes = array('INBOX');
385            $iterator->append($filter);
386
387            if (!$no_mbox) {
388                $mboxes = IMP_Mailbox::formFrom(json_decode($this->vars->mboxes));
389                foreach ($mboxes as $val) {
390                    if (!$val->inbox) {
391                        $ancestors = new IMP_Ftree_IteratorFilter(
392                            new IMP_Ftree_Iterator_Ancestors($val->tree_elt)
393                        );
394                        if ($this->vars->unsub) {
395                            $ancestors->remove($ancestors::UNSUB);
396                        }
397                        $iterator->append($ancestors);
398                    }
399                }
400            }
401        } else {
402            $filter->add($filter::EXPANDED);
403            $this->_base->queue->setMailboxOpt('expand', 1);
404
405            foreach (array_filter(IMP_Mailbox::formFrom(json_decode($this->vars->mboxes))) as $val) {
406                $filter->iterator = new IMP_Ftree_Iterator($val->tree_elt);
407                $iterator->append($filter);
408                $ftree->expand($val);
409            }
410        }
411
412        array_map(
413            array($ftree->eltdiff, 'add'),
414            array_unique(iterator_to_array($iterator, false))
415        );
416
417        if ($this->vars->initial) {
418            $session->start();
419
420            /* We need at least 1 changed mailbox. If not, something went
421             * wrong and we should reinitialize the folder list. */
422            if (!$ftree->eltdiff->changed_elts) {
423                $this->vars->reload = true;
424                $this->listMailboxes();
425                $this->vars->reload = false;
426            }
427        }
428
429        return true;
430    }
431
432    /**
433     * AJAX action: Initialize dynamic view.
434     *
435     * @see IMP_Ajax_Application_Handler_Common#viewPort()
436     * @see listMailboxes()
437     *
438     * @return boolean  True.
439     */
440    public function dynamicInit()
441    {
442        $this->_base->callAction('viewPort');
443
444        $this->vars->initial = 1;
445        $this->vars->mboxes = json_encode(array($this->vars->mailbox));
446        $this->listMailboxes();
447
448        $this->_base->queue->flagConfig(Horde_Registry::VIEW_DYNAMIC);
449
450        return true;
451    }
452
453    /**
454     * AJAX action: Modify list of polled mailboxes.
455     *
456     * Variables used:
457     *   - add: (integer) 1 to add to the poll list, 0 to remove.
458     *   - mbox: (string) The full mailbox name to modify (base64url encoded).
459     *
460     * @return mixed  False on failure, or an object with the following
461     *                entries:
462     *   - add: (integer) 1 if added to the poll list, 0 if removed.
463     *   - mbox: (string) The full mailbox name modified.
464     */
465    public function modifyPoll()
466    {
467        global $injector;
468
469        if (!$this->vars->mbox) {
470            return false;
471        }
472
473        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
474
475        $result = new stdClass;
476        $result->add = intval($this->vars->add);
477        $result->mbox = $this->vars->mbox;
478
479        if ($this->vars->add) {
480            $injector->getInstance('IMP_Ftree')->poll->addPollList($mbox);
481            $this->_base->queue->poll($mbox);
482            $GLOBALS['notification']->push(sprintf(_("\"%s\" mailbox now polled for new mail."), $mbox->display), 'horde.success');
483        } else {
484            $injector->getInstance('IMP_Ftree')->poll->removePollList($mbox);
485            $GLOBALS['notification']->push(sprintf(_("\"%s\" mailbox no longer polled for new mail."), $mbox->display), 'horde.success');
486        }
487
488        return $result;
489    }
490
491    /**
492     * AJAX action: [un]Subscribe to a mailbox.
493     *
494     * Variables used:
495     *   - mbox: (string) The full mailbox name to [un]subscribe to (base64url
496     *           encoded).
497     *   - sub: (integer) 1 to subscribe, empty to unsubscribe.
498     *   - subfolders: (boolean) [Un]subscribe to all subfolders?
499     *
500     * @return boolean  True on success, false on failure.
501     */
502    public function subscribe()
503    {
504        return IMP_Mailbox::formFrom($this->vars->mbox)->subscribe($this->vars->sub, array(
505            'subfolders' => !empty($this->vars->subfolders)
506        ));
507    }
508
509    /**
510     * AJAX action: Import a mailbox.
511     *
512     * Variables used:
513     *   - import_mbox: (string) The mailbox to import into (base64url
514     *                  encoded).
515     *
516     * @return object  Returns response object to display JSON HTML-encoded:
517     *   - action: (string) The action name (importMailbox).
518     *   - mbox: (string) The mailbox the messages were imported to (base64url
519     *           encoded).
520     */
521    public function importMailbox()
522    {
523        global $injector, $notification;
524
525        $mbox = IMP_Mailbox::formFrom($this->vars->import_mbox);
526
527        try {
528            $notification->push($injector->getInstance('IMP_Mbox_Import')->import($mbox, 'import_file'), 'horde.success');
529            $this->_base->queue->poll($mbox);
530        } catch (Horde_Exception $e) {
531            $notification->push($e);
532        }
533
534        $result = new stdClass;
535        $result->action = 'importMailbox';
536        $result->mbox = $this->vars->import_mbox;
537
538        return new Horde_Core_Ajax_Response_HordeCore_JsonHtml($result);
539    }
540
541    /**
542     * AJAX action: Flag messages.
543     *
544     * See the list of variables needed for IMP_Ajax_Application#changed() and
545     * IMP_Ajax_Application#checkUidvalidity(). Mailbox/indices form
546     * parameters needed. Additional variables used:
547     *   - add: (integer) Set the flag?
548     *   - flags: (string) The flags to set (JSON serialized array).
549     *
550     * @return boolean  True on success, false on failure.
551     */
552    public function flagMessages()
553    {
554        global $injector;
555
556        if (!$this->vars->flags || !count($this->_base->indices)) {
557            return false;
558        }
559
560        $change = $this->_base->changed(true);
561
562        if (is_null($change)) {
563            return false;
564        }
565
566        $flags = json_decode($this->vars->flags);
567
568        /* Check for non-system flags. If we find any, and the server supports
569         * CONDSTORE, we should make sure that these flags are only updated if
570         * nobody else has altered the flags. */
571        $system_flags = array(
572            Horde_Imap_Client::FLAG_ANSWERED,
573            Horde_Imap_Client::FLAG_DELETED,
574            Horde_Imap_Client::FLAG_DRAFT,
575            Horde_Imap_Client::FLAG_FLAGGED,
576            Horde_Imap_Client::FLAG_RECENT,
577            Horde_Imap_Client::FLAG_SEEN
578        );
579
580        $unchangedsince = null;
581        if (!$this->_base->indices->mailbox->search &&
582            $this->vars->viewport->cacheid &&
583            array_diff($flags, $system_flags)) {
584            $imp_imap = $this->_base->indices->mailbox->imp_imap;
585            $parsed = $imp_imap->parseCacheId($this->vars->viewport->cacheid);
586
587            try {
588                $unchangedsince[strval($this->_base->indices->mailbox)] = $imp_imap->sync($this->_base->indices->mailbox, $parsed['token'], array(
589                    'criteria' => Horde_Imap_Client::SYNC_UIDVALIDITY
590                ))->highestmodseq;
591            } catch (Horde_Imap_Client_Exception_Sync $e) {}
592        }
593
594        $res = $injector->getInstance('IMP_Message')->flag(array(
595            ($this->vars->add ? 'add' : 'remove') => $flags
596        ), $this->_base->indices, array(
597            'unchangedsince' => $unchangedsince
598        ));
599
600        if (!$res) {
601            $this->_base->checkUidvalidity();
602            return false;
603        }
604
605        if (in_array(Horde_Imap_Client::FLAG_SEEN, $flags)) {
606            $this->_base->queue->poll(array_keys($this->_base->indices->indices()));
607        }
608
609        $this->_base->addTask('viewport', $change ? $this->_base->viewPortData(true) : new IMP_Ajax_Application_Viewport($this->_base->indices->mailbox));
610
611        return true;
612    }
613
614    /**
615     * AJAX action: Add contact.
616     *
617     * Variables used:
618     *   - addr: (string) [JSON array] Address list.
619     *
620     * @return boolean  True on success, false on failure.
621     */
622    public function addContact()
623    {
624        global $injector, $notification;
625
626        $addr_ob = $injector->getInstance('IMP_Dynamic_AddressList')->parseAddressList($this->vars->addr);
627
628        // TODO: Currently supports only a single, non-group contact.
629        $ob = $addr_ob[0];
630        if (!$ob) {
631            return false;
632        } elseif ($ob instanceof Horde_Mail_Rfc822_Group) {
633            $notification->push(_("Adding group lists not currently supported."), 'horde.warning');
634            return false;
635        }
636
637        try {
638            $injector->getInstance('IMP_Contacts')->addAddress($ob->bare_address, $ob->personal);
639            $notification->push(sprintf(_("%s was successfully added to your address book."), $ob->label), 'horde.success');
640            return true;
641        } catch (Horde_Exception $e) {
642            $notification->push($e);
643            return false;
644        }
645    }
646
647    /**
648     * AJAX action: Blacklist/whitelist addresses from messages.
649     *
650     * See the list of variables needed for IMP_Ajax_Application#changed(),
651     * IMP_Ajax_Application#deleteMsgs(), and
652     * IMP_Ajax_Application#checkUidvalidity(). Mailbox/indices form
653     * parameters needed. Additional variables used:
654     *   - blacklist: (integer) 1 to blacklist, 0 to whitelist.
655     *
656     * @return boolean  True on success.
657     */
658    public function blacklist()
659    {
660        if (!count($this->_base->indices)) {
661            return false;
662        }
663
664        if ($this->vars->blacklist) {
665            $change = $this->_base->changed(false);
666            if (!is_null($change)) {
667                try {
668                    if ($GLOBALS['injector']->getInstance('IMP_Filter')->blacklistMessage($this->_base->indices, false)) {
669                        $this->_base->deleteMsgs($this->_base->indices, $change);
670                        return true;
671                    }
672                } catch (Horde_Exception $e) {
673                    $this->_base->checkUidvalidity();
674                }
675            }
676        } else {
677            try {
678                $GLOBALS['injector']->getInstance('IMP_Filter')->whitelistMessage($this->_base->indices, false);
679                return true;
680            } catch (Horde_Exception $e) {
681                $this->_base->checkUidvalidity();
682            }
683        }
684
685        return false;
686    }
687
688    /**
689     * AJAX action: Return the MIME tree representation of the message.
690     *
691     * See the list of variables needed for IMP_Ajax_Application#changed() and
692     * IMP_Ajax_Application#checkUidvalidity(). Mailbox/indices form
693     * parameters needed. Additional variables used:
694     *   - preview: (integer) If set, return preview data. Otherwise, return
695     *              full data.
696     *
697     * @return mixed  On error will return null.
698     *                Otherwise an object with the following entries:
699     *   - tree: (string) The MIME tree representation of the message.
700     *           If viewing preview, on error this object will contain error
701     *           and errortype properties.
702     */
703    public function messageMimeTree()
704    {
705        $result = new stdClass;
706
707        try {
708            $imp_contents = $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create($this->_base->indices);
709            $result->tree = $imp_contents->getTree()->getTree(true);
710        } catch (IMP_Exception $e) {
711            if (!$this->vars->preview) {
712                throw $e;
713            }
714
715            $result->preview->error = $e->getMessage();
716            $result->preview->errortype = 'horde.error';
717            $result->preview->buid = $this->vars->buid;
718            $result->preview->view = $this->vars->view;
719        }
720
721        return $result;
722    }
723
724    /**
725     * AJAX action: Return a list of address objects used to build an address
726     * header for a message.
727     *
728     * See the list of variables needed for IMP_Ajax_Application#changed() and
729     * IMP_Ajax_Application#checkUidvalidity(). Mailbox/indices form
730     * parameters needed. Additional variables used:
731     *   - header: (integer) If set, return preview data. Otherwise, return
732     *              full data.
733     *
734     * @return object  An object with the following entries:
735     *   - hdr_data: (object) Contains header names as keys and lists of
736     *               address objects as values.
737     * @throws IMP_Exception
738     */
739    public function addressHeader()
740    {
741        $show_msg = new IMP_Ajax_Application_ShowMessage($this->_base->indices);
742
743        $hdr = $this->vars->header;
744
745        $result = new stdClass;
746        $result->hdr_data->$hdr = (object)$show_msg->getAddressHeader($this->vars->header, null);
747
748        return $result;
749    }
750
751    /**
752     * AJAX action: Delete an attachment from compose data.
753     *
754     * Variables used:
755     *   - atc_indices: (string) [JSON array] Attachment IDs to delete.
756     *   - imp_compose: (string) The IMP_Compose cache identifier.
757     *   - quiet: (boolean) If true, don't output notifications.
758     *
759     * @return array  The list of attchment IDs that were deleted.
760     */
761    public function deleteAttach()
762    {
763        global $injector, $notification;
764
765        $result = array();
766
767        if (isset($this->vars->atc_indices)) {
768            $imp_compose = $injector->getInstance('IMP_Factory_Compose')->create($this->vars->imp_compose);
769            foreach (json_decode($this->vars->atc_indices) as $val) {
770                if (isset($imp_compose[$val])) {
771                    if (empty($this->vars->quiet)) {
772                        $notification->push(sprintf(_("Deleted attachment \"%s\"."), Horde_Mime::decode($imp_compose[$val]->getPart()->getName(true))), 'horde.success');
773                    }
774                    unset($imp_compose[$val]);
775                    $result[] = $val;
776                    $this->_base->queue->compose($imp_compose);
777                }
778            }
779        }
780
781        if (empty($result) && empty($this->vars->quiet)) {
782            $notification->push(_("At least one attachment could not be deleted."), 'horde.error');
783        }
784
785        return $result;
786    }
787
788    /**
789     * AJAX action: Purge deleted messages.
790     *
791     * See the list of variables needed for IMP_Ajax_Application#changed() and
792     * IMP_Ajax_Application#deleteMsgs().
793     *
794     * @return boolean  True on success.
795     */
796    public function purgeDeleted()
797    {
798        global $injector;
799
800        $change = $this->_base->changed(true);
801        if (is_null($change)) {
802            return false;
803        }
804
805        if (!$change) {
806            $change = ($this->_base->indices->mailbox->getSort()->sortby == Horde_Imap_Client::SORT_THREAD);
807        }
808
809        $expunged = $injector->getInstance('IMP_Message')->expungeMailbox(array(strval($this->_base->indices->mailbox) => 1), array('list' => true));
810
811        if (!($expunge_count = count($expunged))) {
812            return false;
813        }
814
815        $GLOBALS['notification']->push(sprintf(ngettext("%d message was purged from \"%s\".", "%d messages were purged from \"%s\".", $expunge_count), $expunge_count, $this->_base->indices->mailbox->display), 'horde.success');
816
817        $indices = new IMP_Indices_Mailbox();
818        $indices->buids = $this->_base->indices->mailbox->toBuids($expunged);
819        $indices->mailbox = $this->_base->indices->mailbox;
820        $indices->indices = $expunged;
821
822        $this->_base->deleteMsgs($indices, $change, true);
823        $this->_base->queue->poll($this->_base->indices->mailbox);
824
825        return true;
826    }
827
828    /**
829     * AJAX action: Send a Message Disposition Notification (MDN).
830     *
831     * Mailbox/indices form parameters needed.
832     *
833     * @return mixed  False on failure, or an object with these properties:
834     *   - buid: (integer) BUID of message.
835     *   - mbox: (string) Mailbox of message (base64url encoded). Only
836     *           returned if view is a search mailbox.
837     *   - uid: (string) Mailbox of message (base64url encoded). Only
838     *          returned if view is a search mailbox.
839     *   - view: (string) The view ID.
840     */
841    public function sendMDN()
842    {
843        global $injector, $notification;
844
845        if (count($this->_base->indices) != 1) {
846            return false;
847        }
848
849        try {
850            $contents = $injector->getInstance('IMP_Factory_Contents')->create($this->_base->indices);
851        } catch (IMP_Imap_Exception $e) {
852            $e->notify(_("The Message Disposition Notification was not sent. This is what the server said") . ': ' . $e->getMessage());
853            return false;
854        }
855
856        list($mbox, $uid) = $this->_base->indices->getSingle();
857        try {
858            $injector->getInstance('IMP_Message_Ui')->MDNCheck(
859                new IMP_Indices($mbox, $uid),
860                $contents->getHeaderAndMarkAsSeen(),
861                true
862            );
863        } catch (Horde_Exception $e) {
864            $notification->push(_("The Message Disposition Notification was not sent. This is what the server said") . ': ' . $e->getMessage(), 'horde.warning');
865            return false;
866        }
867
868        $notification->push(_("The Message Disposition Notification was sent successfully."), 'horde.success');
869
870        list($view, $buid) = $this->_base->indices->buids->getSingle();
871        list($mbox, $uid) = $this->_base->indices->getSingle();
872
873        $result = new stdClass;
874        $result->buid = $buid;
875        $result->view = $view->form_to;
876
877        if ($view != $mbox) {
878            $result->mbox = $mbox->form_to;
879            $result->uid = $uid;
880        }
881
882        return $result;
883    }
884
885    /**
886     * AJAX action: strip attachment.
887     *
888     * See the list of variables needed for IMP_Ajax_Application#changed() and
889     * IMP_Ajax_Application#checkUidvalidity(). Mailbox/indices form
890     * parameters needed.
891     *
892     * @return mixed  False on failure, or an object with these properties:
893     *   - newbuid: (integer) BUID of new message.
894     *   - newmbox: (string) Mailbox of new message (base64url encoded).
895     */
896    public function stripAttachment()
897    {
898        global $injector, $notification;
899
900        if (count($this->_base->indices) != 1) {
901            return false;
902        }
903
904        $change = $this->_base->changed(true);
905        if (is_null($change)) {
906            return false;
907        }
908
909        try {
910            $this->_base->indices = new IMP_Indices_Mailbox(
911                $this->_base->indices->mailbox,
912                $injector->getInstance('IMP_Message')->stripPart($this->_base->indices, $this->vars->id)
913            );
914        } catch (IMP_Exception $e) {
915            $notification->push($e);
916            return false;
917        }
918
919        $notification->push(_("Attachment successfully stripped."), 'horde.success');
920
921        $result = new stdClass;
922        list($result->newmbox, $result->newbuid) = $this->_base->indices->getSingle();
923        $result->newmbox = $result->newmbox->form_to;
924
925        $this->_base->queue->message($this->_base->indices, true);
926        $this->_base->addTask('viewport', $this->_base->viewPortData(true));
927
928        return $result;
929    }
930
931    /**
932     * AJAX action: Convert HTML to text (compose data).
933     *
934     * Variables used:
935     *   - data: (string) [JSON array] List of data to convert. Keys are UIDs
936     *           used to idetify the return values. Values are arrays with
937     *           these keys:
938     *     - changed: (integer) Has the text changed from the original?
939     *     - text: (string) The text to convert.
940     *   - imp_compose: (string) The IMP_Compose cache identifier.
941     *
942     * @return object  An object with the following entries:
943     *   - text: (array) Array with keys as UIDs and values as the converted
944     *           text string.
945     */
946    public function html2Text()
947    {
948        return $this->_convertText('text');
949    }
950
951    /**
952     * AJAX action: Convert text to HTML (compose data).
953     *
954     * Variables used:
955     *   - data: (string) [JSON array] List of data to convert. Keys are UIDs
956     *           used to idetify the return values. Values are arrays with
957     *           these keys:
958     *     - changed: (integer) Has the text changed from the original?
959     *     - text: (string) The text to convert.
960     *   - imp_compose: (string) The IMP_Compose cache identifier.
961     *
962     * @return object  An object with the following entries:
963     *   - text: (array) Array with keys as UIDs and values as the converted
964     *           text string.
965     */
966    public function text2Html()
967    {
968        return $this->_convertText('html');
969    }
970
971    /**
972     * Helper for html2Text() and text2Html().
973     *
974     * @internal
975     */
976    protected function _convertText($mode)
977    {
978        global $injector;
979
980        $compose = null;
981
982        $result = new stdClass;
983        $result->text = array();
984
985        foreach (json_decode($this->vars->data, true) as $key => $val) {
986            $tmp = null;
987
988            if (empty($val['changed'])) {
989                if (!$compose) {
990                    $compose = $this->_base->initCompose();
991                }
992
993                switch ($compose->compose->replyType()) {
994                case IMP_Compose::FORWARD_BODY:
995                case IMP_Compose::FORWARD_BOTH:
996                    $data = $compose->compose->forwardMessageText($compose->contents, array(
997                        'format' => $mode
998                    ));
999                    $tmp = $data['body'];
1000                    break;
1001
1002                case IMP_Compose::REPLY_ALL:
1003                case IMP_Compose::REPLY_LIST:
1004                case IMP_Compose::REPLY_SENDER:
1005                    $data = $compose->compose->replyMessageText($compose->contents, array(
1006                        'format' => $mode
1007                    ));
1008                    $tmp = $data['body'];
1009                    break;
1010                }
1011            }
1012
1013            $result->text[$key] = is_null($tmp)
1014                ? $injector->getInstance('IMP_Compose_Ui')->convertComposeText($val['text'], $mode)
1015                : $tmp;
1016        }
1017
1018        return $result;
1019    }
1020
1021    /**
1022     * AJAX action: Add an attachment to a compose message (from the ckeditor
1023     * plugin).
1024     *
1025     * Variables used:
1026     *   - CKEditorFuncNum: (integer) CKEditor function identifier to call
1027     *                      when returning URL data
1028     *   - composeCache: (string) The IMP_Compose cache identifier.
1029     *
1030     * @return Horde_Core_Ajax_Response_Raw  text/html return containing
1031     *                                       javascript code to update the
1032     *                                       URL parameter in CKEditor.
1033     */
1034    public function addAttachmentCkeditor()
1035    {
1036        global $injector;
1037
1038        $data = $url = null;
1039
1040        if (isset($this->vars->composeCache)) {
1041            $imp_compose = $injector->getInstance('IMP_Factory_Compose')->create($this->vars->composeCache);
1042
1043            if ($imp_compose->canUploadAttachment()) {
1044                try {
1045                    $atc_ob = $imp_compose->addAttachmentFromUpload('upload');
1046                    if ($atc_ob[0] instanceof IMP_Compose_Exception) {
1047                        throw $atc_ob[0];
1048                    }
1049
1050                    $atc_ob[0]->related = true;
1051
1052                    $data = array(
1053                        IMP_Compose::RELATED_ATTR => 'src;' . $atc_ob[0]->id
1054                    );
1055                    $url = strval($atc_ob[0]->viewUrl());
1056                } catch (IMP_Compose_Exception $e) {
1057                    $data = $e->getMessage();
1058                }
1059            } else {
1060                $data = _("Uploading attachments has been disabled on this server.");
1061            }
1062        } else {
1063            $data = _("Your attachment was not uploaded. Most likely, the file exceeded the maximum size allowed by the server configuration.");
1064        }
1065
1066        return new Horde_Core_Ajax_Response_Raw(
1067            '<html>' .
1068                Horde::wrapInlineScript(array(
1069                    'window.parent.CKEDITOR.tools.callFunction(' . $this->vars->CKEditorFuncNum . ',' . json_encode($url) . ',' . json_encode($data) . ')'
1070                )) .
1071            '</html>',
1072            'text/html'
1073        );
1074    }
1075
1076    /**
1077     * AJAX action: Is the given mailbox fixed? Called dynamically to delay
1078     * retrieval of ACLs of all visible mailboxes at initialization.
1079     *
1080     * Variables used:
1081     *   - mbox: (integer) The mailbox name.
1082     *
1083     * @return object  An object with the following entires:
1084     *   - fixed: (boolean) True if the mailbox is fixed.
1085     */
1086    public function isFixedMbox()
1087    {
1088        $result = new stdClass;
1089        $result->fixed = !(IMP_Mailbox::formFrom($this->vars->mbox)->access_deletembox);
1090        return $result;
1091
1092    }
1093
1094    /**
1095     * AJAX action: Create an IMAP flag.
1096     *
1097     * Variables used:
1098     *   - flagcolor: (string) Background color for flag label.
1099     *   - flagname: (string) Flag name.
1100     *
1101     * @return object  An object with the following properties:
1102     * <pre>
1103     *   - success: (boolean) True if successful.
1104     * </pre>
1105     */
1106    public function createFlag()
1107    {
1108        global $injector, $notification;
1109
1110        $ret = new stdClass;
1111        $ret->success = true;
1112
1113        $imp_flags = $injector->getInstance('IMP_Flags');
1114
1115        try {
1116            $imapflag = $imp_flags->addFlag($this->vars->flagname);
1117        } catch (IMP_Exception $e) {
1118            $notification->push($e, 'horde.error');
1119            $ret->success = false;
1120            return $ret;
1121        }
1122
1123        if (!empty($this->vars->flagcolor)) {
1124            $imp_flags->updateFlag($this->vars->flagname, 'bgcolor', $this->vars->flagcolor);
1125        }
1126
1127        $this->vars->add = true;
1128        $this->vars->flags = json_encode(array($imapflag));
1129        $this->flagMessages();
1130
1131        $this->_base->queue->flagConfig(Horde_Registry::VIEW_DYNAMIC);
1132
1133        $name = 'imp:viewport';
1134        if ($this->_base->tasks->$name) {
1135            $this->_base->tasks->$name->addFlagMetadata();
1136        }
1137
1138        return $ret;
1139    }
1140
1141    /**
1142     * AJAX action: Generate the sent-mail select list.
1143     *
1144     * Variables used: NONE
1145     *
1146     * @return object  An object with the following properties:
1147     * <pre>
1148     *   - flist: (array) TODO
1149     * </pre>
1150     */
1151    public function sentMailList()
1152    {
1153        global $injector;
1154
1155        /* Check to make sure the sent-mail mailboxes are created; they need
1156         * to exist to show up in drop-down list. */
1157        $identity = $injector->getInstance('IMP_Identity');
1158        foreach (array_keys($identity->getAll('id')) as $ident) {
1159            $mbox = $identity->getValue(IMP_Mailbox::MBOX_SENT, $ident);
1160            if ($mbox instanceof IMP_Mailbox) {
1161                $mbox->create();
1162            }
1163        }
1164
1165        $flist = array();
1166        $iterator = new IMP_Ftree_IteratorFilter($injector->getInstance('IMP_Ftree'));
1167        $iterator->add($iterator::NONIMAP);
1168
1169        foreach ($iterator as $val) {
1170            $mbox_ob = $val->mbox_ob;
1171            $tmp = array(
1172                'f' => $mbox_ob->display,
1173                'l' => Horde_String::abbreviate(str_repeat(' ', 2 * $val->level) . $mbox_ob->display, 30),
1174                'v' => $val->container ? '' : $mbox_ob->form_to
1175            );
1176            if ($tmp['f'] == $tmp['v']) {
1177                unset($tmp['f']);
1178            }
1179            $flist[] = $tmp;
1180        }
1181
1182        $ret = new stdClass;
1183        $ret->flist = $flist;
1184
1185        return $ret;
1186    }
1187
1188    /**
1189     * AJAX action: Redirect to the filter edit page and pre-populate with
1190     * an e-mail address.
1191     *
1192     * Requires EITHER 'addr' -or- mailbox/indices from form params.
1193     *
1194     * Variables used:
1195     * <pre>
1196     *   - addr: (string) The e-mail address to use.
1197     * </pre>
1198     *
1199     * @return Horde_Core_Ajax_Response_HordeCore_Reload  Object with URL to
1200     *                                                    redirect to.
1201     */
1202    public function newFilter()
1203    {
1204        global $injector, $notification, $registry;
1205
1206        if (isset($this->vars->addr)) {
1207            $addr_ob = $injector->getInstance('IMP_Dynamic_AddressList')->parseAddressList($this->vars->addr);
1208        } else {
1209            $query = new Horde_Imap_Client_Fetch_Query();
1210            $query->envelope();
1211
1212            $imp_imap = $this->_base->indices->mailbox->imp_imap;
1213            list($mbox, $uid) = $this->_base->indices->getSingle();
1214            $ret = $imp_imap->fetch($mbox, $query, array(
1215                'ids' => $imp_imap->getIdsOb($uid)
1216            ));
1217
1218            $addr_ob = $ret[$uid]->getEnvelope()->from;
1219        }
1220
1221        // TODO: Currently supports only a single, non-group contact.
1222        $ob = $addr_ob[0];
1223        if (!$ob) {
1224            return false;
1225        } elseif ($ob instanceof Horde_Mail_Rfc822_Group) {
1226            $notification->push(_("Editing group lists not currently supported."), 'horde.warning');
1227            return false;
1228        }
1229
1230        try {
1231            return new Horde_Core_Ajax_Response_HordeCore_Reload(
1232                $registry->link('mail/newEmailFilter', array(
1233                    'email' => $ob->bare_address
1234                ))
1235            );
1236        } catch (Horde_Exception $e) {
1237            return false;
1238        }
1239    }
1240
1241    /**
1242     * AJAX action: Return the contacts images for a given e-mail address.
1243     *
1244     * Variables used:
1245     * <pre>
1246     *   - addr: (string) The e-mail address.
1247     * </pre>
1248     *
1249     * @return object  An object with the following properties:
1250     * <pre>
1251     *   - avatar: (string) The URL of the avatar image.
1252     *   - flag: (string) The URL of the sender's country flag image.
1253     *   - flagname: (string) The name of the country of the sender.
1254     * </pre>
1255     */
1256    public function getContactsImage()
1257    {
1258        $contacts_img = new IMP_Contacts_Image($this->vars->addr);
1259        $out = new stdClass;
1260
1261        try {
1262            $res = $contacts_img->getImage($contacts_img::AVATAR);
1263            $out->avatar = strval($res['url']);
1264        } catch (IMP_Exception $e) {}
1265
1266        try {
1267            $res = $contacts_img->getImage($contacts_img::FLAG);
1268            $out->flag = strval($res['url']);
1269            $out->flagname = $res['desc'];
1270        } catch (IMP_Exception $e) {}
1271
1272        return $out;
1273    }
1274
1275    /**
1276     * AJAX action: Determine the size of a mailbox.
1277     *
1278     * Variables used:
1279     *   - mbox: (string) The name of the mailbox to check (base64url
1280     *           encoded).
1281     *
1282     * @return object  An object with the following properties:
1283     * <pre>
1284     *   - size: (string) Formatted size string.
1285     * </pre>
1286     */
1287    public function mailboxSize()
1288    {
1289        $mbox = IMP_Mailbox::formFrom($this->vars->mbox);
1290
1291        $ret = new stdClass;
1292        $ret->size = $mbox->size;
1293
1294        return $ret;
1295    }
1296
1297}
1298