Overview

Packages

  • IMP

Classes

  • IMP
  • IMP_Ajax_Addresses
  • IMP_Ajax_Application
  • IMP_Ajax_Application_Compose
  • IMP_Ajax_Application_Handler_Common
  • IMP_Ajax_Application_Handler_ComposeAttach
  • IMP_Ajax_Application_Handler_Draft
  • IMP_Ajax_Application_Handler_Dynamic
  • IMP_Ajax_Application_Handler_ImageUnblock
  • IMP_Ajax_Application_Handler_Mboxtoggle
  • IMP_Ajax_Application_Handler_Passphrase
  • IMP_Ajax_Application_Handler_Remote
  • IMP_Ajax_Application_Handler_RemotePrefs
  • IMP_Ajax_Application_Handler_Search
  • IMP_Ajax_Application_Handler_Smartmobile
  • IMP_Ajax_Application_ListMessages
  • IMP_Ajax_Application_ShowMessage
  • IMP_Ajax_Application_Viewport
  • IMP_Ajax_Application_Viewport_Error
  • IMP_Ajax_Imple_ImportEncryptKey
  • IMP_Ajax_Imple_ItipRequest
  • IMP_Ajax_Imple_PassphraseDialog
  • IMP_Ajax_Imple_VcardImport
  • IMP_Ajax_Queue
  • IMP_Api
  • IMP_Application
  • IMP_Auth
  • IMP_Basic_Base
  • IMP_Basic_Compose
  • IMP_Basic_Contacts
  • IMP_Basic_Error
  • IMP_Basic_Folders
  • IMP_Basic_Listinfo
  • IMP_Basic_Mailbox
  • IMP_Basic_Message
  • IMP_Basic_Pgp
  • IMP_Basic_Saveimage
  • IMP_Basic_Search
  • IMP_Basic_Searchbasic
  • IMP_Basic_Smime
  • IMP_Basic_Thread
  • IMP_Block_Newmail
  • IMP_Block_Summary
  • IMP_Compose
  • IMP_Compose_Attachment
  • IMP_Compose_Attachment_Linked_Metadata
  • IMP_Compose_Attachment_Metadata
  • IMP_Compose_Attachment_Storage
  • IMP_Compose_Attachment_Storage_AutoDetermine
  • IMP_Compose_Attachment_Storage_Temp
  • IMP_Compose_Attachment_Storage_VfsLinked
  • IMP_Compose_Exception
  • IMP_Compose_Exception_Address
  • IMP_Compose_HtmlSignature
  • IMP_Compose_Link
  • IMP_Compose_LinkedAttachment
  • IMP_Compose_Ui
  • IMP_Compose_View
  • IMP_Contacts
  • IMP_Contacts_Avatar_Addressbook
  • IMP_Contacts_Avatar_Gravatar
  • IMP_Contacts_Avatar_Unknown
  • IMP_Contacts_Flag_Host
  • IMP_Contacts_Image
  • IMP_Contents
  • IMP_Contents_InlineOutput
  • IMP_Contents_View
  • IMP_Crypt_Pgp
  • IMP_Crypt_Smime
  • IMP_Dynamic_AddressList
  • IMP_Dynamic_Base
  • IMP_Dynamic_Compose
  • IMP_Dynamic_Compose_Common
  • IMP_Dynamic_Helper_Base
  • IMP_Dynamic_Mailbox
  • IMP_Dynamic_Message
  • IMP_Exception
  • IMP_Factory_AuthImap
  • IMP_Factory_Compose
  • IMP_Factory_ComposeAtc
  • IMP_Factory_Contacts
  • IMP_Factory_Contents
  • IMP_Factory_Flags
  • IMP_Factory_Ftree
  • IMP_Factory_Identity
  • IMP_Factory_Imap
  • IMP_Factory_Mail
  • IMP_Factory_MailAutoconfig
  • IMP_Factory_Mailbox
  • IMP_Factory_MailboxCache
  • IMP_Factory_MailboxList
  • IMP_Factory_Maillog
  • IMP_Factory_MimeViewer
  • IMP_Factory_Pgp
  • IMP_Factory_PrefsSort
  • IMP_Factory_Quota
  • IMP_Factory_Search
  • IMP_Factory_Sentmail
  • IMP_Factory_Smime
  • IMP_Factory_Spam
  • IMP_Filter
  • IMP_Flag_Base
  • IMP_Flag_Imap
  • IMP_Flag_Imap_Answered
  • IMP_Flag_Imap_Deleted
  • IMP_Flag_Imap_Draft
  • IMP_Flag_Imap_Flagged
  • IMP_Flag_Imap_Forwarded
  • IMP_Flag_Imap_Junk
  • IMP_Flag_Imap_NotJunk
  • IMP_Flag_Imap_Seen
  • IMP_Flag_System_Attachment
  • IMP_Flag_System_Encrypted
  • IMP_Flag_System_HighPriority
  • IMP_Flag_System_List
  • IMP_Flag_System_LowPriority
  • IMP_Flag_System_Match_Address
  • IMP_Flag_System_Match_Flag
  • IMP_Flag_System_Match_Header
  • IMP_Flag_System_Personal
  • IMP_Flag_System_Signed
  • IMP_Flag_System_Unseen
  • IMP_Flag_User
  • IMP_Flags
  • IMP_Ftree
  • IMP_Ftree_Account
  • IMP_Ftree_Account_Imap
  • IMP_Ftree_Account_Inboxonly
  • IMP_Ftree_Account_Remote
  • IMP_Ftree_Account_Vfolder
  • IMP_Ftree_Element
  • IMP_Ftree_Eltdiff
  • IMP_Ftree_Iterator
  • IMP_Ftree_Iterator_Ancestors
  • IMP_Ftree_IteratorFilter
  • IMP_Ftree_IteratorFilter_Children
  • IMP_Ftree_IteratorFilter_Containers
  • IMP_Ftree_IteratorFilter_Expanded
  • IMP_Ftree_IteratorFilter_Invisible
  • IMP_Ftree_IteratorFilter_Mailboxes
  • IMP_Ftree_IteratorFilter_Nonimap
  • IMP_Ftree_IteratorFilter_Polled
  • IMP_Ftree_IteratorFilter_Remote
  • IMP_Ftree_IteratorFilter_Special
  • IMP_Ftree_IteratorFilter_Subscribed
  • IMP_Ftree_IteratorFilter_Vfolder
  • IMP_Ftree_Prefs
  • IMP_Ftree_Prefs_Expanded
  • IMP_Ftree_Prefs_Poll
  • IMP_Ftree_Select
  • IMP_Images
  • IMP_Imap
  • IMP_Imap_Acl
  • IMP_Imap_Cache_Wrapper
  • IMP_Imap_Config
  • IMP_Imap_Exception
  • IMP_Imap_Password
  • IMP_Imap_PermanentFlags
  • IMP_Imap_Remote
  • IMP_Indices
  • IMP_Indices_Mailbox
  • IMP_LoginTasks_SystemTask_GarbageCollection
  • IMP_LoginTasks_SystemTask_Upgrade
  • IMP_LoginTasks_SystemTask_UpgradeAuth
  • IMP_LoginTasks_Task_Autocreate
  • IMP_LoginTasks_Task_DeleteAttachmentsMonthly
  • IMP_LoginTasks_Task_DeleteSentmailMonthly
  • IMP_LoginTasks_Task_FilterOnLogin
  • IMP_LoginTasks_Task_PurgeSentmail
  • IMP_LoginTasks_Task_PurgeSpam
  • IMP_LoginTasks_Task_PurgeTrash
  • IMP_LoginTasks_Task_RecoverDraft
  • IMP_LoginTasks_Task_RenameSentmailMonthly
  • IMP_Mailbox
  • IMP_Mailbox_List
  • IMP_Mailbox_List_Pop3
  • IMP_Mailbox_List_Thread
  • IMP_Mailbox_List_Virtual
  • IMP_Mailbox_SessionCache
  • IMP_Mailbox_Ui
  • IMP_Maillog
  • IMP_Maillog_Log_Base
  • IMP_Maillog_Log_Forward
  • IMP_Maillog_Log_Mdn
  • IMP_Maillog_Log_Redirect
  • IMP_Maillog_Log_Reply
  • IMP_Maillog_Log_Replyall
  • IMP_Maillog_Log_Replylist
  • IMP_Maillog_Message
  • IMP_Maillog_Storage_Base
  • IMP_Maillog_Storage_Composite
  • IMP_Maillog_Storage_History
  • IMP_Maillog_Storage_Mdnsent
  • IMP_Maillog_Storage_Null
  • IMP_Mbox_Generate
  • IMP_Mbox_Import
  • IMP_Mbox_Size
  • IMP_Message
  • IMP_Message_Date
  • IMP_Message_Ui
  • IMP_Mime_Headers
  • IMP_Mime_Status
  • IMP_Mime_Status_RenderIssue
  • IMP_Mime_Status_RenderIssue_Display
  • IMP_Mime_Viewer_Alternative
  • IMP_Mime_Viewer_Appledouble
  • IMP_Mime_Viewer_Audio
  • IMP_Mime_Viewer_Enriched
  • IMP_Mime_Viewer_Externalbody
  • IMP_Mime_Viewer_Html
  • IMP_Mime_Viewer_Images
  • IMP_Mime_Viewer_Itip
  • IMP_Mime_Viewer_Mdn
  • IMP_Mime_Viewer_Partial
  • IMP_Mime_Viewer_Pdf
  • IMP_Mime_Viewer_Pgp
  • IMP_Mime_Viewer_Plain
  • IMP_Mime_Viewer_Related
  • IMP_Mime_Viewer_Rfc822
  • IMP_Mime_Viewer_Smil
  • IMP_Mime_Viewer_Smime
  • IMP_Mime_Viewer_Status
  • IMP_Mime_Viewer_Vcard
  • IMP_Mime_Viewer_Video
  • IMP_Mime_Viewer_Zip
  • IMP_Minimal_Base
  • IMP_Minimal_Compose
  • IMP_Minimal_Error
  • IMP_Minimal_Folders
  • IMP_Minimal_Mailbox
  • IMP_Minimal_Message
  • IMP_Minimal_Messagepart
  • IMP_Minimal_Search
  • IMP_Notification_Event_Status
  • IMP_Notification_Handler_Decorator_ImapAlerts
  • IMP_Notification_Handler_Decorator_NewmailNotify
  • IMP_Perms
  • IMP_Prefs_AttribText
  • IMP_Prefs_Identity
  • IMP_Prefs_Sort
  • IMP_Prefs_Sort_FixedDate
  • IMP_Prefs_Sort_None
  • IMP_Prefs_Sort_Sortpref
  • IMP_Prefs_Sort_Sortpref_Locked
  • IMP_Prefs_Special_Acl
  • IMP_Prefs_Special_ComposeTemplates
  • IMP_Prefs_Special_Drafts
  • IMP_Prefs_Special_Encrypt
  • IMP_Prefs_Special_Flag
  • IMP_Prefs_Special_HtmlSignature
  • IMP_Prefs_Special_ImageReplacement
  • IMP_Prefs_Special_InitialPage
  • IMP_Prefs_Special_Mailto
  • IMP_Prefs_Special_NewmailSound
  • IMP_Prefs_Special_PgpPrivateKey
  • IMP_Prefs_Special_PgpPublicKey
  • IMP_Prefs_Special_Remote
  • IMP_Prefs_Special_Searches
  • IMP_Prefs_Special_Sentmail
  • IMP_Prefs_Special_SmimePrivateKey
  • IMP_Prefs_Special_SmimePublicKey
  • IMP_Prefs_Special_Sourceselect
  • IMP_Prefs_Special_Spam
  • IMP_Prefs_Special_SpecialMboxes
  • IMP_Prefs_Special_Trash
  • IMP_Quota
  • IMP_Quota_Hook
  • IMP_Quota_Imap
  • IMP_Quota_Null
  • IMP_Quota_Ui
  • IMP_Remote
  • IMP_Remote_Account
  • IMP_Script_Package_Autocomplete
  • IMP_Script_Package_ComposeBase
  • IMP_Script_Package_DynamicBase
  • IMP_Script_Package_Editor
  • IMP_Script_Package_Imp
  • IMP_Search
  • IMP_Search_Element
  • IMP_Search_Element_Attachment
  • IMP_Search_Element_Autogenerated
  • IMP_Search_Element_Bulk
  • IMP_Search_Element_Contacts
  • IMP_Search_Element_Daterange
  • IMP_Search_Element_Flag
  • IMP_Search_Element_Header
  • IMP_Search_Element_Mailinglist
  • IMP_Search_Element_Or
  • IMP_Search_Element_Personal
  • IMP_Search_Element_Recipient
  • IMP_Search_Element_Size
  • IMP_Search_Element_Text
  • IMP_Search_Element_Within
  • IMP_Search_Filter
  • IMP_Search_Filter_Attachment
  • IMP_Search_Filter_Autogenerated
  • IMP_Search_Filter_Builtin
  • IMP_Search_Filter_Bulk
  • IMP_Search_Filter_Contacts
  • IMP_Search_Filter_Mailinglist
  • IMP_Search_Filter_Personal
  • IMP_Search_IteratorFilter
  • IMP_Search_Query
  • IMP_Search_Ui
  • IMP_Search_Vfolder
  • IMP_Search_Vfolder_Builtin
  • IMP_Search_Vfolder_Vinbox
  • IMP_Search_Vfolder_Vtrash
  • IMP_Sentmail
  • IMP_Sentmail_Mongo
  • IMP_Sentmail_Null
  • IMP_Sentmail_Sql
  • IMP_Smartmobile
  • IMP_Spam
  • IMP_Spam_Email
  • IMP_Spam_Null
  • IMP_Spam_Program
  • IMP_Test
  • IMP_Tree_Flist
  • IMP_Tree_Jquerymobile
  • IMP_Tree_Simplehtml
  • IMP_View_Subinfo

