Overview

Packages

  • IMP
  • None

Classes

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