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