Interfaces

  • IMP_Compose_Attachment_Linked
  • IMP_Contacts_Avatar_Backend
  • IMP_Contacts_Flag_Backend
  • IMP_Spam_Base
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Copyright 2002-2014 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 2002-2014 Horde LLC
  10:  * @license   http://www.horde.org/licenses/gpl GPL
  11:  * @package   IMP
  12:  */
  13: 
  14: /**
  15:  * The IMP_Compose:: class represents an outgoing mail message.
  16:  *
  17:  * @author    Michael Slusarz <slusarz@horde.org>
  18:  * @category  Horde
  19:  * @copyright 2002-2014 Horde LLC
  20:  * @license   http://www.horde.org/licenses/gpl GPL
  21:  * @package   IMP
  22:  */
  23: class IMP_Compose implements ArrayAccess, Countable, IteratorAggregate
  24: {
  25:     /* The virtual path to save drafts. */
  26:     const VFS_DRAFTS_PATH = '.horde/imp/drafts';
  27: 
  28:     /* Compose types. */
  29:     const COMPOSE = 0;
  30:     const REPLY = 1;
  31:     const REPLY_ALL = 2;
  32:     const REPLY_AUTO = 3;
  33:     const REPLY_LIST = 4;
  34:     const REPLY_SENDER = 5;
  35:     const FORWARD = 6;
  36:     const FORWARD_ATTACH = 7;
  37:     const FORWARD_AUTO = 8;
  38:     const FORWARD_BODY = 9;
  39:     const FORWARD_BOTH = 10;
  40:     const REDIRECT = 11;
  41:     const EDITASNEW = 12;
  42:     const TEMPLATE = 13;
  43: 
  44:     /* Related part attribute name. */
  45:     const RELATED_ATTR = 'imp_related_attr';
  46: 
  47:     /* The blockquote tag to use to indicate quoted text in HTML data. */
  48:     const HTML_BLOCKQUOTE = '<blockquote type="cite" style="border-left:2px solid blue;margin-left:2px;padding-left:12px;">';
  49: 
  50:     /**
  51:      * Attachment ID counter.
  52:      *
  53:      * @var integer
  54:      */
  55:     public $atcId = 0;
  56: 
  57:     /**
  58:      * Mark as changed for purposes of storing in the session.
  59:      * Either empty, 'changed', or 'deleted'.
  60:      *
  61:      * @var string
  62:      */
  63:     public $changed = '';
  64: 
  65:     /**
  66:      * The charset to use for sending.
  67:      *
  68:      * @var string
  69:      */
  70:     public $charset;
  71: 
  72:     /**
  73:      * Attachment data.
  74:      *
  75:      * @var array
  76:      */
  77:     protected $_atc = array();
  78: 
  79:     /**
  80:      * The cache ID used to store object in session.
  81:      *
  82:      * @var string
  83:      */
  84:     protected $_cacheid;
  85: 
  86:     /**
  87:      * Various metadata for this message.
  88:      *
  89:      * @var array
  90:      */
  91:     protected $_metadata = array();
  92: 
  93:     /**
  94:      * The reply type.
  95:      *
  96:      * @var integer
  97:      */
  98:     protected $_replytype = self::COMPOSE;
  99: 
 100:     /**
 101:      * Constructor.
 102:      *
 103:      * @param string $cacheid  The cache ID string.
 104:      */
 105:     public function __construct($cacheid)
 106:     {
 107:         $this->_cacheid = $cacheid;
 108:         $this->charset = $GLOBALS['registry']->getEmailCharset();
 109:     }
 110: 
 111:     /**
 112:      * Tasks to do upon unserialize().
 113:      */
 114:     public function __wakeup()
 115:     {
 116:         $this->changed = '';
 117:     }
 118: 
 119:     /**
 120:      * Destroys an IMP_Compose instance.
 121:      *
 122:      * @param string $action  The action performed to cause the end of this
 123:      *                        instance.  Either 'cancel', 'discard',
 124:      *                        'save_draft', or 'send'.
 125:      */
 126:     public function destroy($action)
 127:     {
 128:         switch ($action) {
 129:         case 'discard':
 130:         case 'send':
 131:             /* Delete the draft. */
 132:             $GLOBALS['injector']->getInstance('IMP_Message')->delete(
 133:                 new IMP_Indices($this->getMetadata('draft_uid')),
 134:                 array('nuke' => true)
 135:             );
 136:             break;
 137: 
 138:         case 'save_draft':
 139:             /* Don't delete any drafts. */
 140:             $this->changed = 'deleted';
 141:             return;
 142: 
 143:         case 'cancel':
 144:             if ($this->getMetadata('draft_auto')) {
 145:                 $this->destroy('discard');
 146:                 return;
 147:             }
 148:             // Fall-through
 149: 
 150:         default:
 151:             // No-op
 152:             break;
 153:         }
 154: 
 155:         $this->deleteAllAttachments();
 156: 
 157:         $this->changed = 'deleted';
 158:     }
 159: 
 160:     /**
 161:      * Gets metadata about the current object.
 162:      *
 163:      * @param string $name  The metadata name.
 164:      *
 165:      * @return mixed  The metadata value or null if it doesn't exist.
 166:      */
 167:     public function getMetadata($name)
 168:     {
 169:         return isset($this->_metadata[$name])
 170:             ? $this->_metadata[$name]
 171:             : null;
 172:     }
 173: 
 174:     /**
 175:      * Sets metadata for the current object.
 176:      *
 177:      * @param string $name  The metadata name.
 178:      * @param mixed $value  The metadata value.
 179:      */
 180:     protected function _setMetadata($name, $value)
 181:     {
 182:         if (is_null($value)) {
 183:             unset($this->_metadata[$name]);
 184:         } else {
 185:             $this->_metadata[$name] = $value;
 186:         }
 187:         $this->changed = 'changed';
 188:     }
 189: 
 190:     /**
 191:      * Saves a draft message.
 192:      *
 193:      * @param array $headers  List of message headers (UTF-8).
 194:      * @param mixed $message  Either the message text (string) or a
 195:      *                        Horde_Mime_Part object that contains the text
 196:      *                        to send.
 197:      * @param array $opts     An array of options w/the following keys:
 198:      *   - autosave: (boolean) Is this an auto-saved draft?
 199:      *   - html: (boolean) Is this an HTML message?
 200:      *   - priority: (string) The message priority ('high', 'normal', 'low').
 201:      *   - readreceipt: (boolean) Add return receipt headers?
 202:      *
 203:      * @return string  Notification text on success (not HTML encoded).
 204:      *
 205:      * @throws IMP_Compose_Exception
 206:      */
 207:     public function saveDraft($headers, $message, array $opts = array())
 208:     {
 209:         $body = $this->_saveDraftMsg($headers, $message, $opts);
 210:         $ret = $this->_saveDraftServer($body);
 211:         $this->_setMetadata('draft_auto', !empty($opts['autosave']));
 212:         return $ret;
 213:     }
 214: 
 215:     /**
 216:      * Prepare the draft message.
 217:      *
 218:      * @param array $headers  List of message headers.
 219:      * @param mixed $message  Either the message text (string) or a
 220:      *                        Horde_Mime_Part object that contains the text
 221:      *                        to send.
 222:      * @param array $opts     An array of options w/the following keys:
 223:      *   - html: (boolean) Is this an HTML message?
 224:      *   - priority: (string) The message priority ('high', 'normal', 'low').
 225:      *   - readreceipt: (boolean) Add return receipt headers?
 226:      *   - verify_email: (boolean) Verify e-mail messages? Default: no.
 227:      *
 228:      * @return string  The body text.
 229:      *
 230:      * @throws IMP_Compose_Exception
 231:      */
 232:     protected function _saveDraftMsg($headers, $message, $opts)
 233:     {
 234:         $has_session = (bool)$GLOBALS['registry']->getAuth();
 235: 
 236:         /* Set up the base message now. */
 237:         $base = $this->_createMimeMessage(new Horde_Mail_Rfc822_List(), $message, array(
 238:             'html' => !empty($opts['html']),
 239:             'noattach' => !$has_session,
 240:             'nofinal' => true
 241:         ));
 242:         $base->isBasePart(true);
 243: 
 244:         $recip_list = $this->recipientList($headers);
 245:         if (!empty($opts['verify_email'])) {
 246:             foreach ($recip_list['list'] as $val) {
 247:                 try {
 248:                     IMP::parseAddressList($val->writeAddress(true), array(
 249:                         'validate' => true
 250:                     ));
 251:                 } catch (Horde_Mail_Exception $e) {
 252:                     throw new IMP_Compose_Exception(sprintf(
 253:                         _("Saving the message failed because it contains an invalid e-mail address: %s."),
 254:                         strval($val),
 255:                         $e->getMessage()
 256:                     ), $e->getCode());
 257:                 }
 258:             }
 259:         }
 260:         $headers = array_merge($headers, $recip_list['header']);
 261: 
 262:         /* Initalize a header object for the draft. */
 263:         $draft_headers = $this->_prepareHeaders($headers, array_merge($opts, array('bcc' => true)));
 264: 
 265:         /* Add information necessary to log replies/forwards when finally
 266:          * sent. */
 267:         $imp_imap = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create();
 268:         if ($this->_replytype) {
 269:             try {
 270:                 $indices = $this->getMetadata('indices');
 271: 
 272:                 $imap_url = new Horde_Imap_Client_Url();
 273:                 $imap_url->hostspec = $imp_imap->getParam('hostspec');
 274:                 $imap_url->protocol = $imp_imap->isImap() ? 'imap' : 'pop';
 275:                 $imap_url->username = $imp_imap->getParam('username');
 276: 
 277:                 $urls = array();
 278:                 foreach ($indices as $val) {
 279:                     $imap_url->mailbox = $val->mbox;
 280:                     $imap_url->uidvalidity = $val->mbox->uidvalid;
 281:                     foreach ($val->uids as $val2) {
 282:                         $imap_url->uid = $val2;
 283:                         $urls[] = '<' . strval($imap_url) . '>';
 284:                     }
 285:                 }
 286: 
 287:                 switch ($this->replyType(true)) {
 288:                 case self::FORWARD:
 289:                     $draft_headers->addHeader('X-IMP-Draft-Forward', implode(', ', $urls));
 290:                     break;
 291: 
 292:                 case self::REPLY:
 293:                     $draft_headers->addHeader('X-IMP-Draft-Reply', implode(', ', $urls));
 294:                     $draft_headers->addHeader('X-IMP-Draft-Reply-Type', $this->_replytype);
 295:                     break;
 296:                 }
 297:             } catch (Horde_Exception $e) {}
 298:         } else {
 299:             $draft_headers->addHeader('X-IMP-Draft', 'Yes');
 300:         }
 301: 
 302:         return $base->toString(array(
 303:             'defserver' => $has_session ? $imp_imap->config->maildomain : null,
 304:             'headers' => $draft_headers
 305:         ));
 306:     }
 307: 
 308:     /**
 309:      * Save a draft message on the IMAP server.
 310:      *
 311:      * @param string $data  The text of the draft message.
 312:      *
 313:      * @return string  Status string (not HTML escaped).
 314:      *
 315:      * @throws IMP_Compose_Exception
 316:      */
 317:     protected function _saveDraftServer($data)
 318:     {
 319:         if (!$drafts_mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_DRAFTS)) {
 320:             throw new IMP_Compose_Exception(_("Saving the draft failed. No drafts mailbox specified."));
 321:         }
 322: 
 323:         /* Check for access to drafts mailbox. */
 324:         if (!$drafts_mbox->create()) {
 325:             throw new IMP_Compose_Exception(_("Saving the draft failed. Could not create a drafts mailbox."));
 326:         }
 327: 
 328:         $append_flags = array(
 329:             Horde_Imap_Client::FLAG_DRAFT,
 330:             /* RFC 3503 [3.4] - MUST set MDNSent flag on draft message. */
 331:             Horde_Imap_Client::FLAG_MDNSENT
 332:         );
 333:         if (!$GLOBALS['prefs']->getValue('unseen_drafts')) {
 334:             $append_flags[] = Horde_Imap_Client::FLAG_SEEN;
 335:         }
 336: 
 337:         $old_uid = $this->getMetadata('draft_uid');
 338: 
 339:         /* Add the message to the mailbox. */
 340:         try {
 341:             $ids = $drafts_mbox->imp_imap->append($drafts_mbox, array(array('data' => $data, 'flags' => $append_flags)));
 342: 
 343:             if ($old_uid) {
 344:                 $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true));
 345:             }
 346: 
 347:             $this->_setMetadata('draft_uid', $drafts_mbox->getIndicesOb($ids));
 348:             return sprintf(_("The draft has been saved to the \"%s\" mailbox."), $drafts_mbox->display);
 349:         } catch (IMP_Imap_Exception $e) {
 350:             return _("The draft was not successfully saved.");
 351:         }
 352:     }
 353: 
 354:     /**
 355:      * Edits a message as new.
 356:      *
 357:      * @see resumeDraft().
 358:      *
 359:      * @param IMP_Indices $indices  An indices object.
 360:      * @param array $opts           Additional options:
 361:      *   - format: (string) Force to this format.
 362:      *             DEFAULT: Auto-determine.
 363:      *
 364:      * @return mixed  See resumeDraft().
 365:      *
 366:      * @throws IMP_Compose_Exception
 367:      */
 368:     public function editAsNew($indices, array $opts = array())
 369:     {
 370:         $ret = $this->_resumeDraft($indices, self::EDITASNEW, $opts);
 371:         $ret['type'] = self::EDITASNEW;
 372:         return $ret;
 373:     }
 374: 
 375:     /**
 376:      * Edit an existing template message. Saving this template later
 377:      * (using saveTemplate()) will cause the original message to be deleted.
 378:      *
 379:      * @param IMP_Indices $indices  An indices object.
 380:      *
 381:      * @return mixed  See resumeDraft().
 382:      *
 383:      * @throws IMP_Compose_Exception
 384:      */
 385:     public function editTemplate($indices)
 386:     {
 387:         $res = $this->useTemplate($indices);
 388:         $this->_setMetadata('template_uid_edit', $indices);
 389:         return $res;
 390:     }
 391: 
 392:     /**
 393:      * Resumes a previously saved draft message.
 394:      *
 395:      * @param IMP_Indices $indices  An indices object.
 396:      * @param array $opts           Additional options:
 397:      *   - format: (string) Force to this format.
 398:      *             DEFAULT: Auto-determine.
 399:      *
 400:      * @return mixed  An array with the following keys:
 401:      *   - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List
 402:      *           objects).
 403:      *   - body: (string) The text of the body part.
 404:      *   - format: (string) The format of the body message ('html', 'text').
 405:      *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
 406:      *   - priority: (string) The message priority.
 407:      *   - readreceipt: (boolean) Add return receipt headers?
 408:      *   - subject: (string) Formatted subject.
 409:      *   - type: (integer) - The compose type.
 410:      *
 411:      * @throws IMP_Compose_Exception
 412:      */
 413:     public function resumeDraft($indices, array $opts = array())
 414:     {
 415:         $res = $this->_resumeDraft($indices, null, $opts);
 416:         $this->_setMetadata('draft_uid', $indices);
 417:         return $res;
 418:     }
 419: 
 420:     /**
 421:      * Uses a template to create a message.
 422:      *
 423:      * @see resumeDraft().
 424:      *
 425:      * @param IMP_Indices $indices  An indices object.
 426:      * @param array $opts           Additional options:
 427:      *   - format: (string) Force to this format.
 428:      *             DEFAULT: Auto-determine.
 429:      *
 430:      * @return mixed  See resumeDraft().
 431:      *
 432:      * @throws IMP_Compose_Exception
 433:      */
 434:     public function useTemplate($indices, array $opts = array())
 435:     {
 436:         $ret = $this->_resumeDraft($indices, self::TEMPLATE, $opts);
 437:         $ret['type'] = self::TEMPLATE;
 438:         return $ret;
 439:     }
 440: 
 441:     /**
 442:      * Resumes a previously saved draft message.
 443:      *
 444:      * @param IMP_Indices $indices  See resumeDraft().
 445:      * @param integer $type         Compose type.
 446:      * @param array $opts           Additional options:
 447:      *   - format: (string) Force to this format.
 448:      *             DEFAULT: Auto-determine.
 449:      *
 450:      * @return mixed  See resumeDraft().
 451:      *
 452:      * @throws IMP_Compose_Exception
 453:      */
 454:     protected function _resumeDraft($indices, $type, $opts)
 455:     {
 456:         global $injector, $prefs;
 457: 
 458:         $contents_factory = $injector->getInstance('IMP_Factory_Contents');
 459: 
 460:         try {
 461:             $contents = $contents_factory->create($indices);
 462:         } catch (IMP_Exception $e) {
 463:             throw new IMP_Compose_Exception($e);
 464:         }
 465: 
 466:         $headers = $contents->getHeader();
 467:         $imp_draft = false;
 468: 
 469:         if ($draft_url = $headers->getValue('x-imp-draft-reply')) {
 470:             if (is_null($type) &&
 471:                 !($type = $headers->getValue('x-imp-draft-reply-type'))) {
 472:                 $type = self::REPLY;
 473:             }
 474:             $imp_draft = self::REPLY;
 475:         } elseif ($draft_url = $headers->getValue('x-imp-draft-forward')) {
 476:             $imp_draft = self::FORWARD;
 477:             if (is_null($type)) {
 478:                 $type = self::FORWARD;
 479:             }
 480:         } elseif ($headers->getValue('x-imp-draft')) {
 481:             $imp_draft = self::COMPOSE;
 482:         }
 483: 
 484:         if (!empty($opts['format'])) {
 485:             $compose_html = ($opts['format'] == 'html');
 486:         } elseif ($prefs->getValue('compose_html')) {
 487:             $compose_html = true;
 488:         } else {
 489:             switch ($type) {
 490:             case self::EDITASNEW:
 491:             case self::FORWARD:
 492:             case self::FORWARD_BODY:
 493:             case self::FORWARD_BOTH:
 494:                 $compose_html = $prefs->getValue('forward_format');
 495:                 break;
 496: 
 497:             case self::REPLY:
 498:             case self::REPLY_ALL:
 499:             case self::REPLY_LIST:
 500:             case self::REPLY_SENDER:
 501:                 $compose_html = $prefs->getValue('reply_format');
 502:                 break;
 503: 
 504:             case self::TEMPLATE:
 505:                 $compose_html = true;
 506:                 break;
 507: 
 508:             default:
 509:                 /* If this is an draft saved by IMP, we know 100% for sure
 510:                  * that if an HTML part exists, the user was composing in
 511:                  * HTML. */
 512:                 $compose_html = ($imp_draft !== false);
 513:                 break;
 514:             }
 515:         }
 516: 
 517:         $msg_text = $this->_getMessageText($contents, array(
 518:             'html' => $compose_html,
 519:             'imp_msg' => $imp_draft,
 520:             'toflowed' => false
 521:         ));
 522: 
 523:         if (empty($msg_text)) {
 524:             $body = '';
 525:             $format = 'text';
 526:             $text_id = 0;
 527:         } else {
 528:             /* Use charset at time of initial composition if this is an IMP
 529:              * draft. */
 530:             if ($imp_draft !== false) {
 531:                 $this->charset = $msg_text['charset'];
 532:             }
 533:             $body = $msg_text['text'];
 534:             $format = $msg_text['mode'];
 535:             $text_id = $msg_text['id'];
 536:         }
 537: 
 538:         $mime_message = $contents->getMIMEMessage();
 539: 
 540:         /* Add attachments. */
 541:         if (($mime_message->getPrimaryType() == 'multipart') &&
 542:             ($mime_message->getType() != 'multipart/alternative')) {
 543:             for ($i = 1; ; ++$i) {
 544:                 if (intval($text_id) == $i) {
 545:                     continue;
 546:                 }
 547: 
 548:                 if (!($part = $contents->getMIMEPart($i))) {
 549:                     break;
 550:                 }
 551: 
 552:                 try {
 553:                     $this->addAttachmentFromPart($part);
 554:                 } catch (IMP_Compose_Exception $e) {
 555:                     $GLOBALS['notification']->push($e, 'horde.warning');
 556:                 }
 557:             }
 558:         }
 559: 
 560:         $alist = new Horde_Mail_Rfc822_List();
 561:         $addr = array(
 562:             'to' => clone $alist,
 563:             'cc' => clone $alist,
 564:             'bcc' => clone $alist
 565:         );
 566: 
 567:         if ($type != self::EDITASNEW) {
 568:             foreach (array('to', 'cc', 'bcc') as $val) {
 569:                 if ($tmp = $headers->getOb($val)) {
 570:                     $addr[$val] = $tmp;
 571:                 }
 572:             }
 573: 
 574:             if ($val = $headers->getValue('references')) {
 575:                 $ref_ob = new Horde_Mail_Rfc822_Identification($val);
 576:                 $this->_setMetadata('references', $ref_ob->ids);
 577: 
 578:                 if ($val = $headers->getValue('in-reply-to')) {
 579:                     $this->_setMetadata('in_reply_to', $val);
 580:                 }
 581:             }
 582: 
 583:             if ($draft_url) {
 584:                 $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
 585:                 $indices = new IMP_Indices();
 586: 
 587:                 foreach (explode(',', $draft_url) as $val) {
 588:                     $imap_url = new Horde_Imap_Client_Url(rtrim(ltrim($val, '<'), '>'));
 589: 
 590:                     try {
 591:                         if (($imap_url->protocol == ($imp_imap->isImap() ? 'imap' : 'pop')) &&
 592:                             ($imap_url->username == $imp_imap->getParam('username')) &&
 593:                             // Ignore hostspec and port, since these can change
 594:                             // even though the server is the same. UIDVALIDITY
 595:                             // should catch any true server/backend changes.
 596:                             (IMP_Mailbox::get($imap_url->mailbox)->uidvalid == $imap_url->uidvalidity) &&
 597:                             $contents_factory->create(new IMP_Indices($imap_url->mailbox, $imap_url->uid))) {
 598:                             $indices->add($imap_url->mailbox, $imap_url->uid);
 599:                         }
 600:                     } catch (Exception $e) {}
 601:                 }
 602: 
 603:                 if (count($indices)) {
 604:                     $this->_setMetadata('indices', $indices);
 605:                     $this->_replytype = $type;
 606:                 }
 607:             }
 608:         }
 609: 
 610:         $mdn = new Horde_Mime_Mdn($headers);
 611:         $readreceipt = (bool)$mdn->getMdnReturnAddr();
 612: 
 613:         $this->changed = 'changed';
 614: 
 615:         return array(
 616:             'addr' => $addr,
 617:             'body' => $body,
 618:             'format' => $format,
 619:             'identity' => $this->_getMatchingIdentity($headers, array('from')),
 620:             'priority' => $injector->getInstance('IMP_Mime_Headers')->getPriority($headers),
 621:             'readreceipt' => $readreceipt,
 622:             'subject' => $headers->getValue('subject'),
 623:             'type' => $type
 624:         );
 625:     }
 626: 
 627:     /**
 628:      * Save a template message on the IMAP server.
 629:      *
 630:      * @param array $headers  List of message headers (UTF-8).
 631:      * @param mixed $message  Either the message text (string) or a
 632:      *                        Horde_Mime_Part object that contains the text
 633:      *                        to save.
 634:      * @param array $opts     An array of options w/the following keys:
 635:      *   - html: (boolean) Is this an HTML message?
 636:      *   - priority: (string) The message priority ('high', 'normal', 'low').
 637:      *   - readreceipt: (boolean) Add return receipt headers?
 638:      *
 639:      * @return string  Notification text on success.
 640:      *
 641:      * @throws IMP_Compose_Exception
 642:      */
 643:     public function saveTemplate($headers, $message, array $opts = array())
 644:     {
 645:         if (!$mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_TEMPLATES)) {
 646:             throw new IMP_Compose_Exception(_("Saving the template failed: no template mailbox exists."));
 647:         }
 648: 
 649:         /* Check for access to mailbox. */
 650:         if (!$mbox->create()) {
 651:             throw new IMP_Compose_Exception(_("Saving the template failed: could not create the templates mailbox."));
 652:         }
 653: 
 654:         $append_flags = array(
 655:             // Don't mark as draft, since other MUAs could potentially
 656:             // delete it.
 657:             Horde_Imap_Client::FLAG_SEEN
 658:         );
 659: 
 660:         $old_uid = $this->getMetadata('template_uid_edit');
 661: 
 662:         /* Add the message to the mailbox. */
 663:         try {
 664:             $mbox->imp_imap->append($mbox, array(array(
 665:                 'data' => $this->_saveDraftMsg($headers, $message, $opts),
 666:                 'flags' => $append_flags,
 667:                 'verify_email' => true
 668:             )));
 669: 
 670:             if ($old_uid) {
 671:                 $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true));
 672:             }
 673:         } catch (IMP_Imap_Exception $e) {
 674:             return _("The template was not successfully saved.");
 675:         }
 676: 
 677:         return _("The template has been saved.");
 678:     }
 679: 
 680:     /**
 681:      * Does this message have any drafts associated with it?
 682:      *
 683:      * @return boolean  True if draft messages exist.
 684:      */
 685:     public function hasDrafts()
 686:     {
 687:         return (bool)$this->getMetadata('draft_uid');
 688:     }
 689: 
 690:     /**
 691:      * Builds and sends a MIME message.
 692:      *
 693:      * @param string $body                  The message body.
 694:      * @param array $header                 List of message headers.
 695:      * @param IMP_Prefs_Identity $identity  The Identity object for the sender
 696:      *                                      of this message.
 697:      * @param array $opts                   An array of options w/the
 698:      *                                      following keys:
 699:      *  - encrypt: (integer) A flag whether to encrypt or sign the message.
 700:      *            One of:
 701:      *    - IMP_Crypt_Pgp::ENCRYPT</li>
 702:      *    - IMP_Crypt_Pgp::SIGNENC</li>
 703:      *    - IMP_Crypt_Smime::ENCRYPT</li>
 704:      *    - IMP_Crypt_Smime::SIGNENC</li>
 705:      *  - html: (boolean) Whether this is an HTML message.
 706:      *          DEFAULT: false
 707:      *  - pgp_attach_pubkey: (boolean) Attach the user's PGP public key to the
 708:      *                       message?
 709:      *  - priority: (string) The message priority ('high', 'normal', 'low').
 710:      *  - save_sent: (boolean) Save sent mail?
 711:      *  - sent_mail: (IMP_Mailbox) The sent-mail mailbox (UTF-8).
 712:      *  - strip_attachments: (bool) Strip attachments from the message?
 713:      *  - signature: (string) The message signature.
 714:      *  - readreceipt: (boolean) Add return receipt headers?
 715:      *  - useragent: (string) The User-Agent string to use.
 716:      *  - vcard_attach: (string) Attach the user's vCard (value is name to
 717:      *                  display as vcard filename).
 718:      *
 719:      * @throws Horde_Exception
 720:      * @throws IMP_Compose_Exception
 721:      * @throws IMP_Compose_Exception_Address
 722:      * @throws IMP_Exception
 723:      */
 724:     public function buildAndSendMessage(
 725:         $body, $header, IMP_Prefs_Identity $identity, array $opts = array()
 726:     )
 727:     {
 728:         global $conf, $injector, $notification, $prefs, $registry, $session;
 729: 
 730:         /* We need at least one recipient & RFC 2822 requires that no 8-bit
 731:          * characters can be in the address fields. */
 732:         $recip = $this->recipientList($header);
 733:         if (!count($recip['list'])) {
 734:             if ($recip['has_input']) {
 735:                 throw new IMP_Compose_Exception(_("Invalid e-mail address."));
 736:             }
 737:             throw new IMP_Compose_Exception(_("Need at least one message recipient."));
 738:         }
 739:         $header = array_merge($header, $recip['header']);
 740: 
 741:         /* Check for correct identity usage. */
 742:         if (!$this->getMetadata('identity_check') &&
 743:             (count($recip['list']) === 1)) {
 744:             $identity_search = $identity->getMatchingIdentity($recip['list'], false);
 745:             if (!is_null($identity_search) &&
 746:                 ($identity->getDefault() != $identity_search)) {
 747:                 $this->_setMetadata('identity_check', true);
 748: 
 749:                 $e = new IMP_Compose_Exception(_("Recipient address does not match the currently selected identity."));
 750:                 $e->tied_identity = $identity_search;
 751:                 throw $e;
 752:             }
 753:         }
 754: 
 755:         /* Check body size of message. */
 756:         $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
 757:         if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_BODYSIZE, strlen($body))) {
 758:             Horde::permissionDeniedError('imp', 'max_bodysize');
 759:             throw new IMP_Compose_Exception(sprintf(
 760:                 _("Your message body has exceeded the limit by body size by %d characters."),
 761:                 (strlen($body) - $imp_imap->max_compose_bodysize)
 762:             ));
 763:         }
 764: 
 765:         $from = new Horde_Mail_Rfc822_Address($header['from']);
 766:         if (is_null($from->host)) {
 767:             $from->host = $imp_imap->config->maildomain;
 768:         }
 769: 
 770:         /* Prepare the array of messages to send out.  May be more
 771:          * than one if we are encrypting for multiple recipients or
 772:          * are storing an encrypted message locally. */
 773:         $encrypt = empty($opts['encrypt']) ? 0 : $opts['encrypt'];
 774:         $send_msgs = array();
 775:         $msg_options = array(
 776:             'encrypt' => $encrypt,
 777:             'html' => !empty($opts['html']),
 778:             'identity' => $identity,
 779:             'pgp_attach_pubkey' => (!empty($opts['pgp_attach_pubkey']) && $prefs->getValue('use_pgp') && $prefs->getValue('pgp_public_key')),
 780:             'signature' => is_null($opts['signature']) ? $identity : $opts['signature'],
 781:             'vcard_attach' => ((!empty($opts['vcard_attach']) && $registry->hasMethod('contacts/ownVCard')) ? ((strlen($opts['vcard_attach']) ? $opts['vcard_attach'] : 'vcard') . '.vcf') : null)
 782:         );
 783: 
 784:         /* Must encrypt & send the message one recipient at a time. */
 785:         if ($prefs->getValue('use_smime') &&
 786:             in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGNENC))) {
 787:             foreach ($recip['list'] as $val) {
 788:                 $list_ob = new Horde_Mail_Rfc822_List($val);
 789:                 $send_msgs[] = array(
 790:                     'base' => $this->_createMimeMessage($list_ob, $body, $msg_options),
 791:                     'recipients' => $list_ob
 792:                 );
 793:             }
 794: 
 795:             /* Must target the encryption for the sender before saving message
 796:              * in sent-mail. */
 797:             $save_msg = $this->_createMimeMessage(IMP::parseAddressList($header['from']), $body, $msg_options);
 798:         } else {
 799:             /* Can send in clear-text all at once, or PGP can encrypt
 800:              * multiple addresses in the same message. */
 801:             $msg_options['from'] = $from;
 802:             $save_msg = $this->_createMimeMessage($recip['list'], $body, $msg_options);
 803:             $send_msgs[] = array(
 804:                 'base' => $save_msg,
 805:                 'recipients' => $recip['list']
 806:             );
 807:         }
 808: 
 809:         /* Initalize a header object for the outgoing message. */
 810:         $headers = $this->_prepareHeaders($header, $opts);
 811: 
 812:         /* Add a Received header for the hop from browser to server. */
 813:         $headers->addReceivedHeader(array(
 814:             'dns' => $injector->getInstance('Net_DNS2_Resolver'),
 815:             'server' => $conf['server']['name']
 816:         ));
 817: 
 818:         /* Add Reply-To header. */
 819:         if (!empty($header['replyto']) &&
 820:             ($header['replyto'] != $from->bare_address)) {
 821:             $headers->addHeader('Reply-to', $header['replyto']);
 822:         }
 823: 
 824:         /* Add the 'User-Agent' header. */
 825:         if (empty($opts['useragent'])) {
 826:             $headers->setUserAgent('Internet Messaging Program (IMP) ' . $registry->getVersion());
 827:         } else {
 828:             $headers->setUserAgent($opts['useragent']);
 829:         }
 830:         $headers->addUserAgentHeader();
 831: 
 832:         /* Add preferred reply language(s). */
 833:         if ($lang = @unserialize($prefs->getValue('reply_lang'))) {
 834:             $headers->addHeader('Accept-Language', implode(',', $lang));
 835:         }
 836: 
 837:         /* Send the messages out now. */
 838:         $sentmail = $injector->getInstance('IMP_Sentmail');
 839: 
 840:         foreach ($send_msgs as $val) {
 841:             switch (intval($this->replyType(true))) {
 842:             case self::REPLY:
 843:                 $senttype = IMP_Sentmail::REPLY;
 844:                 break;
 845: 
 846:             case self::FORWARD:
 847:                 $senttype = IMP_Sentmail::FORWARD;
 848:                 break;
 849: 
 850:             case self::REDIRECT:
 851:                 $senttype = IMP_Sentmail::REDIRECT;
 852:                 break;
 853: 
 854:             default:
 855:                 $senttype = IMP_Sentmail::NEWMSG;
 856:                 break;
 857:             }
 858: 
 859:             try {
 860:                 $this->_prepSendMessageAssert($val['recipients'], $headers, $val['base']);
 861:                 $this->sendMessage($val['recipients'], $headers, $val['base']);
 862: 
 863:                 /* Store history information. */
 864:                 $msg_id = new Horde_Mail_Rfc822_Identification(
 865:                     $headers->getValue('message-id')
 866:                 );
 867:                 $sentmail->log(
 868:                     $senttype,
 869:                     reset($msg_id->ids),
 870:                     $val['recipients'],
 871:                     true
 872:                 );
 873:             } catch (IMP_Compose_Exception_Address $e) {
 874:                 throw $e;
 875:             } catch (IMP_Compose_Exception $e) {
 876:                 /* Unsuccessful send. */
 877:                 if ($e->log()) {
 878:                     $msg_id = new Horde_Mail_Rfc822_Identification(
 879:                         $headers->getValue('message-id')
 880:                     );
 881:                     $sentmail->log(
 882:                         $senttype,
 883:                         reset($msg_id->ids),
 884:                         $val['recipients'],
 885:                         false
 886:                     );
 887:                 }
 888:                 throw new IMP_Compose_Exception(sprintf(_("There was an error sending your message: %s"), $e->getMessage()));
 889:             }
 890:         }
 891: 
 892:         $recipients = strval($recip['list']);
 893: 
 894:         if ($this->_replytype) {
 895:             /* Log the reply. */
 896:             if ($indices = $this->getMetadata('indices')) {
 897:                 switch ($this->_replytype) {
 898:                 case self::FORWARD:
 899:                 case self::FORWARD_ATTACH:
 900:                 case self::FORWARD_BODY:
 901:                 case self::FORWARD_BOTH:
 902:                     $log = new IMP_Maillog_Log_Forward($recipients);
 903:                     break;
 904: 
 905:                 case self::REPLY:
 906:                 case self::REPLY_SENDER:
 907:                     $log = new IMP_Maillog_Log_Reply();
 908:                     break;
 909: 
 910:                 case IMP_Compose::REPLY_ALL:
 911:                     $log = new IMP_Maillog_Log_Replyall();
 912:                     break;
 913: 
 914:                 case IMP_Compose::REPLY_LIST:
 915:                     $log = new IMP_Maillog_Log_Replylist();
 916:                     break;
 917:                 }
 918: 
 919:                 $log_msgs = array();
 920:                 foreach ($indices as $val) {
 921:                     foreach ($val->uids as $val2) {
 922:                         $log_msgs[] = new IMP_Maillog_Message(
 923:                             new IMP_Indices($val->mbox, $val2)
 924:                         );
 925:                     }
 926:                 }
 927: 
 928:                 $injector->getInstance('IMP_Maillog')->log($log_msgs, $log);
 929:             }
 930: 
 931:             $imp_message = $injector->getInstance('IMP_Message');
 932:             $reply_uid = new IMP_Indices($this);
 933: 
 934:             switch ($this->replyType(true)) {
 935:             case self::FORWARD:
 936:                 /* Set the Forwarded flag, if possible, in the mailbox.
 937:                  * See RFC 5550 [5.9] */
 938:                 $imp_message->flag(array(
 939:                     'add' => array(Horde_Imap_Client::FLAG_FORWARDED)
 940:                 ), $reply_uid);
 941:                 break;
 942: 
 943:             case self::REPLY:
 944:                 /* Make sure to set the IMAP reply flag and unset any
 945:                  * 'flagged' flag. */
 946:                 $imp_message->flag(array(
 947:                     'add' => array(Horde_Imap_Client::FLAG_ANSWERED),
 948:                     'remove' => array(Horde_Imap_Client::FLAG_FLAGGED)
 949:                 ), $reply_uid);
 950:                 break;
 951:             }
 952:         }
 953: 
 954:         Horde::log(
 955:             sprintf(
 956:                 "Message sent to %s from %s (%s)",
 957:                 $recipients,
 958:                 $registry->getAuth(),
 959:                 $session->get('horde', 'auth/remoteAddr')
 960:             ),
 961:             'INFO'
 962:         );
 963: 
 964:         /* Should we save this message in the sent mail mailbox? */
 965:         if (!empty($opts['sent_mail']) &&
 966:             ((!$prefs->isLocked('save_sent_mail') &&
 967:               !empty($opts['save_sent'])) ||
 968:              ($prefs->isLocked('save_sent_mail') &&
 969:               $prefs->getValue('save_sent_mail')))) {
 970:             /* Keep Bcc: headers on saved messages. */
 971:             if (count($header['bcc'])) {
 972:                 $headers->addHeader('Bcc', $header['bcc']);
 973:             }
 974: 
 975:             /* Strip attachments if requested. */
 976:             if (!empty($opts['strip_attachments'])) {
 977:                 $save_msg->buildMimeIds();
 978: 
 979:                 /* Don't strip any part if this is a text message with both
 980:                  * plaintext and HTML representation. */
 981:                 if ($save_msg->getType() != 'multipart/alternative') {
 982:                     for ($i = 2; ; ++$i) {
 983:                         if (!($oldPart = $save_msg->getPart($i))) {
 984:                             break;
 985:                         }
 986: 
 987:                         $replace_part = new Horde_Mime_Part();
 988:                         $replace_part->setType('text/plain');
 989:                         $replace_part->setCharset($this->charset);
 990:                         $replace_part->setLanguage($GLOBALS['language']);
 991:                         $replace_part->setContents('[' . _("Attachment stripped: Original attachment type") . ': "' . $oldPart->getType() . '", ' . _("name") . ': "' . $oldPart->getName(true) . '"]');
 992:                         $save_msg->alterPart($i, $replace_part);
 993:                     }
 994:                 }
 995:             }
 996: 
 997:             /* Generate the message string. */
 998:             $fcc = $save_msg->toString(array(
 999:                 'defserver' => $imp_imap->config->maildomain,
1000:                 'headers' => $headers,
1001:                 'stream' => true
1002:             ));
1003: 
1004:             /* Make sure sent mailbox is created. */
1005:             $sent_mail = IMP_Mailbox::get($opts['sent_mail']);
1006:             $sent_mail->create();
1007: 
1008:             $flags = array(
1009:                 Horde_Imap_Client::FLAG_SEEN,
1010:                 /* RFC 3503 [3.3] - MUST set MDNSent flag on sent message. */
1011:                 Horde_Imap_Client::FLAG_MDNSENT
1012:             );
1013: 
1014:             try {
1015:                 $imp_imap->append($sent_mail, array(array('data' => $fcc, 'flags' => $flags)));
1016:             } catch (IMP_Imap_Exception $e) {
1017:                 $notification->push(sprintf(_("Message sent successfully, but not saved to %s."), $sent_mail->display));
1018:             }
1019:         }
1020: 
1021:         /* Delete the attachment data. */
1022:         $this->deleteAllAttachments();
1023: 
1024:         /* Save recipients to address book? */
1025:         $this->_saveRecipients($recip['list']);
1026: 
1027:         /* Call post-sent hook. */
1028:         try {
1029:             $injector->getInstance('Horde_Core_Hooks')->callHook(
1030:                 'post_sent',
1031:                 'imp',
1032:                 array($save_msg['msg'], $headers)
1033:             );
1034:         } catch (Horde_Exception_HookNotSet $e) {}
1035:     }
1036: 
1037:     /**
1038:      * Prepare header object with basic header fields and converts headers
1039:      * to the current compose charset.
1040:      *
1041:      * @param array $headers  Array with 'from', 'to', 'cc', 'bcc', and
1042:      *                        'subject' values.
1043:      * @param array $opts     An array of options w/the following keys:
1044:      *   - bcc: (boolean) Add BCC header to output.
1045:      *   - priority: (string) The message priority ('high', 'normal', 'low').
1046:      *
1047:      * @return Horde_Mime_Headers  Headers object with the appropriate headers
1048:      *                             set.
1049:      */
1050:     protected function _prepareHeaders($headers, array $opts = array())
1051:     {
1052:         $ob = new Horde_Mime_Headers();
1053: 
1054:         $ob->addHeader('Date', date('r'));
1055:         $ob->addMessageIdHeader();
1056: 
1057:         if (isset($headers['from']) && strlen($headers['from'])) {
1058:             $ob->addHeader('From', $headers['from']);
1059:         }
1060: 
1061:         if (isset($headers['to']) &&
1062:             (is_object($headers['to']) || strlen($headers['to']))) {
1063:             $ob->addHeader('To', $headers['to']);
1064:         } elseif (!isset($headers['cc'])) {
1065:             $ob->addHeader('To', 'undisclosed-recipients:;');
1066:         }
1067: 
1068:         if (isset($headers['cc']) &&
1069:             (is_object($headers['cc']) || strlen($headers['cc']))) {
1070:             $ob->addHeader('Cc', $headers['cc']);
1071:         }
1072: 
1073:         if (!empty($opts['bcc']) &&
1074:             isset($headers['bcc']) &&
1075:             (is_object($headers['bcc']) || strlen($headers['bcc']))) {
1076:             $ob->addHeader('Bcc', $headers['bcc']);
1077:         }
1078: 
1079:         if (isset($headers['subject']) && strlen($headers['subject'])) {
1080:             $ob->addHeader('Subject', $headers['subject']);
1081:         }
1082: 
1083:         if ($this->replyType(true) == self::REPLY) {
1084:             if ($refs = $this->getMetadata('references')) {
1085:                 $ob->addHeader('References', implode(' ', $refs));
1086:             }
1087:             if ($this->getMetadata('in_reply_to')) {
1088:                 $ob->addHeader('In-Reply-To', $this->getMetadata('in_reply_to'));
1089:             }
1090:         }
1091: 
1092:         /* Add priority header, if requested. */
1093:         if (!empty($opts['priority'])) {
1094:             switch ($opts['priority']) {
1095:             case 'high':
1096:                 $ob->addHeader('Importance', 'High');
1097:                 $ob->addHeader('X-Priority', '1 (Highest)');
1098:                 break;
1099: 
1100:             case 'low':
1101:                 $ob->addHeader('Importance', 'Low');
1102:                 $ob->addHeader('X-Priority', '5 (Lowest)');
1103:                 break;
1104:             }
1105:         }
1106: 
1107:         /* Add Return Receipt Headers. */
1108:         if (!empty($opts['readreceipt'])) {
1109:             $from = $ob->getOb('from');
1110:             $from = $from[0];
1111:             if (is_null($from->host)) {
1112:                 $from->host = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create()->config->maildomain;
1113:             }
1114: 
1115:             $mdn = new Horde_Mime_Mdn($ob);
1116:             $mdn->addMdnRequestHeaders($from);
1117:         }
1118: 
1119:         return $ob;
1120:     }
1121: 
1122:     /**
1123:      * Sends a message.
1124:      *
1125:      * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1126:      * @param Horde_Mime_Headers $headers    The object holding this message's
1127:      *                                       headers.
1128:      * @param Horde_Mime_Part $message       The object that contains the text
1129:      *                                       to send.
1130:      *
1131:      * @throws IMP_Compose_Exception
1132:      */
1133:     public function sendMessage(Horde_Mail_Rfc822_List $email,
1134:                                 Horde_Mime_Headers $headers,
1135:                                 Horde_Mime_Part $message)
1136:     {
1137:         $email = $this->_prepSendMessage($email, $message);
1138: 
1139:         $opts = array();
1140:         if ($this->getMetadata('encrypt_sign')) {
1141:             /* Signing requires that the body not be altered in transport. */
1142:             $opts['encode'] = Horde_Mime_Part::ENCODE_7BIT;
1143:         }
1144: 
1145:         try {
1146:             $message->send($email, $headers, $GLOBALS['injector']->getInstance('IMP_Mail'), $opts);
1147:         } catch (Horde_Mime_Exception $e) {
1148:             throw new IMP_Compose_Exception($e);
1149:         }
1150:     }
1151: 
1152:     /**
1153:      * Sanity checking/MIME formatting before sending a message.
1154:      *
1155:      * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1156:      * @param Horde_Mime_Part $message       The object that contains the text
1157:      *                                       to send.
1158:      *
1159:      * @return string  The encoded $email list.
1160:      *
1161:      * @throws IMP_Compose_Exception
1162:      */
1163:     protected function _prepSendMessage(Horde_Mail_Rfc822_List $email,
1164:                                         $message = null)
1165:     {
1166:         /* Properly encode the addresses we're sending to. Always try
1167:          * charset of original message as we know that the user can handle
1168:          * that charset. */
1169:         try {
1170:             return $this->_prepSendMessageEncode($email, is_null($message) ? 'UTF-8' : $message->getHeaderCharset());
1171:         } catch (IMP_Compose_Exception $e) {
1172:             if (is_null($message)) {
1173:                 throw $e;
1174:             }
1175:         }
1176: 
1177:         /* Fallback to UTF-8 (if replying, original message might be in
1178:          * US-ASCII, for example, but To/Subject/Etc. may contain 8-bit
1179:          * characters. */
1180:         $message->setHeaderCharset('UTF-8');
1181:         return $this->_prepSendMessageEncode($email, 'UTF-8');
1182:     }
1183: 
1184:     /**
1185:      * Additonal checks to do if this is a user-generated compose message.
1186:      *
1187:      * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1188:      * @param Horde_Mime_Headers $headers    The object holding this message's
1189:      *                                       headers.
1190:      * @param Horde_Mime_Part $message       The object that contains the text
1191:      *                                       to send.
1192:      *
1193:      * @throws IMP_Compose_Exception
1194:      */
1195:     protected function _prepSendMessageAssert(Horde_Mail_Rfc822_List $email,
1196:                                               Horde_Mime_Headers $headers = null,
1197:                                               Horde_Mime_Part $message = null)
1198:     {
1199:         global $injector;
1200: 
1201:         $email_count = count($email);
1202:         $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create();
1203: 
1204:         if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_TIMELIMIT, $email_count)) {
1205:             Horde::permissionDeniedError('imp', 'max_timelimit');
1206:             throw new IMP_Compose_Exception(sprintf(
1207:                 ngettext(
1208:                     "You are not allowed to send messages to more than %d recipient within %d hours.",
1209:                     "You are not allowed to send messages to more than %d recipients within %d hours.",
1210:                     $imp_imap->max_compose_timelimit
1211:                 ),
1212:                 $imp_imap->max_compose_timelimit,
1213:                 $injector->getInstance('IMP_Sentmail')->limit_period
1214:             ));
1215:         }
1216: 
1217:         /* Count recipients if necessary. We need to split email groups
1218:          * because the group members count as separate recipients. */
1219:         if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_RECIPIENTS, $email_count)) {
1220:             Horde::permissionDeniedError('imp', 'max_recipients');
1221:             throw new IMP_Compose_Exception(sprintf(
1222:                 ngettext(
1223:                     "You are not allowed to send messages to more than %d recipient.",
1224:                     "You are not allowed to send messages to more than %d recipients.",
1225:                     $imp_imap->max_compose_recipients
1226:                 ),
1227:                 $imp_imap->max_compose_recipients
1228:             ));
1229:         }
1230: 
1231:         /* Pass to hook to allow alteration of message details. */
1232:         if (!is_null($message)) {
1233:             try {
1234:                 $injector->getInstance('Horde_Core_Hooks')->callHook(
1235:                     'pre_sent',
1236:                     'imp',
1237:                     array($message, $headers, $this)
1238:                 );
1239:             } catch (Horde_Exception_HookNotSet $e) {}
1240:         }
1241:     }
1242: 
1243:     /**
1244:      * Encode address and do sanity checking on encoded address.
1245:      *
1246:      * @param Horde_Mail_Rfc822_List $email  The e-mail list to send to.
1247:      * @param string $charset                The charset to encode to.
1248:      *
1249:      * @return string  The encoded $email list.
1250:      *
1251:      * @throws IMP_Compose_Exception_Address
1252:      */
1253:     protected function _prepSendMessageEncode(Horde_Mail_Rfc822_List $email,
1254:                                               $charset)
1255:     {
1256:         global $injector;
1257: 
1258:         $exception = new IMP_Compose_Exception_Address();
1259:         $hook = true;
1260:         $out = array();
1261: 
1262:         foreach ($email as $val) {
1263:             /* $email contains address objects that already have the default
1264:              * maildomain appended. Need to encode personal part and encode
1265:              * IDN domain names. */
1266:             $tmp = $val->writeAddress(array(
1267:                 'encode' => $charset,
1268:                 'idn' => true
1269:             ));
1270: 
1271:             try {
1272:                 /* We have written address, but it still may not be valid.
1273:                  * So double-check. */
1274:                 $alist = IMP::parseAddressList($tmp, array(
1275:                     'validate' => true
1276:                 ));
1277: 
1278:                 $error = null;
1279: 
1280:                 if ($hook) {
1281:                     try {
1282:                         $error = $injector->getInstance('Horde_Core_Hooks')->callHook(
1283:                             'compose_addr',
1284:                             'imp',
1285:                             array($alist[0])
1286:                         );
1287:                     } catch (Horde_Exception_HookNotSet $e) {
1288:                         $hook = false;
1289:                     }
1290:                 }
1291:             } catch (Horde_Mail_Exception $e) {
1292:                 $error = array(
1293:                     'msg' => sprintf(_("Invalid e-mail address (%s)."), $val)
1294:                 );
1295:             }
1296: 
1297:             if (is_array($error)) {
1298:                 switch (isset($error['level']) ? $error['level'] : $exception::BAD) {
1299:                 case $exception::WARN:
1300:                 case 'warn':
1301:                     if (($warn = $this->getMetadata('warn_addr')) &&
1302:                         in_array(strval($val), $warn)) {
1303:                         $out[] = $tmp;
1304:                         continue 2;
1305:                     }
1306:                     $warn[] = strval($val);
1307:                     $this->_setMetadata('warn_addr', $warn);
1308:                     $this->changed = 'changed';
1309:                     $level = $exception::WARN;
1310:                     break;
1311: 
1312:                 default:
1313:                     $level = $exception::BAD;
1314:                     break;
1315:                 }
1316: 
1317:                 $exception->addAddress($val, $error['msg'], $level);
1318:             } else {
1319:                 $out[] = $tmp;
1320:             }
1321:         }
1322: 
1323:         if (count($exception)) {
1324:             throw $exception;
1325:         }
1326: 
1327:         return implode(', ', $out);
1328:     }
1329: 
1330:     /**
1331:      * Save the recipients done in a sendMessage().
1332:      *
1333:      * @param Horde_Mail_Rfc822_List $recipients  The list of recipients.
1334:      */
1335:     public function _saveRecipients(Horde_Mail_Rfc822_List $recipients)
1336:     {
1337:         global $notification, $prefs, $registry;
1338: 
1339:         if (!$prefs->getValue('save_recipients') ||
1340:             !$registry->hasMethod('contacts/import') ||
1341:             !($abook = $prefs->getValue('add_source'))) {
1342:             return;
1343:         }
1344: 
1345:         foreach ($recipients as $recipient) {
1346:             $name = is_null($recipient->personal)
1347:                 ? $recipient->mailbox
1348:                 : $recipient->personal;
1349: 
1350:             try {
1351:                 $registry->call('contacts/import', array(array('name' => $name, 'email' => $recipient->bare_address), 'array', $abook));
1352:                 $notification->push(sprintf(_("Entry \"%s\" was successfully added to the address book"), $name), 'horde.success');
1353:             } catch (Turba_Exception_ObjectExists $e) {
1354:             } catch (Horde_Exception $e) {
1355:                 if ($e->getCode() == 'horde.error') {
1356:                     $notification->push($e, $e->getCode());
1357:                 }
1358:             }
1359:         }
1360:     }
1361: 
1362:     /**
1363:      * Cleans up and returns the recipient list. Method designed to parse
1364:      * user entered data; does not encode/validate addresses.
1365:      *
1366:      * @param array $hdr  An array of MIME headers and/or address list
1367:      *                    objects. Recipients will be extracted from the 'to',
1368:      *                    'cc', and 'bcc' entries.
1369:      *
1370:      * @return array  An array with the following entries:
1371:      *   - has_input: (boolean) True if at least one of the headers contains
1372:      *                user input.
1373:      *   - header: (array) Contains the cleaned up 'to', 'cc', and 'bcc'
1374:      *             address list (Horde_Mail_Rfc822_List objects).
1375:      *   - list: (Horde_Mail_Rfc822_List) Recipient addresses.
1376:      */
1377:     public function recipientList($hdr)
1378:     {
1379:         $addrlist = new Horde_Mail_Rfc822_List();
1380:         $has_input = false;
1381:         $header = array();
1382: 
1383:         foreach (array('to', 'cc', 'bcc') as $key) {
1384:             if (isset($hdr[$key])) {
1385:                 $ob = IMP::parseAddressList($hdr[$key]);
1386:                 if (count($ob)) {
1387:                     $addrlist->add($ob);
1388:                     $header[$key] = $ob;
1389:                     $has_input = true;
1390:                 } else {
1391:                     $header[$key] = null;
1392:                 }
1393:             }
1394:         }
1395: 
1396:         return array(
1397:             'has_input' => $has_input,
1398:             'header' => $header,
1399:             'list' => $addrlist
1400:         );
1401:     }
1402: 
1403:     /**
1404:      * Create the base Horde_Mime_Part for sending.
1405:      *
1406:      * @param Horde_Mail_Rfc822_List $to  The recipient list.
1407:      * @param string $body                Message body.
1408:      * @param array $options              Additional options:
1409:      *   - encrypt: (integer) The encryption flag.
1410:      *   - from: (Horde_Mail_Rfc822_Address) The outgoing from address (only
1411:      *           needed for multiple PGP encryption).
1412:      *   - html: (boolean) Is this a HTML message?
1413:      *   - identity: (IMP_Prefs_Identity) Identity of the sender.
1414:      *   - nofinal: (boolean) This is not a message which will be sent out.
1415:      *   - noattach: (boolean) Don't add attachment information.
1416:      *   - pgp_attach_pubkey: (boolean) Attach the user's PGP public key?
1417:      *   - signature: (IMP_Prefs_Identity|string) If set, add the signature to
1418:      *                the message.
1419:      *   - vcard_attach: (string) If set, attach user's vcard to message.
1420:      *
1421:      * @return Horde_Mime_Part  The MIME message to send.
1422:      *
1423:      * @throws Horde_Exception
1424:      * @throws IMP_Compose_Exception
1425:      */
1426:     protected function _createMimeMessage(
1427:         Horde_Mail_Rfc822_List $to, $body, array $options = array()
1428:     )
1429:     {
1430:         global $conf, $injector, $prefs, $registry;
1431: 
1432:         /* Get body text. */
1433:         if (empty($options['html'])) {
1434:             $body_html = null;
1435:         } else {
1436:             $tfilter = $injector->getInstance('Horde_Core_Factory_TextFilter');
1437: 
1438:             $body_html = $tfilter->filter(
1439:                 $body,
1440:                 'Xss',
1441:                 array(
1442:                     'return_dom' => true,
1443:                     'strip_style_attributes' => false
1444:                 )
1445:             );
1446:             $body_html_body = $body_html->getBody();
1447: 
1448:             $body = $tfilter->filter(
1449:                 $body_html->returnHtml(),
1450:                 'Html2text',
1451:                 array(
1452:                     'wrap' => false
1453:                 )
1454:             );
1455:         }
1456: 
1457:         $hooks = $injector->getInstance('Horde_Core_Hooks');
1458: 
1459:         /* We need to do the attachment check before any of the body text
1460:          * has been altered. */
1461:         if (!count($this) && !$this->getMetadata('attach_body_check')) {
1462:             $this->_setMetadata('attach_body_check', true);
1463: 
1464:             try {
1465:                 $check = $hooks->callHook(
1466:                     'attach_body_check',
1467:                     'imp',
1468:                     array($body)
1469:                 );
1470:             } catch (Horde_Exception_HookNotSet $e) {
1471:                 $check = array();
1472:             }
1473: 
1474:             if (!empty($check) &&
1475:                 preg_match('/\b(' . implode('|', array_map('preg_quote', $check)) . ')\b/i', $body, $matches)) {
1476:                 throw IMP_Compose_Exception::createAndLog('DEBUG', sprintf(_("Found the word %s in the message text although there are no files attached to the message. Did you forget to attach a file? (This check will not be performed again for this message.)"), $matches[0]));
1477:             }
1478:         }
1479: 
1480:         /* Add signature data. */
1481:         if (!empty($options['signature'])) {
1482:             if (is_string($options['signature'])) {
1483:                 if (empty($options['html'])) {
1484:                     $body .= "\n\n" . trim($options['signature']);
1485:                 } else {
1486:                     $html_sig = trim($options['signature']);
1487:                     $body .= "\n" . $tfilter->filter($html_sig, 'Html2text');
1488:                 }
1489:             } else {
1490:                 $sig = $options['signature']->getSignature('text');
1491:                 $body .= $sig;
1492: 
1493:                 if (!empty($options['html'])) {
1494:                     $html_sig = $options['signature']->getSignature('html');
1495:                     if (!strlen($html_sig) && strlen($sig)) {
1496:                         $html_sig = $this->text2html($sig);
1497:                     }
1498:                 }
1499:             }
1500: 
1501:             if (!empty($options['html'])) {
1502:                 try {
1503:                     $sig_ob = new IMP_Compose_HtmlSignature($html_sig);
1504:                 } catch (IMP_Exception $e) {
1505:                     throw new IMP_Compose_Exception($e);
1506:                 }
1507: 
1508:                 foreach ($sig_ob->dom->getBody()->childNodes as $child) {
1509:                     $body_html_body->appendChild(
1510:                         $body_html->dom->importNode($child, true)
1511:                     );
1512:                 }
1513:             }
1514:         }
1515: 
1516:         /* Add linked attachments. */
1517:         if (empty($options['nofinal'])) {
1518:             $this->_linkAttachments($body, $body_html);
1519:         }
1520: 
1521:         /* Get trailer text (if any). */
1522:         if (empty($options['nofinal'])) {
1523:             try {
1524:                 $trailer = $hooks->callHook(
1525:                     'trailer',
1526:                     'imp',
1527:                     array(false, $options['identity'], $to)
1528:                 );
1529:                 $html_trailer = $hooks->callHook(
1530:                     'trailer',
1531:                     'imp',
1532:                     array(true, $options['identity'], $to)
1533:                 );
1534:             } catch (Horde_Exception_HookNotSet $e) {
1535:                 $trailer = $html_trailer = null;
1536:             }
1537: 
1538:             $body .= strval($trailer);
1539: 
1540:             if (!empty($options['html'])) {
1541:                 if (is_null($html_trailer) && strlen($trailer)) {
1542:                     $html_trailer = $this->text2html($trailer);
1543:                 }
1544: 
1545:                 if (strlen($html_trailer)) {
1546:                     $t_dom = new Horde_Domhtml($html_trailer, 'UTF-8');
1547:                     foreach ($t_dom->getBody()->childNodes as $child) {
1548:                         $body_html_body->appendChild($body_html->dom->importNode($child, true));
1549:                     }
1550:                 }
1551:             }
1552:         }
1553: 
1554:         /* Convert text to sending charset. HTML text will be converted
1555:          * via Horde_Domhtml. */
1556:         $body = Horde_String::convertCharset($body, 'UTF-8', $this->charset);
1557: 
1558:         /* Set up the body part now. */
1559:         $textBody = new Horde_Mime_Part();
1560:         $textBody->setType('text/plain');
1561:         $textBody->setCharset($this->charset);
1562:         $textBody->setDisposition('inline');
1563: 
1564:         /* Send in flowed format. */
1565:         $flowed = new Horde_Text_Flowed($body, $this->charset);
1566:         $flowed->setDelSp(true);
1567:         $textBody->setContentTypeParameter('format', 'flowed');
1568:         $textBody->setContentTypeParameter('DelSp', 'Yes');
1569:         $text_contents = $flowed->toFlowed();
1570:         $textBody->setContents($text_contents);
1571: 
1572:         /* Determine whether or not to send a multipart/alternative
1573:          * message with an HTML part. */
1574:         if (!empty($options['html'])) {
1575:             $htmlBody = new Horde_Mime_Part();
1576:             $htmlBody->setType('text/html');
1577:             $htmlBody->setCharset($this->charset);
1578:             $htmlBody->setDisposition('inline');
1579:             $htmlBody->setDescription(Horde_String::convertCharset(_("HTML Message"), 'UTF-8', $this->charset));
1580: 
1581:             /* Add default font CSS information here. */
1582:             $styles = array();
1583:             if ($font_family = $prefs->getValue('compose_html_font_family')) {
1584:                 $styles[] = 'font-family:' . $font_family;
1585:             }
1586:             if ($font_size = intval($prefs->getValue('compose_html_font_size'))) {
1587:                 $styles[] = 'font-size:' . $font_size . 'px';
1588:             }
1589: 
1590:             if (!empty($styles)) {
1591:                 $body_html_body->setAttribute('style', implode(';', $styles));
1592:             }
1593: 
1594:             if (empty($options['nofinal'])) {
1595:                 $this->_cleanHtmlOutput($body_html);
1596:             }
1597: 
1598:             $to_add = $this->_convertToRelated($body_html, $htmlBody);
1599: 
1600:             /* Now, all parts referred to in the HTML data have been added
1601:              * to the attachment list. Convert to multipart/related if
1602:              * this is the case. Exception: if text representation is empty,
1603:              * just send HTML part. */
1604:             if (strlen(trim($text_contents))) {
1605:                 $textpart = new Horde_Mime_Part();
1606:                 $textpart->setType('multipart/alternative');
1607:                 $textpart->addPart($textBody);
1608:                 $textpart->addPart($to_add);
1609:                 $textpart->setHeaderCharset($this->charset);
1610: 
1611:                 $textBody->setDescription(Horde_String::convertCharset(_("Plaintext Message"), 'UTF-8', $this->charset));
1612:             } else {
1613:                 $textpart = $to_add;
1614:             }
1615: 
1616:             $htmlBody->setContents(
1617:                 $tfilter->filter(
1618:                     $body_html->returnHtml(array(
1619:                         'charset' => $this->charset,
1620:                         'metacharset' => true
1621:                     )),
1622:                     'Cleanhtml',
1623:                     array(
1624:                         'charset' => $this->charset
1625:                     )
1626:                 )
1627:             );
1628: 
1629:             $base = $textpart;
1630:         } else {
1631:             $base = $textpart = strlen(trim($text_contents))
1632:                 ? $textBody
1633:                 : null;
1634:         }
1635: 
1636:         /* Add attachments. */
1637:         if (empty($options['noattach'])) {
1638:             $parts = array();
1639: 
1640:             foreach ($this as $val) {
1641:                 if (!$val->related && !$val->linked) {
1642:                     $parts[] = $val->getPart(true);
1643:                 }
1644:             }
1645: 
1646:             if (!empty($options['pgp_attach_pubkey'])) {
1647:                 $parts[] = $injector->getInstance('IMP_Crypt_Pgp')->publicKeyMIMEPart();
1648:             }
1649: 
1650:             if (!empty($options['vcard_attach'])) {
1651:                 try {
1652:                     $vpart = new Horde_Mime_Part();
1653:                     $vpart->setType('text/x-vcard');
1654:                     $vpart->setCharset('UTF-8');
1655:                     $vpart->setContents($registry->call('contacts/ownVCard'));
1656:                     $vpart->setName($options['vcard_attach']);
1657: 
1658:                     $parts[] = $vpart;
1659:                 } catch (Horde_Exception $e) {
1660:                     throw new IMP_Compose_Exception(sprintf(_("Can't attach contact information: %s"), $e->getMessage()));
1661:                 }
1662:             }
1663: 
1664:             if (!empty($parts)) {
1665:                 if (is_null($base) && (count($parts) === 1)) {
1666:                     /* If this is a single attachment with no text, the
1667:                      * attachment IS the message. */
1668:                     $base = reset($parts);
1669:                 } else {
1670:                     $base = new Horde_Mime_Part();
1671:                     $base->setType('multipart/mixed');
1672:                     if (!is_null($textpart)) {
1673:                         $base->addPart($textpart);
1674:                     }
1675:                     foreach ($parts as $val) {
1676:                         $base->addPart($val);
1677:                     }
1678:                 }
1679:             }
1680:         }
1681: 
1682:         /* If we reach this far with no base, we are sending a blank message.
1683:          * Assume this is what the user wants. */
1684:         if (is_null($base)) {
1685:             $base = $textBody;
1686:         }
1687: 
1688:         /* Set up the base message now. */
1689:         $encrypt = empty($options['encrypt'])
1690:             ? IMP::ENCRYPT_NONE
1691:             : $options['encrypt'];
1692:         if ($prefs->getValue('use_pgp') &&
1693:             !empty($conf['gnupg']['path']) &&
1694:             in_array($encrypt, array(IMP_Crypt_Pgp::ENCRYPT, IMP_Crypt_Pgp::SIGN, IMP_Crypt_Pgp::SIGNENC, IMP_Crypt_Pgp::SYM_ENCRYPT, IMP_Crypt_Pgp::SYM_SIGNENC))) {
1695:             $imp_pgp = $injector->getInstance('IMP_Crypt_Pgp');
1696:             $symmetric_passphrase = null;
1697: 
1698:             switch ($encrypt) {
1699:             case IMP_Crypt_Pgp::SIGN:
1700:             case IMP_Crypt_Pgp::SIGNENC:
1701:             case IMP_Crypt_Pgp::SYM_SIGNENC:
1702:                 /* Check to see if we have the user's passphrase yet. */
1703:                 $passphrase = $imp_pgp->getPassphrase('personal');
1704:                 if (empty($passphrase)) {
1705:                     $e = new IMP_Compose_Exception(_("PGP: Need passphrase for personal private key."));
1706:                     $e->encrypt = 'pgp_passphrase_dialog';
1707:                     throw $e;
1708:                 }
1709:                 break;
1710: 
1711:             case IMP_Crypt_Pgp::SYM_ENCRYPT:
1712:             case IMP_Crypt_Pgp::SYM_SIGNENC:
1713:                 /* Check to see if we have the user's symmetric passphrase
1714:                  * yet. */
1715:                 $symmetric_passphrase = $imp_pgp->getPassphrase('symmetric', 'imp_compose_' . $this->_cacheid);
1716:                 if (empty($symmetric_passphrase)) {
1717:                     $e = new IMP_Compose_Exception(_("PGP: Need passphrase to encrypt your message with."));
1718:                     $e->encrypt = 'pgp_symmetric_passphrase_dialog';
1719:                     throw $e;
1720:                 }
1721:                 break;
1722:             }
1723: 
1724:             /* Do the encryption/signing requested. */
1725:             try {
1726:                 switch ($encrypt) {
1727:                 case IMP_Crypt_Pgp::SIGN:
1728:                     $base = $imp_pgp->impSignMimePart($base);
1729:                     $this->_setMetadata('encrypt_sign', true);
1730:                     break;
1731: 
1732:                 case IMP_Crypt_Pgp::ENCRYPT:
1733:                 case IMP_Crypt_Pgp::SYM_ENCRYPT:
1734:                     $to_list = clone $to;
1735:                     if (count($options['from'])) {
1736:                         $to_list->add($options['from']);
1737:                     }
1738:                     $base = $imp_pgp->IMPencryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_ENCRYPT) ? $symmetric_passphrase : null);
1739:                     break;
1740: 
1741:                 case IMP_Crypt_Pgp::SIGNENC:
1742:                 case IMP_Crypt_Pgp::SYM_SIGNENC:
1743:                     $to_list = clone $to;
1744:                     if (count($options['from'])) {
1745:                         $to_list->add($options['from']);
1746:                     }
1747:                     $base = $imp_pgp->IMPsignAndEncryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_SIGNENC) ? $symmetric_passphrase : null);
1748:                     break;
1749:                 }
1750:             } catch (Horde_Exception $e) {
1751:                 throw new IMP_Compose_Exception(_("PGP Error: ") . $e->getMessage(), $e->getCode());
1752:             }
1753:         } elseif ($prefs->getValue('use_smime') &&
1754:                   in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) {
1755:             $imp_smime = $injector->getInstance('IMP_Crypt_Smime');
1756: 
1757:             /* Check to see if we have the user's passphrase yet. */
1758:             if (in_array($encrypt, array(IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) {
1759:                 $passphrase = $imp_smime->getPassphrase();
1760:                 if ($passphrase === false) {
1761:                     $e = new IMP_Compose_Exception(_("S/MIME Error: Need passphrase for personal private key."));
1762:                     $e->encrypt = 'smime_passphrase_dialog';
1763:                     throw $e;
1764:                 }
1765:             }
1766: 
1767:             /* Do the encryption/signing requested. */
1768:             try {
1769:                 switch ($encrypt) {
1770:                 case IMP_Crypt_Smime::SIGN:
1771:                     $base = $imp_smime->IMPsignMIMEPart($base);
1772:                     $this->_setMetadata('encrypt_sign', true);
1773:                     break;
1774: 
1775:                 case IMP_Crypt_Smime::ENCRYPT:
1776:                     $base = $imp_smime->IMPencryptMIMEPart($base, $to[0]);
1777:                     break;
1778: 
1779:                 case IMP_Crypt_Smime::SIGNENC:
1780:                     $base = $imp_smime->IMPsignAndEncryptMIMEPart($base, $to[0]);
1781:                     break;
1782:                 }
1783:             } catch (Horde_Exception $e) {
1784:                 throw new IMP_Compose_Exception(_("S/MIME Error: ") . $e->getMessage(), $e->getCode());
1785:             }
1786:         }
1787: 
1788:         /* Flag this as the base part and rebuild MIME IDs. */
1789:         $base->isBasePart(true);
1790:         $base->buildMimeIds();
1791: 
1792:         return $base;
1793:     }
1794: 
1795:     /**
1796:      * Determines the reply text and headers for a message.
1797:      *
1798:      * @param integer $type           The reply type (self::REPLY* constant).
1799:      * @param IMP_Contents $contents  An IMP_Contents object.
1800:      * @param array $opts             Additional options:
1801:      *   - format: (string) Force to this format.
1802:      *             DEFAULT: Auto-determine.
1803:      *   - to: (string) The recipient of the reply. Overrides the
1804:      *         automatically determined value.
1805:      *
1806:      * @return array  An array with the following keys:
1807:      *   - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List
1808:      *           objects).
1809:      *   - body: (string) The text of the body part.
1810:      *   - format: (string) The format of the body message (html, text).
1811:      *   - identity: (integer) The identity to use for the reply based on the
1812:      *               original message's addresses.
1813:      *   - lang: (array) Language code (keys)/language name (values) of the
1814:      *           original sender's preferred language(s).
1815:      *   - reply_list_id: (string) List ID label.
1816:      *   - reply_recip: (integer) Number of recipients in reply list.
1817:      *   - subject: (string) Formatted subject.
1818:      *   - type: (integer) The reply type used (either self::REPLY_ALL,
1819:      *           self::REPLY_LIST, or self::REPLY_SENDER).
1820:      * @throws IMP_Exception
1821:      */
1822:     public function replyMessage($type, $contents, array $opts = array())
1823:     {
1824:         global $injector, $language, $prefs;
1825: 
1826:         if (!($contents instanceof IMP_Contents)) {
1827:             throw new IMP_Exception(
1828:                 _("Could not retrieve message data from the mail server.")
1829:             );
1830:         }
1831: 
1832:         $alist = new Horde_Mail_Rfc822_List();
1833:         $addr = array(
1834:             'to' => clone $alist,
1835:             'cc' => clone $alist,
1836:             'bcc' => clone $alist
1837:         );
1838: 
1839:         $h = $contents->getHeader();
1840:         $match_identity = $this->_getMatchingIdentity($h);
1841:         $reply_type = self::REPLY_SENDER;
1842: 
1843:         if (!$this->_replytype) {
1844:             $this->_setMetadata('indices', $contents->getIndicesOb());
1845: 
1846:             /* Set the Message-ID related headers (RFC 5322 [3.6.4]). */
1847:             $msg_id = new Horde_Mail_Rfc822_Identification(
1848:                 $h->getValue('message-id')
1849:             );
1850:             if (count($msg_id->ids)) {
1851:                 $this->_setMetadata('in_reply_to', reset($msg_id->ids));
1852:             }
1853: 
1854:             $ref_ob = new Horde_Mail_Rfc822_Identification(
1855:                 $h->getValue('references')
1856:             );
1857:             if (!count($ref_ob->ids)) {
1858:                 $ref_ob = new Horde_Mail_Rfc822_Identification(
1859:                     $h->getValue('in-reply-to')
1860:                 );
1861:                 if (count($ref_ob->ids) > 1) {
1862:                     $ref_ob->ids = array();
1863:                 }
1864:             }
1865: 
1866:             if (count($ref_ob->ids)) {
1867:                 $this->_setMetadata(
1868:                     'references',
1869:                     array_merge($ref_ob->ids, array(reset($msg_id->ids)))
1870:                 );
1871:             }
1872:         }
1873: 
1874:         $subject = strlen($s = $h->getValue('subject'))
1875:             ? 'Re: ' . strval(new Horde_Imap_Client_Data_BaseSubject($s, array('keepblob' => true)))
1876:             : 'Re: ';
1877: 
1878:         $force = false;
1879:         if (in_array($type, array(self::REPLY_AUTO, self::REPLY_SENDER))) {
1880:             if (isset($opts['to'])) {
1881:                 $addr['to']->add($opts['to']);
1882:                 $force = true;
1883:             } elseif ($tmp = $h->getOb('reply-to')) {
1884:                 $addr['to']->add($tmp);
1885:                 $force = true;
1886:             } else {
1887:                 $addr['to']->add($h->getOb('from'));
1888:             }
1889:         }
1890: 
1891:         /* We might need $list_info in the reply_all section. */
1892:         $list_info = in_array($type, array(self::REPLY_AUTO, self::REPLY_LIST))
1893:             ? $injector->getInstance('IMP_Message_Ui')->getListInformation($h)
1894:             : null;
1895: 
1896:         if (!is_null($list_info) && !empty($list_info['reply_list'])) {
1897:             /* If To/Reply-To and List-Reply address are the same, no need
1898:              * to handle these address separately. */
1899:             $rlist = new Horde_Mail_Rfc822_Address($list_info['reply_list']);
1900:             if (!$rlist->match($addr['to'])) {
1901:                 $addr['to'] = clone $alist;
1902:                 $addr['to']->add($rlist);
1903:                 $reply_type = self::REPLY_LIST;
1904:             }
1905:         } elseif (in_array($type, array(self::REPLY_ALL, self::REPLY_AUTO))) {
1906:             /* Clear the To field if we are auto-determining addresses. */
1907:             if ($type == self::REPLY_AUTO) {
1908:                 $addr['to'] = clone $alist;
1909:             }
1910: 
1911:             /* Filter out our own address from the addresses we reply to. */
1912:             $identity = $injector->getInstance('IMP_Identity');
1913:             $all_addrs = $identity->getAllFromAddresses();
1914: 
1915:             /* Build the To: header. It is either:
1916:              * 1) the Reply-To address (if not a personal address)
1917:              * 2) the From address(es) (if it doesn't contain a personal
1918:              * address)
1919:              * 3) all remaining Cc addresses. */
1920:             $to_fields = array('from', 'reply-to');
1921: 
1922:             foreach (array('reply-to', 'from', 'to', 'cc') as $val) {
1923:                 /* If either a reply-to or $to is present, we use this address
1924:                  * INSTEAD of the from address. */
1925:                 if (($force && ($val == 'from')) ||
1926:                     !($ob = $h->getOb($val))) {
1927:                     continue;
1928:                 }
1929: 
1930:                 /* For From: need to check if at least one of the addresses is
1931:                  * personal. */
1932:                 if ($val == 'from') {
1933:                     foreach ($ob->raw_addresses as $addr_ob) {
1934:                         if ($all_addrs->contains($addr_ob)) {
1935:                             /* The from field contained a personal address.
1936:                              * Use the 'To' header as the primary reply-to
1937:                              * address instead. */
1938:                             $to_fields[] = 'to';
1939: 
1940:                             /* Add other non-personal from addresses to the
1941:                              * list of CC addresses. */
1942:                             $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs);
1943:                             $addr['cc']->add($ob);
1944:                             $all_addrs->add($ob);
1945:                             continue 2;
1946:                         }
1947:                     }
1948:                 }
1949: 
1950:                 $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs);
1951: 
1952:                 foreach ($ob as $hdr_ob) {
1953:                     if ($hdr_ob instanceof Horde_Mail_Rfc822_Group) {
1954:                         $addr['cc']->add($hdr_ob);
1955:                         $all_addrs->add($hdr_ob->addresses);
1956:                     } elseif (($val != 'to') ||
1957:                               is_null($list_info) ||
1958:                               !$force ||
1959:                               empty($list_info['exists'])) {
1960:                         /* Don't add as To address if this is a list that
1961:                          * doesn't have a post address but does have a
1962:                          * reply-to address. */
1963:                         if (in_array($val, $to_fields)) {
1964:                             /* If from/reply-to doesn't have personal
1965:                              * information, check from address. */
1966:                             if (is_null($hdr_ob->personal) &&
1967:                                 ($to_ob = $h->getOb('from')) &&
1968:                                 !is_null($to_ob[0]->personal) &&
1969:                                 ($hdr_ob->match($to_ob[0]))) {
1970:                                 $addr['to']->add($to_ob);
1971:                             } else {
1972:                                 $addr['to']->add($hdr_ob);
1973:                             }
1974:                         } else {
1975:                             $addr['cc']->add($hdr_ob);
1976:                         }
1977: 
1978:                         $all_addrs->add($hdr_ob);
1979:                     }
1980:                 }
1981:             }
1982: 
1983:             /* Build the Cc: (or possibly the To:) header. If this is a
1984:              * reply to a message that was already replied to by the user,
1985:              * this reply will go to the original recipients (Request
1986:              * #8485).  */
1987:             if (count($addr['cc'])) {
1988:                 $reply_type = self::REPLY_ALL;
1989:             }
1990:             if (!count($addr['to'])) {
1991:                 $addr['to'] = $addr['cc'];
1992:                 $addr['cc'] = clone $alist;
1993:             }
1994: 
1995:             /* Build the Bcc: header. */
1996:             if ($bcc = $h->getOb('bcc')) {
1997:                 $bcc->add($identity->getBccAddresses());
1998:                 $bcc->setIteratorFilter(0, $all_addrs);
1999:                 foreach ($bcc as $val) {
2000:                     $addr['bcc']->add($val);
2001:                 }
2002:             }
2003:         }
2004: 
2005:         if (!$this->_replytype || ($reply_type != $this->_replytype)) {
2006:             $this->_replytype = $reply_type;
2007:             $this->changed = 'changed';
2008:         }
2009: 
2010:         $ret = $this->replyMessageText($contents, array(
2011:             'format' => isset($opts['format']) ? $opts['format'] : null
2012:         ));
2013:         if ($prefs->getValue('reply_charset') &&
2014:             ($ret['charset'] != $this->charset)) {
2015:             $this->charset = $ret['charset'];
2016:             $this->changed = 'changed';
2017:         }
2018:         unset($ret['charset']);
2019: 
2020:         if ($type == self::REPLY_AUTO) {
2021:             switch ($reply_type) {
2022:             case self::REPLY_ALL:
2023:                 try {
2024:                     $recip_list = $this->recipientList($addr);
2025:                     $ret['reply_recip'] = count($recip_list['list']);
2026:                 } catch (IMP_Compose_Exception $e) {
2027:                     $ret['reply_recip'] = 0;
2028:                 }
2029:                 break;
2030: 
2031:             case self::REPLY_LIST:
2032:                 if (($list_parse = $injector->getInstance('Horde_ListHeaders')->parse('list-id', $h->getValue('list-id'))) &&
2033:                     !is_null($list_parse->label)) {
2034:                     $ret['reply_list_id'] = $list_parse->label;
2035:                 }
2036:                 break;
2037:             }
2038:         }
2039: 
2040:         if (($lang = $h->getValue('accept-language')) ||
2041:             ($lang = $h->getValue('x-accept-language'))) {
2042:             $langs = array();
2043:             foreach (explode(',', $lang) as $val) {
2044:                 if (($name = Horde_Nls::getLanguageISO($val)) !== null) {
2045:                     $langs[trim($val)] = $name;
2046:                 }
2047:             }
2048:             $ret['lang'] = array_unique($langs);
2049: 
2050:             /* Don't show display if original recipient is asking for reply in
2051:              * the user's native language. */
2052:             if ((count($ret['lang']) == 1) &&
2053:                 reset($ret['lang']) &&
2054:                 (substr(key($ret['lang']), 0, 2) == substr($language, 0, 2))) {
2055:                 unset($ret['lang']);
2056:             }
2057:         }
2058: 
2059:         return array_merge(array(
2060:             'addr' => $addr,
2061:             'identity' => $match_identity,
2062:             'subject' => $subject,
2063:             'type' => $reply_type
2064:         ), $ret);
2065:     }
2066: 
2067:     /**
2068:      * Returns the reply text for a message.
2069:      *
2070:      * @param IMP_Contents $contents  An IMP_Contents object.
2071:      * @param array $opts             Additional options:
2072:      *   - format: (string) Force to this format.
2073:      *             DEFAULT: Auto-determine.
2074:      *
2075:      * @return array  An array with the following keys:
2076:      *   - body: (string) The text of the body part.
2077:      *   - charset: (string) The guessed charset to use for the reply.
2078:      *   - format: (string) The format of the body message ('html', 'text').
2079:      */
2080:     public function replyMessageText($contents, array $opts = array())
2081:     {
2082:         global $prefs;
2083: 
2084:         if (!$prefs->getValue('reply_quote')) {
2085:             return array(
2086:                 'body' => '',
2087:                 'charset' => '',
2088:                 'format' => 'text'
2089:             );
2090:         }
2091: 
2092:         $h = $contents->getHeader();
2093: 
2094:         $from = $h->getOb('from');
2095: 
2096:         if ($prefs->getValue('reply_headers') && !empty($h)) {
2097:             $from_text = strval(new IMP_Prefs_AttribText($from, $h, '%f'));
2098: 
2099:             $msg_pre = '----- ' .
2100:                 ($from_text ? sprintf(_("Message from %s"), $from_text) : _("Message")) .
2101:                 /* Extra '-'s line up with "End Message" below. */
2102:                 " ---------\n" .
2103:                 $this->_getMsgHeaders($h);
2104: 
2105:             $msg_post = "\n\n----- " .
2106:                 ($from_text ? sprintf(_("End message from %s"), $from_text) : _("End message")) .
2107:                 " -----\n";
2108:         } else {
2109:             $msg_pre = strval(new IMP_Prefs_AttribText($from, $h));
2110:             $msg_post = '';
2111:         }
2112: 
2113:         list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'reply_format');
2114: 
2115:         $msg_text = $this->_getMessageText($contents, array(
2116:             'html' => $compose_html,
2117:             'replylimit' => true,
2118:             'toflowed' => true
2119:         ));
2120: 
2121:         if (!empty($msg_text) &&
2122:             (($msg_text['mode'] == 'html') || $force_html)) {
2123:             $msg = '<p>' . $this->text2html(trim($msg_pre)) . '</p>' .
2124:                    self::HTML_BLOCKQUOTE .
2125:                    (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['flowed'] ? $msg_text['flowed'] : $msg_text['text']) : $msg_text['text']) .
2126:                    '</blockquote><br />' .
2127:                    ($msg_post ? $this->text2html($msg_post) : '') . '<br />';
2128:             $msg_text['mode'] = 'html';
2129:         } else {
2130:             $msg = empty($msg_text['text'])
2131:                 ? '[' . _("No message body text") . ']'
2132:                 : $msg_pre . "\n\n" . $msg_text['text'] . $msg_post;
2133:             $msg_text['mode'] = 'text';
2134:         }
2135: 
2136:         // Bug #10148: Message text might be us-ascii, but reply headers may
2137:         // contain 8-bit characters.
2138:         if (($msg_text['charset'] == 'us-ascii') &&
2139:             (Horde_Mime::is8bit($msg_pre, 'UTF-8') ||
2140:              Horde_Mime::is8bit($msg_post, 'UTF-8'))) {
2141:             $msg_text['charset'] = 'UTF-8';
2142:         }
2143: 
2144:         return array(
2145:             'body' => $msg . "\n",
2146:             'charset' => $msg_text['charset'],
2147:             'format' => $msg_text['mode']
2148:         );
2149:     }
2150: 
2151:     /**
2152:      * Determine text editor format.
2153:      *
2154:      * @param array $opts        Options (contains 'format' param).
2155:      * @param string $pref_name  The pref name that controls formatting.
2156:      *
2157:      * @return array  Use HTML? and Force HTML?
2158:      */
2159:     protected function _msgTextFormat($opts, $pref_name)
2160:     {
2161:         if (!empty($opts['format'])) {
2162:             $compose_html = $force_html = ($opts['format'] == 'html');
2163:         } elseif ($GLOBALS['prefs']->getValue('compose_html')) {
2164:             $compose_html = $force_html = true;
2165:         } else {
2166:             $compose_html = $GLOBALS['prefs']->getValue($pref_name);
2167:             $force_html = false;
2168:         }
2169: 
2170:         return array($compose_html, $force_html);
2171:     }
2172: 
2173:     /**
2174:      * Determine the text and headers for a forwarded message.
2175:      *
2176:      * @param integer $type           The forward type (self::FORWARD*
2177:      *                                constant).
2178:      * @param IMP_Contents $contents  An IMP_Contents object.
2179:      * @param boolean $attach         Attach the forwarded message?
2180:      * @param array $opts             Additional options:
2181:      *   - format: (string) Force to this format.
2182:      *             DEFAULT: Auto-determine.
2183:      *
2184:      * @return array  An array with the following keys:
2185:      *   - attach: (boolean) True if original message was attached.
2186:      *   - body: (string) The text of the body part.
2187:      *   - format: (string) The format of the body message ('html', 'text').
2188:      *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
2189:      *   - subject: (string) Formatted subject.
2190:      *   - title: (string) Title to use on page.
2191:      *   - type: (integer) - The compose type.
2192:      * @throws IMP_Exception
2193:      */
2194:     public function forwardMessage($type, $contents, $attach = true,
2195:                                    array $opts = array())
2196:     {
2197:         global $prefs;
2198: 
2199:         if (!($contents instanceof IMP_Contents)) {
2200:             throw new IMP_Exception(
2201:                 _("Could not retrieve message data from the mail server.")
2202:             );
2203:         }
2204: 
2205:         if ($type == self::FORWARD_AUTO) {
2206:             switch ($prefs->getValue('forward_default')) {
2207:             case 'body':
2208:                 $type = self::FORWARD_BODY;
2209:                 break;
2210: 
2211:             case 'both':
2212:                 $type = self::FORWARD_BOTH;
2213:                 break;
2214: 
2215:             case 'editasnew':
2216:                 $ret = $this->editAsNew(new IMP_Indices($contents));
2217:                 $ret['title'] = _("New Message");
2218:                 return $ret;
2219: 
2220:             case 'attach':
2221:             default:
2222:                 $type = self::FORWARD_ATTACH;
2223:                 break;
2224:             }
2225:         }
2226: 
2227:         $h = $contents->getHeader();
2228: 
2229:         $this->_replytype = $type;
2230:         $this->_setMetadata('indices', $contents->getIndicesOb());
2231: 
2232:         if (strlen($s = $h->getValue('subject'))) {
2233:             $s = strval(new Horde_Imap_Client_Data_BaseSubject($s, array(
2234:                 'keepblob' => true
2235:             )));
2236:             $subject = 'Fwd: ' . $s;
2237:             $title = _("Forward") . ': ' . $s;
2238:         } else {
2239:             $subject = 'Fwd:';
2240:             $title = _("Forward");
2241:         }
2242: 
2243:         $fwd_attach = false;
2244:         if ($attach &&
2245:             in_array($type, array(self::FORWARD_ATTACH, self::FORWARD_BOTH))) {
2246:             try {
2247:                 $this->attachImapMessage(new IMP_Indices($contents));
2248:                 $fwd_attach = true;
2249:             } catch (IMP_Exception $e) {}
2250:         }
2251: 
2252:         if (in_array($type, array(self::FORWARD_BODY, self::FORWARD_BOTH))) {
2253:             $ret = $this->forwardMessageText($contents, array(
2254:                 'format' => isset($opts['format']) ? $opts['format'] : null
2255:             ));
2256:             unset($ret['charset']);
2257:         } else {
2258:             $ret = array(
2259:                 'body' => '',
2260:                 'format' => $prefs->getValue('compose_html') ? 'html' : 'text'
2261:             );
2262:         }
2263: 
2264:         return array_merge(array(
2265:             'attach' => $fwd_attach,
2266:             'identity' => $this->_getMatchingIdentity($h),
2267:             'subject' => $subject,
2268:             'title' => $title,
2269:             'type' => $type
2270:         ), $ret);
2271:     }
2272: 
2273:     /**
2274:      * Returns the forward text for a message.
2275:      *
2276:      * @param IMP_Contents $contents  An IMP_Contents object.
2277:      * @param array $opts             Additional options:
2278:      *   - format: (string) Force to this format.
2279:      *             DEFAULT: Auto-determine.
2280:      *
2281:      * @return array  An array with the following keys:
2282:      *   - body: (string) The text of the body part.
2283:      *   - charset: (string) The guessed charset to use for the forward.
2284:      *   - format: (string) The format of the body message ('html', 'text').
2285:      */
2286:     public function forwardMessageText($contents, array $opts = array())
2287:     {
2288:         $h = $contents->getHeader();
2289: 
2290:         $from = strval($h->getOb('from'));
2291: 
2292:         $msg_pre = "\n----- " .
2293:             ($from ? sprintf(_("Forwarded message from %s"), $from) : _("Forwarded message")) .
2294:             " -----\n" . $this->_getMsgHeaders($h) . "\n";
2295:         $msg_post = "\n\n----- " . _("End forwarded message") . " -----\n";
2296: 
2297:         list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'forward_format');
2298: 
2299:         $msg_text = $this->_getMessageText($contents, array(
2300:             'html' => $compose_html
2301:         ));
2302: 
2303:         if (!empty($msg_text) &&
2304:             (($msg_text['mode'] == 'html') || $force_html)) {
2305:             $msg = $this->text2html($msg_pre) .
2306:                 (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['text']) : $msg_text['text']) .
2307:                 $this->text2html($msg_post);
2308:             $format = 'html';
2309:         } else {
2310:             $msg = $msg_pre . $msg_text['text'] . $msg_post;
2311:             $format = 'text';
2312:         }
2313: 
2314:         // Bug #10148: Message text might be us-ascii, but forward headers may
2315:         // contain 8-bit characters.
2316:         if (($msg_text['charset'] == 'us-ascii') &&
2317:             (Horde_Mime::is8bit($msg_pre, 'UTF-8') ||
2318:              Horde_Mime::is8bit($msg_post, 'UTF-8'))) {
2319:             $msg_text['charset'] = 'UTF-8';
2320:         }
2321: 
2322:         return array(
2323:             'body' => $msg,
2324:             'charset' => $msg_text['charset'],
2325:             'format' => $format
2326:         );
2327:     }
2328: 
2329:     /**
2330:      * Prepares a forwarded message using multiple messages.
2331:      *
2332:      * @param IMP_Indices $indices  An indices object containing the indices
2333:      *                              of the forwarded messages.
2334:      *
2335:      * @return array  An array with the following keys:
2336:      *   - body: (string) The text of the body part.
2337:      *   - format: (string) The format of the body message ('html', 'text').
2338:      *   - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity().
2339:      *   - subject: (string) Formatted subject.
2340:      *   - title: (string) Title to use on page.
2341:      *   - type: (integer) The compose type.
2342:      */
2343:     public function forwardMultipleMessages(IMP_Indices $indices)
2344:     {
2345:         global $injector, $prefs, $session;
2346: 
2347:         $this->_setMetadata('indices', $indices);
2348:         $this->_replytype = self::FORWARD_ATTACH;
2349: 
2350:         $subject = $this->attachImapMessage($indices);
2351: 
2352:         return array(
2353:             'body' => '',
2354:             'format' => ($prefs->getValue('compose_html') && $session->get('imp', 'rteavail')) ? 'html' : 'text',
2355:             'identity' => $injector->getInstance('IMP_Identity')->getDefault(),
2356:             'subject' => $subject,
2357:             'title' => $subject,
2358:             'type' => self::FORWARD
2359:         );
2360:     }
2361: 
2362:     /**
2363:      * Prepare a redirect message.
2364:      *
2365:      * @param IMP_Indices $indices  An indices object.
2366:      */
2367:     public function redirectMessage(IMP_Indices $indices)
2368:     {
2369:         $this->_setMetadata('redirect_indices', $indices);
2370:         $this->_replytype = self::REDIRECT;
2371:     }
2372: 
2373:     /**
2374:      * Send a redirect (a/k/a resent) message. See RFC 5322 [3.6.6].
2375:      *
2376:      * @param mixed $to  The addresses to redirect to.
2377:      * @param boolean $log  Whether to log the resending in the history and
2378:      *                      sentmail log.
2379:      *
2380:      * @return array  An object with the following properties for each
2381:      *                redirected message:
2382:      *   - contents: (IMP_Contents) The contents object.
2383:      *   - headers: (Horde_Mime_Headers) The header object.
2384:      *   - mbox: (IMP_Mailbox) Mailbox of the message.
2385:      *   - uid: (string) UID of the message.
2386:      *
2387:      * @throws IMP_Compose_Exception
2388:      */
2389:     public function sendRedirectMessage($to, $log = true)
2390:     {
2391:         global $injector, $registry;
2392: 
2393:         $recip = $this->recipientList(array('to' => $to));
2394: 
2395:         $identity = $injector->getInstance('IMP_Identity');
2396:         $from_addr = $identity->getFromAddress();
2397: 
2398:         $out = array();
2399: 
2400:         foreach ($this->getMetadata('redirect_indices') as $val) {
2401:             foreach ($val->uids as $val2) {
2402:                 try {
2403:                     $contents = $injector->getInstance('IMP_Factory_Contents')->create($val->mbox->getIndicesOb($val2));
2404:                 } catch (IMP_Exception $e) {
2405:                     throw new IMP_Compose_Exception(_("Error when redirecting message."));
2406:                 }
2407: 
2408:                 $headers = $contents->getHeader();
2409: 
2410:                 /* We need to set the Return-Path header to the current user -
2411:                  * see RFC 2821 [4.4]. */
2412:                 $headers->removeHeader('return-path');
2413:                 $headers->addHeader('Return-Path', $from_addr);
2414: 
2415:                 /* Generate the 'Resent' headers (RFC 5322 [3.6.6]). These
2416:                  * headers are prepended to the message. */
2417:                 $resent_headers = new Horde_Mime_Headers();
2418:                 $resent_headers->addHeader('Resent-Date', date('r'));
2419:                 $resent_headers->addHeader('Resent-From', $from_addr);
2420:                 $resent_headers->addHeader('Resent-To', $recip['header']['to']);
2421:                 $resent_headers->addHeader('Resent-Message-ID', $headers->generateMessageId());
2422: 
2423:                 $header_text = trim($resent_headers->toString(array('encode' => 'UTF-8'))) . "\n" . trim($contents->getHeader(IMP_Contents::HEADER_TEXT));
2424: 
2425:                 $this->_prepSendMessageAssert($recip['list']);
2426:                 $to = $this->_prepSendMessage($recip['list']);
2427:                 $hdr_array = $headers->toArray(array('charset' => 'UTF-8'));
2428:                 $hdr_array['_raw'] = $header_text;
2429: 
2430:                 try {
2431:                     $injector->getInstance('IMP_Mail')->send($to, $hdr_array, $contents->getBody());
2432:                 } catch (Horde_Mail_Exception $e) {
2433:                     throw new IMP_Compose_Exception($e);
2434:                 }
2435: 
2436:                 $recipients = strval($recip['list']);
2437: 
2438:                 Horde::log(sprintf("%s Redirected message sent to %s from %s", $_SERVER['REMOTE_ADDR'], $recipients, $registry->getAuth()), 'INFO');
2439: 
2440:                 if ($log) {
2441:                     /* Store history information. */
2442:                     $msg_id = new Horde_Mail_Rfc822_Identification(
2443:                         $headers->getValue('message-id')
2444:                     );
2445: 
2446:                     $injector->getInstance('IMP_Maillog')->log(
2447:                         new IMP_Maillog_Message(reset($msg_id->ids)),
2448:                         new IMP_Maillog_Log_Redirect($recipients)
2449:                     );
2450: 
2451:                     $injector->getInstance('IMP_Sentmail')->log(
2452:                         IMP_Sentmail::REDIRECT,
2453:                         reset($msg_id->ids),
2454:                         $recipients
2455:                     );
2456:                 }
2457: 
2458:                 $tmp = new stdClass;
2459:                 $tmp->contents = $contents;
2460:                 $tmp->headers = $headers;
2461:                 $tmp->mbox = $val->mbox;
2462:                 $tmp->uid = $val2;
2463: 
2464:                 $out[] = $tmp;
2465:             }
2466:         }
2467: 
2468:         return $out;
2469:     }
2470: 
2471:     /**
2472:      * Get "tieto" identity information.
2473:      *
2474:      * @param Horde_Mime_Headers $h  The headers object for the message.
2475:      * @param array $only            Only use these headers.
2476:      *
2477:      * @return integer  The matching identity. If no exact match, returns the
2478:      *                  default identity.
2479:      */
2480:     protected function _getMatchingIdentity($h, array $only = array())
2481:     {
2482:         global $injector;
2483: 
2484:         $identity = $injector->getInstance('IMP_Identity');
2485:         $msgAddresses = array();
2486:         if (empty($only)) {
2487:             /* Bug #9271: Check 'from' address first; if replying to a message
2488:              * originally sent by user, this should be the identity used for
2489:              * the reply also. */
2490:             $only = array('from', 'to', 'cc', 'bcc');
2491:         }
2492: 
2493:         foreach ($only as $val) {
2494:             $msgAddresses[] = $h->getValue($val);
2495:         }
2496: 
2497:         $match = $identity->getMatchingIdentity($msgAddresses);
2498: 
2499:         return is_null($match)
2500:             ? $identity->getDefault()
2501:             : $match;
2502:     }
2503: 
2504:     /**
2505:      * Add mail message(s) from the mail server as a message/rfc822
2506:      * attachment.
2507:      *
2508:      * @param IMP_Indices $indices  An indices object.
2509:      *
2510:      * @return string  Subject string.
2511:      *
2512:      * @throws IMP_Exception
2513:      */
2514:     public function attachImapMessage($indices)
2515:     {
2516:         if (!count($indices)) {
2517:             return false;
2518:         }
2519: 
2520:         $attached = 0;
2521:         foreach ($indices as $ob) {
2522:             foreach ($ob->uids as $idx) {
2523:                 ++$attached;
2524:                 $contents = $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create(new IMP_Indices($ob->mbox, $idx));
2525:                 $headerob = $contents->getHeader();
2526: 
2527:                 $part = new Horde_Mime_Part();
2528:                 $part->setCharset('UTF-8');
2529:                 $part->setType('message/rfc822');
2530:                 $part->setName(_("Forwarded Message"));
2531:                 $part->setContents($contents->fullMessageText(array(
2532:                     'stream' => true
2533:                 )), array(
2534:                     'usestream' => true
2535:                 ));
2536: 
2537:                 // Throws IMP_Compose_Exception.
2538:                 $this->addAttachmentFromPart($part);
2539: 
2540:                 $part->clearContents();
2541:             }
2542:         }
2543: 
2544:         if ($attached > 1) {
2545:             return 'Fwd: ' . sprintf(_("%u Forwarded Messages"), $attached);
2546:         }
2547: 
2548:         if ($name = $headerob->getValue('subject')) {
2549:             $name = Horde_String::truncate($name, 80);
2550:         } else {
2551:             $name = _("[No Subject]");
2552:         }
2553: 
2554:         return 'Fwd: ' . strval(new Horde_Imap_Client_Data_BaseSubject($name, array('keepblob' => true)));
2555:     }
2556: 
2557:     /**
2558:      * Determine the header information to display in the forward/reply.
2559:      *
2560:      * @param Horde_Mime_Headers $h  The headers object for the message.
2561:      *
2562:      * @return string  The header information for the original message.
2563:      */
2564:     protected function _getMsgHeaders($h)
2565:     {
2566:         $tmp = array();
2567: 
2568:         if (($ob = $h->getValue('date'))) {
2569:             $tmp[_("Date")] = $ob;
2570:         }
2571: 
2572:         if (($ob = strval($h->getOb('from')))) {
2573:             $tmp[_("From")] = $ob;
2574:         }
2575: 
2576:         if (($ob = strval($h->getOb('reply-to')))) {
2577:             $tmp[_("Reply-To")] = $ob;
2578:         }
2579: 
2580:         if (($ob = $h->getValue('subject'))) {
2581:             $tmp[_("Subject")] = $ob;
2582:         }
2583: 
2584:         if (($ob = strval($h->getOb('to')))) {
2585:             $tmp[_("To")] = $ob;
2586:         }
2587: 
2588:         if (($ob = strval($h->getOb('cc')))) {
2589:             $tmp[_("Cc")] = $ob;
2590:         }
2591: 
2592:         $text = '';
2593: 
2594:         if (!empty($tmp)) {
2595:             $max = max(array_map(array('Horde_String', 'length'), array_keys($tmp))) + 2;
2596: 
2597:             foreach ($tmp as $key => $val) {
2598:                 $text .= Horde_String::pad($key . ': ', $max, ' ', STR_PAD_LEFT) . $val . "\n";
2599:             }
2600:         }
2601: 
2602:         return $text;
2603:     }
2604: 
2605:     /**
2606:      * Add an attachment referred to in a related part.
2607:      *
2608:      * @param IMP_Compose_Attachment $act_ob  Attachment data.
2609:      * @param DOMElement $node                Node element containg the
2610:      *                                        related reference.
2611:      * @param string $attribute               Element attribute containing the
2612:      *                                        related reference.
2613:      */
2614:     public function addRelatedAttachment(IMP_Compose_Attachment $atc_ob,
2615:                                          DOMElement $node, $attribute)
2616:     {
2617:         $atc_ob->related = true;
2618:         $node->setAttribute(self::RELATED_ATTR, $attribute . ';' . $atc_ob->id);
2619:     }
2620: 
2621:     /**
2622:      * Deletes all attachments.
2623:      */
2624:     public function deleteAllAttachments()
2625:     {
2626:         foreach (array_keys($this->_atc) as $key) {
2627:             unset($this[$key]);
2628:         }
2629:     }
2630: 
2631:     /**
2632:      * Obtains the cache ID for the session object.
2633:      *
2634:      * @return string  The message cache ID.
2635:      */
2636:     public function getCacheId()
2637:     {
2638:         return $this->_cacheid;
2639:     }
2640: 
2641:     /**
2642:      * Generate HMAC hash used to validate data on a session expiration. Uses
2643:      * the unique compose cache ID of the expired message, the username, and
2644:      * the secret key of the server to generate a reproducible value that can
2645:      * be validated if session data doesn't exist.
2646:      *
2647:      * @param string $cacheid  The cache ID to use. If null, uses cache ID of
2648:      *                         the compose object.
2649:      * @param string $user     The user ID to use. If null, uses the current
2650:      *                         authenticated username.
2651:      *
2652:      * @return string  The HMAC hash string.
2653:      */
2654:     public function getHmac($cacheid = null, $user = null)
2655:     {
2656:         global $conf, $registry;
2657: 
2658:         return hash_hmac(
2659:             (PHP_MINOR_VERSION >= 4) ? 'fnv132' : 'sha1',
2660:             (is_null($cacheid) ? $this->getCacheId() : $cacheid) . '|' .
2661:                 (is_null($user) ? $registry->getAuth() : $user),
2662:             $conf['secret_key']
2663:         );
2664:     }
2665: 
2666:     /**
2667:      * How many more attachments are allowed?
2668:      *
2669:      * @return mixed  Returns true if no attachment limit.
2670:      *                Else returns the number of additional attachments
2671:      *                allowed.
2672:      */
2673:     public function additionalAttachmentsAllowed()
2674:     {
2675:         global $conf;
2676: 
2677:         return empty($conf['compose']['attach_count_limit'])
2678:             ? true
2679:             : ($conf['compose']['attach_count_limit'] - count($this));
2680:     }
2681: 
2682:     /**
2683:      * What is the maximum attachment size?
2684:      *
2685:      * @return integer  The maximum attachment size (in bytes).
2686:      */
2687:     public function maxAttachmentSize()
2688:     {
2689:         $size = $GLOBALS['session']->get('imp', 'file_upload');
2690: 
2691:         return empty($GLOBALS['conf']['compose']['attach_size_limit'])
2692:             ? $size
2693:             : min($size, $GLOBALS['conf']['compose']['attach_size_limit']);
2694:     }
2695: 
2696:     /**
2697:      * Clean outgoing HTML (remove unexpected data URLs).
2698:      *
2699:      * @param Horde_Domhtml $html  The HTML data.
2700:      */
2701:     protected function _cleanHtmlOutput(Horde_Domhtml $html)
2702:     {
2703:         global $registry;
2704: 
2705:         $xpath = new DOMXPath($html->dom);
2706: 
2707:         foreach ($xpath->query('//*[@src]') as $node) {
2708:             $src = $node->getAttribute('src');
2709: 
2710:             /* Check for attempts to sneak data URL information into the
2711:              * output. */
2712:             if (Horde_Url_Data::isData($src)) {
2713:                 if (IMP_Compose_HtmlSignature::isSigImage($node, true)) {
2714:                     /* This is HTML signature image data. Convert to an
2715:                      * attachment. */
2716:                     $sig_img = new Horde_Url_Data($src);
2717:                     if ($sig_img->data) {
2718:                         $data_part = new Horde_Mime_Part();
2719:                         $data_part->setContents($sig_img->data);
2720:                         $data_part->setType($sig_img->type);
2721: 
2722:                         try {
2723:                             $this->addRelatedAttachment(
2724:                                 $this->addAttachmentFromPart($data_part),
2725:                                 $node,
2726:                                 'src'
2727:                             );
2728:                         } catch (IMP_Compose_Exception $e) {
2729:                             // Remove image on error.
2730:                         }
2731:                     }
2732:                 }
2733: 
2734:                 $node->removeAttribute('src');
2735:             } elseif (strcasecmp($node->tagName, 'IMG') === 0) {
2736:                 /* Check for smileys. They live in the JS directory, under
2737:                  * the base ckeditor directory, so search for that and replace
2738:                  * with the filesystem information if found (Request
2739:                  * #13051). Need to ignore other image links that may have
2740:                  * been explicitly added by the user. */
2741:                 $js_path = strval(Horde::url($registry->get('jsuri', 'horde'), true));
2742:                 if (stripos($src, $js_path . '/ckeditor') === 0) {
2743:                     $file = str_replace(
2744:                         $js_path,
2745:                         $registry->get('jsfs', 'horde'),
2746:                         $src
2747:                     );
2748: 
2749:                     if (is_readable($file)) {
2750:                         $data_part = new Horde_Mime_Part();
2751:                         $data_part->setContents(file_get_contents($file));
2752:                         $data_part->setName(basename($file));
2753: 
2754:                         try {
2755:                             $this->addRelatedAttachment(
2756:                                 $this->addAttachmentFromPart($data_part),
2757:                                 $node,
2758:                                 'src'
2759:                             );
2760:                         } catch (IMP_Compose_Exception $e) {
2761:                             // Keep existing data on error.
2762:                         }
2763:                     }
2764:                 }
2765:             }
2766:         }
2767:     }
2768: 
2769:     /**
2770:      * Converts an HTML part to a multipart/related part, if necessary.
2771:      *
2772:      * @param Horde_Domhtml $html    HTML data.
2773:      * @param Horde_Mime_Part $part  The HTML part.
2774:      *
2775:      * @return Horde_Mime_Part  The part to add to the compose output.
2776:      */
2777:     protected function _convertToRelated(Horde_Domhtml $html,
2778:                                          Horde_Mime_Part $part)
2779:     {
2780:         $r_part = false;
2781:         foreach ($this as $atc) {
2782:             if ($atc->related) {
2783:                 $r_part = true;
2784:                 break;
2785:             }
2786:         }
2787: 
2788:         if (!$r_part) {
2789:             return $part;
2790:         }
2791: 
2792:         /* Create new multipart/related part. */
2793:         $related = new Horde_Mime_Part();
2794:         $related->setType('multipart/related');
2795:         /* Get the CID for the 'root' part. Although by default the first part
2796:          * is the root part (RFC 2387 [3.2]), we may as well be explicit and
2797:          * put the CID in the 'start' parameter. */
2798:         $related->setContentTypeParameter('start', $part->setContentId());
2799:         $related->addPart($part);
2800: 
2801:         /* HTML iteration is from child->parent, so need to gather related
2802:          * parts and add at end after sorting to generate a more sensible
2803:          * attachment list. */
2804:         $add = array();
2805: 
2806:         foreach ($html as $node) {
2807:             if (($node instanceof DOMElement) &&
2808:                 $node->hasAttribute(self::RELATED_ATTR)) {
2809:                 list($attr_name, $atc_id) = explode(';', $node->getAttribute(self::RELATED_ATTR));
2810: 
2811:                 /* If attachment can't be found, ignore. */
2812:                 if ($r_atc = $this[$atc_id]) {
2813:                     if ($r_atc->linked) {
2814:                         $attr = strval($r_atc->link_url);
2815:                     } else {
2816:                         $related_part = $r_atc->getPart(true);
2817:                         $attr = 'cid:' . $related_part->setContentId();
2818:                         $add[] = $related_part;
2819:                     }
2820: 
2821:                     $node->setAttribute($attr_name, $attr);
2822:                 }
2823: 
2824:                 $node->removeAttribute(self::RELATED_ATTR);
2825:             }
2826:         }
2827: 
2828:         array_map(array($related, 'addPart'), array_reverse($add));
2829: 
2830:         return $related;
2831:     }
2832: 
2833:     /**
2834:      * Adds linked attachments to message.
2835:      *
2836:      * @param string &$body  Plaintext data.
2837:      * @param mixed $html    HTML data (Horde_Domhtml) or null.
2838:      *
2839:      * @throws IMP_Compose_Exception
2840:      */
2841:     protected function _linkAttachments(&$body, $html)
2842:     {
2843:         global $conf;
2844: 
2845:         $link_all = false;
2846:         $linked = array();
2847: 
2848:         if (!empty($conf['compose']['link_attach_size_hard'])) {
2849:             $limit = intval($conf['compose']['link_attach_size_hard']);
2850:             foreach ($this as $val) {
2851:                 if (($limit -= $val->getPart()->getBytes()) < 0) {
2852:                     $link_all = true;
2853:                     break;
2854:                 }
2855:             }
2856:         }
2857: 
2858:         foreach (iterator_to_array($this) as $key => $val) {
2859:             if ($link_all && !$val->linked) {
2860:                 $val = new IMP_Compose_Attachment($this, $val->getPart(), $val->storage->getTempFile());
2861:                 $val->forceLinked = true;
2862:                 unset($this[$key]);
2863:                 $this[$key] = $val;
2864:             }
2865: 
2866:             if ($val->linked && !$val->related) {
2867:                 $linked[] = $val;
2868:             }
2869:         }
2870: 
2871:         if (empty($linked)) {
2872:             return;
2873:         }
2874: 
2875:         if ($del_time = IMP_Compose_LinkedAttachment::keepDate(false)) {
2876:             /* Subtract 1 from time to get the last day of the previous
2877:              * month. */
2878:             $expire = ' (' . sprintf(_("links will expire on %s"), strftime('%x', $del_time - 1)) . ')';
2879:         }
2880: 
2881:         $body .= "\n-----\n" . _("Attachments") . $expire . ":\n";
2882:         if ($html) {
2883:             $body = $html->getBody();
2884:             $dom = $html->dom;
2885: 
2886:             $body->appendChild($dom->createElement('HR'));
2887:             $body->appendChild($div = $dom->createElement('DIV'));
2888:             $div->appendChild($dom->createElement('H4', _("Attachments") . $expire . ':'));
2889:             $div->appendChild($ol = $dom->createElement('OL'));
2890:         }
2891: 
2892:         $i = 0;
2893:         foreach ($linked as $val) {
2894:             $apart = $val->getPart();
2895:             $name = $apart->getName(true);
2896:             $size = IMP::sizeFormat($apart->getBytes());
2897:             $url = strval($val->link_url->setRaw(true));
2898: 
2899:             $body .= "\n" . (++$i) . '. ' .
2900:                 $name . ' (' . $size . ') [' . $apart->getType() . "]\n" .
2901:                 sprintf(_("Download link: %s"), $url) . "\n";
2902: 
2903:             if ($html) {
2904:                 $ol->appendChild($li = $dom->createElement('LI'));
2905:                 $li->appendChild($dom->createElement('STRONG', $name));
2906:                 $li->appendChild($dom->createTextNode(' (' . $size . ') [' . htmlspecialchars($apart->getType()) . ']'));
2907:                 $li->appendChild($dom->createElement('BR'));
2908:                 $li->appendChild($dom->createTextNode(_("Download link") . ': '));
2909:                 $li->appendChild($a = $dom->createElement('A', htmlspecialchars($url)));
2910:                 $a->setAttribute('href', $url);
2911:             }
2912:         }
2913:     }
2914: 
2915:     /**
2916:      * Regenerates body text for use in the compose screen from IMAP data.
2917:      *
2918:      * @param IMP_Contents $contents  An IMP_Contents object.
2919:      * @param array $options          Additional options:
2920:      * <ul>
2921:      *  <li>html: (boolean) Return text/html part, if available.</li>
2922:      *  <li>imp_msg: (integer) If non-empty, the message data was created by
2923:      *               IMP. Either:
2924:      *   <ul>
2925:      *    <li>self::COMPOSE</li>
2926:      *    <li>self::FORWARD</li>
2927:      *    <li>self::REPLY</li>
2928:      *   </ul>
2929:      *  </li>
2930:      *  <li>replylimit: (boolean) Enforce length limits?</li>
2931:      *  <li>toflowed: (boolean) Do flowed conversion?</li>
2932:      * </ul>
2933:      *
2934:      * @return mixed  Null if bodypart not found, or array with the following
2935:      *                keys:
2936:      *   - charset: (string) The guessed charset to use.
2937:      *   - flowed: (Horde_Text_Flowed) A flowed object, if the text is flowed.
2938:      *             Otherwise, null.
2939:      *   - id: (string) The MIME ID of the bodypart.
2940:      *   - mode: (string) Either 'text' or 'html'.
2941:      *   - text: (string) The body text.
2942:      */
2943:     protected function _getMessageText($contents, array $options = array())
2944:     {
2945:         global $conf, $injector, $notification, $prefs, $session;
2946: 
2947:         $body_id = null;
2948:         $mode = 'text';
2949:         $options = array_merge(array(
2950:             'imp_msg' => self::COMPOSE
2951:         ), $options);
2952: 
2953:         if (!empty($options['html']) &&
2954:             $session->get('imp', 'rteavail') &&
2955:             (($body_id = $contents->findBody('html')) !== null)) {
2956:             $mime_message = $contents->getMIMEMessage();
2957: 
2958:             switch ($mime_message->getPrimaryType()) {
2959:             case 'multipart':
2960:                 if (($body_id != '1') &&
2961:                     ($mime_message->getSubType() == 'mixed') &&
2962:                     ($id_ob = new Horde_Mime_Id('1')) &&
2963:                     !$id_ob->isChild($body_id)) {
2964:                     $body_id = null;
2965:                 } else {
2966:                     $mode = 'html';
2967:                 }
2968:                 break;
2969: 
2970:             default:
2971:                 if (strval($body_id) != '1') {
2972:                     $body_id = null;
2973:                 } else {
2974:                     $mode = 'html';
2975:                 }
2976:                 break;
2977:             }
2978:         }
2979: 
2980:         if (is_null($body_id)) {
2981:             $body_id = $contents->findBody();
2982:             if (is_null($body_id)) {
2983:                 return null;
2984:             }
2985:         }
2986: 
2987:         $part = $contents->getMIMEPart($body_id);
2988:         $type = $part->getType();
2989:         $part_charset = $part->getCharset();
2990: 
2991:         $msg = Horde_String::convertCharset($part->getContents(), $part_charset, 'UTF-8');
2992: 
2993:         /* Enforce reply limits. */
2994:         if (!empty($options['replylimit']) &&
2995:             !empty($conf['compose']['reply_limit'])) {
2996:             $limit = $conf['compose']['reply_limit'];
2997:             if (Horde_String::length($msg) > $limit) {
2998:                 $msg = Horde_String::substr($msg, 0, $limit) . "\n" . _("[Truncated Text]");
2999:             }
3000:         }
3001: 
3002:         if ($mode == 'html') {
3003:             $dom = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter(
3004:                 $msg,
3005:                 'Xss',
3006:                 array(
3007:                     'charset' => $this->charset,
3008:                     'return_dom' => true,
3009:                     'strip_style_attributes' => false
3010:                 )
3011:             );
3012: 
3013:             /* If we are replying to a related part, and this part refers
3014:              * to local message parts, we need to move those parts into this
3015:              * message (since the original message may disappear during the
3016:              * compose process). */
3017:             if ($related_part = $contents->findMimeType($body_id, 'multipart/related')) {
3018:                 $this->_setMetadata('related_contents', $contents);
3019:                 $related_ob = new Horde_Mime_Related($related_part);
3020:                 $related_ob->cidReplace($dom, array($this, '_getMessageTextCallback'), $part_charset);
3021:                 $this->_setMetadata('related_contents', null);
3022:             }
3023: 
3024:             /* Convert any Data URLs to attachments. */
3025:             $xpath = new DOMXPath($dom->dom);
3026:             foreach ($xpath->query('//*[@src]') as $val) {
3027:                 $data_url = new Horde_Url_Data($val->getAttribute('src'));
3028:                 if (strlen($data_url->data)) {
3029:                     $data_part = new Horde_Mime_Part();
3030:                     $data_part->setContents($data_url->data);
3031:                     $data_part->setType($data_url->type);
3032: 
3033:                     try {
3034:                         $atc = $this->addAttachmentFromPart($data_part);
3035:                         $val->setAttribute('src', $atc->viewUrl());
3036:                         $this->addRelatedAttachment($atc, $val, 'src');
3037:                     } catch (IMP_Compose_Exception $e) {
3038:                         $notification->push($e, 'horde.warning');
3039:                     }
3040:                 }
3041:             }
3042: 
3043:             $msg = $dom->returnBody();
3044:         } elseif ($type == 'text/html') {
3045:             $msg = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Html2text');
3046:             $type = 'text/plain';
3047:         }
3048: 
3049:         /* Always remove leading/trailing whitespace. The data in the
3050:          * message body is not intended to be the exact representation of the
3051:          * original message (use forward as message/rfc822 part for that). */
3052:         $msg = trim($msg);
3053: 
3054:         if ($type == 'text/plain') {
3055:             if ($prefs->getValue('reply_strip_sig') &&
3056:                 (($pos = strrpos($msg, "\n-- ")) !== false)) {
3057:                 $msg = rtrim(substr($msg, 0, $pos));
3058:             }
3059: 
3060:             /* Remove PGP armored text. */
3061:             $pgp = $injector->getInstance('Horde_Crypt_Pgp_Parse')->parseToPart($msg);
3062:             if (!is_null($pgp)) {
3063:                 $msg = '';
3064:                 $pgp->buildMimeIds();
3065:                 foreach ($pgp->contentTypeMap() as $key => $val) {
3066:                     if (strpos($val, 'text/') === 0) {
3067:                         $msg .= $pgp[$key]->getContents();
3068:                     }
3069:                 }
3070:             }
3071: 
3072:             if ($part->getContentTypeParameter('format') == 'flowed') {
3073:                 $flowed = new Horde_Text_Flowed($msg, 'UTF-8');
3074:                 if (Horde_String::lower($part->getContentTypeParameter('delsp')) == 'yes') {
3075:                     $flowed->setDelSp(true);
3076:                 }
3077:                 $flowed->setMaxLength(0);
3078:                 $msg = $flowed->toFixed(false);
3079:             } else {
3080:                 /* If the input is *not* in flowed format, make sure there is
3081:                  * no padding at the end of lines. */
3082:                 $msg = preg_replace("/\s*\n/U", "\n", $msg);
3083:             }
3084: 
3085:             if (isset($options['toflowed'])) {
3086:                 $flowed = new Horde_Text_Flowed($msg, 'UTF-8');
3087:                 $msg = $options['toflowed']
3088:                     ? $flowed->toFlowed(true)
3089:                     : $flowed->toFlowed(false, array('nowrap' => true));
3090:             }
3091:         }
3092: 
3093:         if (strcasecmp($part->getCharset(), 'windows-1252') === 0) {
3094:             $part_charset = 'ISO-8859-1';
3095:         }
3096: 
3097:         return array(
3098:             'charset' => $part_charset,
3099:             'flowed' => isset($flowed) ? $flowed : null,
3100:             'id' => $body_id,
3101:             'mode' => $mode,
3102:             'text' => $msg
3103:         );
3104:     }
3105: 
3106:     /**
3107:      * Callback used in _getMessageText().
3108:      *
3109:      * @return Horde_Url
3110:      */
3111:     public function _getMessageTextCallback($id, $attribute, $node)
3112:     {
3113:         $atc = $this->addAttachmentFromPart($this->getMetadata('related_contents')->getMIMEPart($id));
3114:         $this->addRelatedAttachment($atc, $node, $attribute);
3115: 
3116:         return $atc->viewUrl();
3117:     }
3118: 
3119:     /**
3120:      * Adds an attachment from Horde_Mime_Part data.
3121:      *
3122:      * @param Horde_Mime_Part $part  The object that contains the attachment
3123:      *                               data.
3124:      *
3125:      * @return IMP_Compose_Attachment  Attachment object.
3126:      * @throws IMP_Compose_Exception
3127:      */
3128:     public function addAttachmentFromPart($part)
3129:     {
3130:         /* Extract the data from the Horde_Mime_Part. */
3131:         $atc_file = Horde::getTempFile('impatt');
3132:         $stream = $part->getContents(array(
3133:             'stream' => true
3134:         ));
3135:         rewind($stream);
3136: 
3137:         if (file_put_contents($atc_file, $stream) === false) {
3138:             throw new IMP_Compose_Exception(sprintf(_("Could not attach %s to the message."), $part->getName()));
3139:         }
3140: 
3141:         return $this->_addAttachment(
3142:             $atc_file,
3143:             ftell($stream),
3144:             $part->getName(true),
3145:             $part->getType()
3146:         );
3147:     }
3148: 
3149:     /**
3150:      * Add attachment from uploaded (form) data.
3151:      *
3152:      * @param string $field  The form field name.
3153:      *
3154:      * @return array  A list of IMP_Compose_Attachment objects (if
3155:      *                successfully attached) or IMP_Compose_Exception objects
3156:      *                (if error when attaching).
3157:      * @throws IMP_Compose_Exception
3158:      */
3159:     public function addAttachmentFromUpload($field)
3160:     {
3161:         global $browser;
3162: 
3163:         try {
3164:             $browser->wasFileUploaded($field, _("attachment"));
3165:         } catch (Horde_Browser_Exception $e) {
3166:             throw new IMP_Compose_Exception($e);
3167:         }
3168: 
3169:         $finfo = array();
3170:         if (is_array($_FILES[$field]['size'])) {
3171:             for ($i = 0; $i < count($_FILES[$field]['size']); ++$i) {
3172:                 $tmp = array();
3173:                 foreach ($_FILES[$field] as $key => $val) {
3174:                     $tmp[$key] = $val[$i];
3175:                 }
3176:                 $finfo[] = $tmp;
3177:             }
3178:         } else {
3179:             $finfo[] = $_FILES[$field];
3180:         }
3181: 
3182:         $out = array();
3183: 
3184:         foreach ($finfo as $val) {
3185:             switch (empty($val['type']) ? $val['type'] : '') {
3186:             case 'application/unknown':
3187:             case '':
3188:                 $type = 'application/octet-stream';
3189:                 break;
3190: 
3191:             default:
3192:                 $type = $val['type'];
3193:                 break;
3194:             }
3195: 
3196:             try {
3197:                 $out[] = $this->_addAttachment(
3198:                     $val['tmp_name'],
3199:                     $val['size'],
3200:                     Horde_Util::dispelMagicQuotes($val['name']),
3201:                     $type
3202:                 );
3203:             } catch (IMP_Compose_Exception $e) {
3204:                 $out[] = $e;
3205:             }
3206:         }
3207: 
3208:         return $out;
3209:     }
3210: 
3211:     /**
3212:      * Adds an attachment to the outgoing compose message.
3213:      *
3214:      * @param string $atc_file  Temporary file containing attachment contents.
3215:      * @param integer $bytes    Size of data, in bytes.
3216:      * @param string $filename  Filename of data.
3217:      * @param string $type      MIME type of data.
3218:      *
3219:      * @return IMP_Compose_Attachment  Attachment object.
3220:      * @throws IMP_Compose_Exception
3221:      */
3222:     protected function _addAttachment($atc_file, $bytes, $filename, $type)
3223:     {
3224:         global $conf, $injector;
3225: 
3226:         $atc = new Horde_Mime_Part();
3227:         $atc->setBytes($bytes);
3228: 
3229:         /* Try to determine the MIME type from 1) the extension and
3230:          * then 2) analysis of the file (if available). */
3231:         if (strlen($filename)) {
3232:             $atc->setName($filename);
3233:             if ($type == 'application/octet-stream') {
3234:                 $type = Horde_Mime_Magic::filenameToMIME($filename, false);
3235:             }
3236:         }
3237: 
3238:         $atc->setType($type);
3239: 
3240:         if (($atc->getType() == 'application/octet-stream') ||
3241:             ($atc->getPrimaryType() == 'text')) {
3242:             $analyze = Horde_Mime_Magic::analyzeFile($atc_file, empty($conf['mime']['magic_db']) ? null : $conf['mime']['magic_db'], array(
3243:                 'nostrip' => true
3244:             ));
3245: 
3246:             if ($analyze) {
3247:                 $ctype = new Horde_Mime_ContentParam($analyze);
3248:                 $atc->setType($ctype->value);
3249:                 $atc->setCharset(isset($ctype->params['charset']) ? $ctype->params['charset'] : 'UTF-8');
3250:             } else {
3251:                 $atc->setCharset('UTF-8');
3252:             }
3253:         } else {
3254:             $atc->setHeaderCharset('UTF-8');
3255:         }
3256: 
3257:         $atc_ob = new IMP_Compose_Attachment($this, $atc, $atc_file);
3258: 
3259:         /* Check for attachment size limitations. */
3260:         $size_limit = null;
3261:         if ($atc_ob->linked) {
3262:             if (!empty($conf['compose']['link_attach_size_limit'])) {
3263:                 $linked = true;
3264:                 $size_limit = 'link_attach_size_limit';
3265:             }
3266:         } elseif (!empty($conf['compose']['attach_size_limit'])) {
3267:             $linked = false;
3268:             $size_limit = 'attach_size_limit';
3269:         }
3270: 
3271:         if (!is_null($size_limit)) {
3272:             $total_size = $conf['compose'][$size_limit] - $bytes;
3273:             foreach ($this as $val) {
3274:                 if ($val->linked == $linked) {
3275:                     $total_size -= $val->getPart()->getBytes();
3276:                 }
3277:             }
3278: 
3279:             if ($total_size < 0) {
3280:                 throw new IMP_Compose_Exception(strlen($filename) ? sprintf(_("Attached file \"%s\" exceeds the attachment size limits. File NOT attached."), $filename) : _("Attached file exceeds the attachment size limits. File NOT attached."));
3281:             }
3282:         }
3283: 
3284:         try {
3285:             $injector->getInstance('Horde_Core_Hooks')->callHook(
3286:                 'compose_attachment',
3287:                 'imp',
3288:                 array($atc_ob)
3289:             );
3290:         } catch (Horde_Exception_HookNotSet $e) {}
3291: 
3292:         $this->_atc[$atc_ob->id] = $atc_ob;
3293:         $this->changed = 'changed';
3294: 
3295:         return $atc_ob;
3296:     }
3297: 
3298:     /**
3299:      * Store draft compose data if session expires.
3300:      *
3301:      * @param Horde_Variables $vars  Object with the form data.
3302:      */
3303:     public function sessionExpireDraft(Horde_Variables $vars)
3304:     {
3305:         global $injector;
3306: 
3307:         if (!isset($vars->composeCache) ||
3308:             !isset($vars->composeHmac) ||
3309:             !isset($vars->user) ||
3310:             ($this->getHmac($vars->composeCache, $vars->user) != $vars->composeHmac)) {
3311:             return;
3312:         }
3313: 
3314:         $headers = array();
3315:         foreach (array('to', 'cc', 'bcc', 'subject') as $val) {
3316:             $headers[$val] = $vars->$val;
3317:         }
3318: 
3319:         try {
3320:             $body = $this->_saveDraftMsg($headers, $vars->message, array(
3321:                 'html' => $vars->rtemode,
3322:                 'priority' => $vars->priority,
3323:                 'readreceipt' => $vars->request_read_receipt
3324:             ));
3325: 
3326:             $injector->getInstance('Horde_Core_Factory_Vfs')->create()->writeData(self::VFS_DRAFTS_PATH, hash('sha1', $vars->user), $body, true);
3327:         } catch (Exception $e) {}
3328:     }
3329: 
3330:     /**
3331:      * Restore session expiration draft compose data.
3332:      */
3333:     public function recoverSessionExpireDraft()
3334:     {
3335:         global $injector, $notification;
3336: 
3337:         $filename = hash('sha1', $GLOBALS['registry']->getAuth());
3338: 
3339:         try {
3340:             $vfs = $injector->getInstance('Horde_Core_Factory_Vfs')->create();
3341: 
3342:             if ($vfs->exists(self::VFS_DRAFTS_PATH, $filename)) {
3343:                 $data = $vfs->read(self::VFS_DRAFTS_PATH, $filename);
3344:                 $this->_saveDraftServer($data);
3345:                 $vfs->deleteFile(self::VFS_DRAFTS_PATH, $filename);
3346:                 $notification->push(
3347:                     _("A message you were composing when your session expired has been recovered. You may resume composing your message by going to your Drafts mailbox."),
3348:                     'horde.message',
3349:                     array('sticky')
3350:                 );
3351:             }
3352:         } catch (Exception $e) {}
3353:     }
3354: 
3355:     /**
3356:      * If this object contains sufficient metadata, return an IMP_Contents
3357:      * object reflecting that metadata.
3358:      *
3359:      * @return mixed  Either an IMP_Contents object or null.
3360:      */
3361:     public function getContentsOb()
3362:     {
3363:         return ($this->_replytype && ($indices = $this->getMetadata('indices')) && (count($indices) === 1))
3364:             ? $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create($indices)
3365:             : null;
3366:     }
3367: 
3368:     /**
3369:      * Return the reply type.
3370:      *
3371:      * @param boolean $base  Return the base reply type?
3372:      *
3373:      * @return string  The reply type, or null if not a reply.
3374:      */
3375:     public function replyType($base = false)
3376:     {
3377:         switch ($this->_replytype) {
3378:         case self::FORWARD:
3379:         case self::FORWARD_ATTACH:
3380:         case self::FORWARD_BODY:
3381:         case self::FORWARD_BOTH:
3382:             return $base
3383:                 ? self::FORWARD
3384:                 : $this->_replytype;
3385: 
3386:         case self::REPLY:
3387:         case self::REPLY_ALL:
3388:         case self::REPLY_LIST:
3389:         case self::REPLY_SENDER:
3390:             return $base
3391:                 ? self::REPLY
3392:                 : $this->_replytype;
3393: 
3394:         case self::REDIRECT:
3395:             return $this->_replytype;
3396: 
3397:         default:
3398:             return null;
3399:         }
3400:     }
3401: 
3402:     /* Static methods. */
3403: 
3404:     /**
3405:      * Is composing messages allowed?
3406:      *
3407:      * @return boolean  True if compose allowed.
3408:      * @throws Horde_Exception
3409:      */
3410:     public static function canCompose()
3411:     {
3412:         try {
3413:             return !$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook('disable_compose', 'imp');
3414:         } catch (Horde_Exception_HookNotSet $e) {
3415:             return true;
3416:         }
3417:     }
3418: 
3419:     /**
3420:      * Can attachments be uploaded?
3421:      *
3422:      * @return boolean  True if attachments can be uploaded.
3423:      */
3424:     public static function canUploadAttachment()
3425:     {
3426:         return ($GLOBALS['session']->get('imp', 'file_upload') != 0);
3427:     }
3428: 
3429:     /**
3430:      * Shortcut function to convert text -> HTML for purposes of composition.
3431:      *
3432:      * @param string $msg  The message text.
3433:      *
3434:      * @return string  HTML text.
3435:      */
3436:     public static function text2html($msg)
3437:     {
3438:         return $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Text2html', array(
3439:             'always_mailto' => true,
3440:             'flowed' => self::HTML_BLOCKQUOTE,
3441:             'parselevel' => Horde_Text_Filter_Text2html::MICRO
3442:         ));
3443:     }
3444: 
3445:     /* ArrayAccess methods. */
3446: 
3447:     public function offsetExists($offset)
3448:     {
3449:         return isset($this->_atc[$offset]);
3450:     }
3451: 
3452:     public function offsetGet($offset)
3453:     {
3454:         return isset($this->_atc[$offset])
3455:             ? $this->_atc[$offset]
3456:             : null;
3457:     }
3458: 
3459:     public function offsetSet($offset, $value)
3460:     {
3461:         $this->_atc[$offset] = $value;
3462:         $this->changed = 'changed';
3463:     }
3464: 
3465:     public function offsetUnset($offset)
3466:     {
3467:         if (($atc = $this->_atc[$offset]) === null) {
3468:             return;
3469:         }
3470: 
3471:         $atc->delete();
3472:         unset($this->_atc[$offset]);
3473: 
3474:         $this->changed = 'changed';
3475:     }
3476: 
3477:     /* Magic methods. */
3478: 
3479:     /**
3480:      * String representation: the cache ID.
3481:      */
3482:     public function __toString()
3483:     {
3484:         return $this->getCacheId();
3485:     }
3486: 
3487:     /* Countable method. */
3488: 
3489:     /**
3490:      * Returns the number of attachments currently in this message.
3491:      *
3492:      * @return integer  The number of attachments in this message.
3493:      */
3494:     public function count()
3495:     {
3496:         return count($this->_atc);
3497:     }
3498: 
3499:     /* IteratorAggregate method. */
3500: 
3501:     /**
3502:      */
3503:     public function getIterator()
3504:     {
3505:         return new ArrayIterator($this->_atc);
3506:     }
3507: 
3508: }
3509: 
API documentation generated by ApiGen