⚝
One Hat Cyber Team
⚝
Your IP:
216.73.216.97
Server IP:
41.128.143.86
Server:
Linux host.raqmix.cloud 6.8.0-1025-azure #30~22.04.1-Ubuntu SMP Wed Mar 12 15:28:20 UTC 2025 x86_64
Server Software:
Apache
PHP Version:
8.3.23
Buat File
|
Buat Folder
Eksekusi
Dir :
~
/
usr
/
share
/
psa-horde
/
imp
/
lib
/
Edit File: Compose.php
<?php /** * Copyright 2002-2017 Horde LLC (http://www.horde.org/) * * See the enclosed file COPYING for license information (GPL). If you * did not receive this file, see http://www.horde.org/licenses/gpl. * * @category Horde * @copyright 2002-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP */ /** * The IMP_Compose:: class represents an outgoing mail message. * * @author Michael Slusarz <slusarz@horde.org> * @category Horde * @copyright 2002-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP */ class IMP_Compose implements ArrayAccess, Countable, IteratorAggregate { /* The virtual path to save drafts. */ const VFS_DRAFTS_PATH = '.horde/imp/drafts'; /* Compose types. */ const COMPOSE = 0; const REPLY = 1; const REPLY_ALL = 2; const REPLY_AUTO = 3; const REPLY_LIST = 4; const REPLY_SENDER = 5; const FORWARD = 6; const FORWARD_ATTACH = 7; const FORWARD_AUTO = 8; const FORWARD_BODY = 9; const FORWARD_BOTH = 10; const REDIRECT = 11; const EDITASNEW = 12; const TEMPLATE = 13; /* Related part attribute name. */ const RELATED_ATTR = 'imp_related_attr'; /* The blockquote tag to use to indicate quoted text in HTML data. */ const HTML_BLOCKQUOTE = '<blockquote type="cite" style="border-left:2px solid blue;margin-left:2px;padding-left:12px;">'; /** * Attachment ID counter. * * @var integer */ public $atcId = 0; /** * Mark as changed for purposes of storing in the session. * Either empty, 'changed', or 'deleted'. * * @var string */ public $changed = ''; /** * The charset to use for sending. * * @var string */ public $charset; /** * Attachment data. * * @var array */ protected $_atc = array(); /** * The cache ID used to store object in session. * * @var string */ protected $_cacheid; /** * Various metadata for this message. * * @var array */ protected $_metadata = array(); /** * The reply type. * * @var integer */ protected $_replytype = self::COMPOSE; /** * Constructor. * * @param string $cacheid The cache ID string. */ public function __construct($cacheid) { $this->_cacheid = $cacheid; $this->charset = $GLOBALS['registry']->getEmailCharset(); } /** * Tasks to do upon unserialize(). */ public function __wakeup() { $this->changed = ''; } /** * Destroys an IMP_Compose instance. * * @param string $action The action performed to cause the end of this * instance. Either 'cancel', 'discard', * 'save_draft', or 'send'. */ public function destroy($action) { switch ($action) { case 'discard': case 'send': /* Delete the draft. */ $GLOBALS['injector']->getInstance('IMP_Message')->delete( new IMP_Indices($this->getMetadata('draft_uid')), array('nuke' => true) ); break; case 'save_draft': /* Don't delete any drafts. */ $this->changed = 'deleted'; return; case 'cancel': if ($this->getMetadata('draft_auto')) { $this->destroy('discard'); return; } // Fall-through default: // No-op break; } $this->deleteAllAttachments(); $this->changed = 'deleted'; } /** * Gets metadata about the current object. * * @param string $name The metadata name. * * @return mixed The metadata value or null if it doesn't exist. */ public function getMetadata($name) { return isset($this->_metadata[$name]) ? $this->_metadata[$name] : null; } /** * Sets metadata for the current object. * * @param string $name The metadata name. * @param mixed $value The metadata value. */ protected function _setMetadata($name, $value) { if (is_null($value)) { unset($this->_metadata[$name]); } else { $this->_metadata[$name] = $value; } $this->changed = 'changed'; } /** * Saves a draft message. * * @param array $headers List of message headers (UTF-8). * @param mixed $message Either the message text (string) or a * Horde_Mime_Part object that contains the text * to send. * @param array $opts An array of options w/the following keys: * <pre> * - autosave: (boolean) Is this an auto-saved draft? * - html: (boolean) Is this an HTML message? * - priority: (string) The message priority ('high', 'normal', 'low'). * - readreceipt: (boolean) Add return receipt headers? * </pre> * * @return string Notification text on success (not HTML encoded). * * @throws IMP_Compose_Exception */ public function saveDraft($headers, $message, array $opts = array()) { $body = $this->_saveDraftMsg($headers, $message, $opts); $ret = $this->_saveDraftServer($body); $this->_setMetadata('draft_auto', !empty($opts['autosave'])); return $ret; } /** * Prepare the draft message. * * @param array $headers List of message headers. * @param mixed $message Either the message text (string) or a * Horde_Mime_Part object that contains the text * to send. * @param array $opts An array of options w/the following keys: * - html: (boolean) Is this an HTML message? * - priority: (string) The message priority ('high', 'normal', 'low'). * - readreceipt: (boolean) Add return receipt headers? * - verify_email: (boolean) Verify e-mail messages? Default: no. * * @return string The body text. * * @throws IMP_Compose_Exception */ protected function _saveDraftMsg($headers, $message, $opts) { $has_session = (bool)$GLOBALS['registry']->getAuth(); /* Set up the base message now. */ $base = $this->_createMimeMessage(new Horde_Mail_Rfc822_List(), $message, array( 'html' => !empty($opts['html']), 'noattach' => !$has_session, 'nofinal' => true )); $base->isBasePart(true); $recip_list = $this->recipientList($headers); if (!empty($opts['verify_email'])) { foreach ($recip_list['list'] as $val) { try { IMP::parseAddressList($val->writeAddress(true), array( 'validate' => true )); } catch (Horde_Mail_Exception $e) { throw new IMP_Compose_Exception(sprintf( _("Saving the message failed because it contains an invalid e-mail address: %s."), strval($val), $e->getMessage() ), $e->getCode()); } } } $headers = array_merge($headers, $recip_list['header']); /* Initalize a header object for the draft. */ $draft_headers = $this->_prepareHeaders($headers, array_merge($opts, array('bcc' => true))); /* Add information necessary to log replies/forwards when finally * sent. */ $imp_imap = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create(); if ($this->_replytype) { try { $indices = $this->getMetadata('indices'); $imap_url = new Horde_Imap_Client_Url(); $imap_url->hostspec = $imp_imap->getParam('hostspec'); $imap_url->protocol = $imp_imap->isImap() ? 'imap' : 'pop'; $imap_url->username = $imp_imap->getParam('username'); $urls = array(); foreach ($indices as $val) { $imap_url->mailbox = $val->mbox; $imap_url->uidvalidity = $val->mbox->uidvalid; foreach ($val->uids as $val2) { $imap_url->uid = $val2; $urls[] = '<' . strval($imap_url) . '>'; } } switch ($this->replyType(true)) { case self::FORWARD: $draft_headers->addHeader('X-IMP-Draft-Forward', implode(', ', $urls)); break; case self::REPLY: $draft_headers->addHeader('X-IMP-Draft-Reply', implode(', ', $urls)); $draft_headers->addHeader('X-IMP-Draft-Reply-Type', $this->_replytype); break; } } catch (Horde_Exception $e) {} } else { $draft_headers->addHeader('X-IMP-Draft', 'Yes'); } return $base->toString(array( 'defserver' => $has_session ? $imp_imap->config->maildomain : null, 'headers' => $draft_headers )); } /** * Save a draft message on the IMAP server. * * @param string $data The text of the draft message. * * @return string Status string (not HTML escaped). * * @throws IMP_Compose_Exception */ protected function _saveDraftServer($data) { if (!$drafts_mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_DRAFTS)) { throw new IMP_Compose_Exception(_("Saving the draft failed. No drafts mailbox specified.")); } /* Check for access to drafts mailbox. */ if (!$drafts_mbox->create()) { throw new IMP_Compose_Exception(_("Saving the draft failed. Could not create a drafts mailbox.")); } $append_flags = array( Horde_Imap_Client::FLAG_DRAFT, /* RFC 3503 [3.4] - MUST set MDNSent flag on draft message. */ Horde_Imap_Client::FLAG_MDNSENT ); if (!$GLOBALS['prefs']->getValue('unseen_drafts')) { $append_flags[] = Horde_Imap_Client::FLAG_SEEN; } $old_uid = $this->getMetadata('draft_uid'); /* Add the message to the mailbox. */ try { $ids = $drafts_mbox->imp_imap->append($drafts_mbox, array(array('data' => $data, 'flags' => $append_flags))); if ($old_uid) { $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true)); } $this->_setMetadata('draft_uid', $drafts_mbox->getIndicesOb($ids)); return sprintf(_("The draft has been saved to the \"%s\" mailbox."), $drafts_mbox->display); } catch (IMP_Imap_Exception $e) { return _("The draft was not successfully saved."); } } /** * Edits a message as new. * * @see resumeDraft(). * * @param IMP_Indices $indices An indices object. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return mixed See resumeDraft(). * * @throws IMP_Compose_Exception */ public function editAsNew($indices, array $opts = array()) { $ret = $this->_resumeDraft($indices, self::EDITASNEW, $opts); $ret['type'] = self::EDITASNEW; return $ret; } /** * Edit an existing template message. Saving this template later * (using saveTemplate()) will cause the original message to be deleted. * * @param IMP_Indices $indices An indices object. * * @return mixed See resumeDraft(). * * @throws IMP_Compose_Exception */ public function editTemplate($indices) { $res = $this->useTemplate($indices); $this->_setMetadata('template_uid_edit', $indices); return $res; } /** * Resumes a previously saved draft message. * * @param IMP_Indices $indices An indices object. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return mixed An array with the following keys: * - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List * objects). * - body: (string) The text of the body part. * - format: (string) The format of the body message ('html', 'text'). * - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity(). * - priority: (string) The message priority. * - readreceipt: (boolean) Add return receipt headers? * - subject: (string) Formatted subject. * - type: (integer) - The compose type. * * @throws IMP_Compose_Exception */ public function resumeDraft($indices, array $opts = array()) { $res = $this->_resumeDraft($indices, null, $opts); $this->_setMetadata('draft_uid', $indices); return $res; } /** * Uses a template to create a message. * * @see resumeDraft(). * * @param IMP_Indices $indices An indices object. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return mixed See resumeDraft(). * * @throws IMP_Compose_Exception */ public function useTemplate($indices, array $opts = array()) { $ret = $this->_resumeDraft($indices, self::TEMPLATE, $opts); $ret['type'] = self::TEMPLATE; return $ret; } /** * Resumes a previously saved draft message. * * @param IMP_Indices $indices See resumeDraft(). * @param integer $type Compose type. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return mixed See resumeDraft(). * * @throws IMP_Compose_Exception */ protected function _resumeDraft($indices, $type, $opts) { global $injector, $notification, $prefs; $contents_factory = $injector->getInstance('IMP_Factory_Contents'); try { $contents = $contents_factory->create($indices); } catch (IMP_Exception $e) { throw new IMP_Compose_Exception($e); } $headers = $contents->getHeader(); $imp_draft = false; if ($draft_url = $headers->getValue('x-imp-draft-reply')) { if (is_null($type) && !($type = $headers->getValue('x-imp-draft-reply-type'))) { $type = self::REPLY; } $imp_draft = self::REPLY; } elseif ($draft_url = $headers->getValue('x-imp-draft-forward')) { $imp_draft = self::FORWARD; if (is_null($type)) { $type = self::FORWARD; } } elseif ($headers->getValue('x-imp-draft')) { $imp_draft = self::COMPOSE; } if (!empty($opts['format'])) { $compose_html = ($opts['format'] == 'html'); } elseif ($prefs->getValue('compose_html')) { $compose_html = true; } else { switch ($type) { case self::EDITASNEW: case self::FORWARD: case self::FORWARD_BODY: case self::FORWARD_BOTH: $compose_html = $prefs->getValue('forward_format'); break; case self::REPLY: case self::REPLY_ALL: case self::REPLY_LIST: case self::REPLY_SENDER: $compose_html = $prefs->getValue('reply_format'); break; case self::TEMPLATE: $compose_html = true; break; default: /* If this is an draft saved by IMP, we know 100% for sure * that if an HTML part exists, the user was composing in * HTML. */ $compose_html = ($imp_draft !== false); break; } } $msg_text = $this->_getMessageText($contents, array( 'html' => $compose_html, 'imp_msg' => $imp_draft, 'toflowed' => false )); if (empty($msg_text)) { $body = ''; $format = 'text'; $text_id = 0; } else { /* Use charset at time of initial composition if this is an IMP * draft. */ if ($imp_draft !== false) { $this->charset = $msg_text['charset']; } $body = $msg_text['text']; $format = $msg_text['mode']; $text_id = $msg_text['id']; } $mime_message = $contents->getMIMEMessage(); /* Add attachments. */ $parts = array(); if (($mime_message->getPrimaryType() == 'multipart') && ($mime_message->getType() != 'multipart/alternative')) { for ($i = 1; ; ++$i) { if (intval($text_id) == $i) { continue; } if ($part = $contents->getMIMEPart($i)) { $parts[] = $part; } else { break; } } } elseif ($mime_message->getDisposition() == 'attachment') { $parts[] = $contents->getMimePart('1'); } foreach ($parts as $val) { try { $this->addAttachmentFromPart($val); } catch (IMP_Compose_Exception $e) { $notification->push($e, 'horde.warning'); } } $alist = new Horde_Mail_Rfc822_List(); $addr = array( 'to' => clone $alist, 'cc' => clone $alist, 'bcc' => clone $alist ); if ($type != self::EDITASNEW) { foreach (array('to', 'cc', 'bcc') as $val) { if ($tmp = $headers->getOb($val)) { $addr[$val] = $tmp; } } if ($val = $headers->getValue('references')) { $ref_ob = new Horde_Mail_Rfc822_Identification($val); $this->_setMetadata('references', $ref_ob->ids); if ($val = $headers->getValue('in-reply-to')) { $this->_setMetadata('in_reply_to', $val); } } if ($draft_url) { $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create(); $indices = new IMP_Indices(); foreach (explode(',', $draft_url) as $val) { $imap_url = new Horde_Imap_Client_Url(rtrim(ltrim($val, '<'), '>')); try { if (($imap_url->protocol == ($imp_imap->isImap() ? 'imap' : 'pop')) && ($imap_url->username == $imp_imap->getParam('username')) && // Ignore hostspec and port, since these can change // even though the server is the same. UIDVALIDITY // should catch any true server/backend changes. (IMP_Mailbox::get($imap_url->mailbox)->uidvalid == $imap_url->uidvalidity) && $contents_factory->create(new IMP_Indices($imap_url->mailbox, $imap_url->uid))) { $indices->add($imap_url->mailbox, $imap_url->uid); } } catch (Exception $e) {} } if (count($indices)) { $this->_setMetadata('indices', $indices); $this->_replytype = $type; } } } $mdn = new Horde_Mime_Mdn($headers); $readreceipt = (bool)$mdn->getMdnReturnAddr(); $this->changed = 'changed'; return array( 'addr' => $addr, 'body' => $body, 'format' => $format, 'identity' => $this->_getMatchingIdentity($headers, array('from')), 'priority' => $injector->getInstance('IMP_Mime_Headers')->getPriority($headers), 'readreceipt' => $readreceipt, 'subject' => $headers->getValue('subject'), 'type' => $type ); } /** * Save a template message on the IMAP server. * * @param array $headers List of message headers (UTF-8). * @param mixed $message Either the message text (string) or a * Horde_Mime_Part object that contains the text * to save. * @param array $opts An array of options w/the following keys: * - html: (boolean) Is this an HTML message? * - priority: (string) The message priority ('high', 'normal', 'low'). * - readreceipt: (boolean) Add return receipt headers? * * @return string Notification text on success. * * @throws IMP_Compose_Exception */ public function saveTemplate($headers, $message, array $opts = array()) { if (!$mbox = IMP_Mailbox::getPref(IMP_Mailbox::MBOX_TEMPLATES)) { throw new IMP_Compose_Exception(_("Saving the template failed: no template mailbox exists.")); } /* Check for access to mailbox. */ if (!$mbox->create()) { throw new IMP_Compose_Exception(_("Saving the template failed: could not create the templates mailbox.")); } $append_flags = array( // Don't mark as draft, since other MUAs could potentially // delete it. Horde_Imap_Client::FLAG_SEEN ); $old_uid = $this->getMetadata('template_uid_edit'); /* Add the message to the mailbox. */ try { $mbox->imp_imap->append($mbox, array(array( 'data' => $this->_saveDraftMsg($headers, $message, $opts), 'flags' => $append_flags, 'verify_email' => true ))); if ($old_uid) { $GLOBALS['injector']->getInstance('IMP_Message')->delete($old_uid, array('nuke' => true)); } } catch (IMP_Imap_Exception $e) { return _("The template was not successfully saved."); } return _("The template has been saved."); } /** * Does this message have any drafts associated with it? * * @return boolean True if draft messages exist. */ public function hasDrafts() { return (bool)$this->getMetadata('draft_uid'); } /** * Builds and sends a MIME message. * * @param string $body The message body. * @param array $header List of message headers. * @param IMP_Prefs_Identity $identity The Identity object for the sender * of this message. * @param array $opts An array of options w/the * following keys: * - encrypt: (integer) A flag whether to encrypt or sign the message. * One of: * - IMP_Crypt_Pgp::ENCRYPT</li> * - IMP_Crypt_Pgp::SIGNENC</li> * - IMP_Crypt_Smime::ENCRYPT</li> * - IMP_Crypt_Smime::SIGNENC</li> * - html: (boolean) Whether this is an HTML message. * DEFAULT: false * - pgp_attach_pubkey: (boolean) Attach the user's PGP public key to the * message? * - priority: (string) The message priority ('high', 'normal', 'low'). * - save_sent: (boolean) Save sent mail? * - sent_mail: (IMP_Mailbox) The sent-mail mailbox (UTF-8). * - strip_attachments: (bool) Strip attachments from the message? * - signature: (string) The message signature. * - readreceipt: (boolean) Add return receipt headers? * - useragent: (string) The User-Agent string to use. * - vcard_attach: (string) Attach the user's vCard (value is name to * display as vcard filename). * * @throws Horde_Exception * @throws IMP_Compose_Exception * @throws IMP_Compose_Exception_Address * @throws IMP_Exception */ public function buildAndSendMessage( $body, $header, IMP_Prefs_Identity $identity, array $opts = array() ) { global $conf, $injector, $notification, $prefs, $registry, $session; /* We need at least one recipient & RFC 2822 requires that no 8-bit * characters can be in the address fields. */ $recip = $this->recipientList($header); if (!count($recip['list'])) { if ($recip['has_input']) { throw new IMP_Compose_Exception(_("Invalid e-mail address.")); } throw new IMP_Compose_Exception(_("Need at least one message recipient.")); } $header = array_merge($header, $recip['header']); /* Check for correct identity usage. */ if (!$this->getMetadata('identity_check') && (count($recip['list']) === 1)) { $identity_search = $identity->getMatchingIdentity($recip['list'], false); if (!is_null($identity_search) && ($identity->getDefault() != $identity_search)) { $this->_setMetadata('identity_check', true); $e = new IMP_Compose_Exception(_("Recipient address does not match the currently selected identity.")); $e->tied_identity = $identity_search; throw $e; } } /* Check body size of message. */ $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create(); if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_BODYSIZE, strlen($body))) { Horde::permissionDeniedError('imp', 'max_bodysize'); throw new IMP_Compose_Exception(sprintf( _("Your message body has exceeded the limit by body size by %d characters."), (strlen($body) - $imp_imap->max_compose_bodysize) )); } $from = new Horde_Mail_Rfc822_Address($header['from']); if (is_null($from->host)) { $from->host = $imp_imap->config->maildomain; } /* Prepare the array of messages to send out. May be more * than one if we are encrypting for multiple recipients or * are storing an encrypted message locally. */ $encrypt = empty($opts['encrypt']) ? 0 : $opts['encrypt']; $send_msgs = array(); $msg_options = array( 'encrypt' => $encrypt, 'html' => !empty($opts['html']), 'identity' => $identity, 'pgp_attach_pubkey' => (!empty($opts['pgp_attach_pubkey']) && $prefs->getValue('use_pgp') && $prefs->getValue('pgp_public_key')), 'signature' => is_null($opts['signature']) ? $identity : $opts['signature'], 'vcard_attach' => ((!empty($opts['vcard_attach']) && $registry->hasMethod('contacts/ownVCard')) ? ((strlen($opts['vcard_attach']) ? $opts['vcard_attach'] : 'vcard') . '.vcf') : null) ); /* Must encrypt & send the message one recipient at a time. */ if ($prefs->getValue('use_smime') && in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGNENC))) { foreach ($recip['list'] as $val) { $list_ob = new Horde_Mail_Rfc822_List($val); $send_msgs[] = array( 'base' => $this->_createMimeMessage($list_ob, $body, $msg_options), 'recipients' => $list_ob ); } /* Must target the encryption for the sender before saving message * in sent-mail. */ $save_msg = $this->_createMimeMessage(IMP::parseAddressList($header['from']), $body, $msg_options); } else { /* Can send in clear-text all at once, or PGP can encrypt * multiple addresses in the same message. */ $msg_options['from'] = $from; $save_msg = $this->_createMimeMessage($recip['list'], $body, $msg_options); $send_msgs[] = array( 'base' => $save_msg, 'recipients' => $recip['list'] ); } /* Initalize a header object for the outgoing message. */ $headers = $this->_prepareHeaders($header, $opts); /* Add a Received header for the hop from browser to server. */ $headers->addReceivedHeader(array( 'dns' => $injector->getInstance('Net_DNS2_Resolver'), 'server' => $conf['server']['name'] )); /* Add Reply-To header. */ if (!empty($header['replyto']) && ($header['replyto'] != $from->bare_address)) { $headers->addHeader('Reply-to', $header['replyto']); } /* Add the 'User-Agent' header. */ if (empty($opts['useragent'])) { $headers->setUserAgent('Internet Messaging Program (IMP) ' . $registry->getVersion()); } else { $headers->setUserAgent($opts['useragent']); } $headers->addUserAgentHeader(); /* Add preferred reply language(s). */ if ($lang = @unserialize($prefs->getValue('reply_lang'))) { $headers->addHeader('Accept-Language', implode(',', $lang)); } /* Send the messages out now. */ $sentmail = $injector->getInstance('IMP_Sentmail'); foreach ($send_msgs as $val) { switch (intval($this->replyType(true))) { case self::REPLY: $senttype = IMP_Sentmail::REPLY; break; case self::FORWARD: $senttype = IMP_Sentmail::FORWARD; break; case self::REDIRECT: $senttype = IMP_Sentmail::REDIRECT; break; default: $senttype = IMP_Sentmail::NEWMSG; break; } $headers_copy = clone $headers; try { $this->_prepSendMessageAssert($val['recipients'], $headers_copy, $val['base']); $this->sendMessage($val['recipients'], $headers_copy, $val['base']); /* Store history information. */ $msg_id = new Horde_Mail_Rfc822_Identification( $headers_copy->getValue('message-id') ); $sentmail->log( $senttype, reset($msg_id->ids), $val['recipients'], true ); } catch (IMP_Compose_Exception_Address $e) { throw $e; } catch (IMP_Compose_Exception $e) { /* Unsuccessful send. */ if ($e->log()) { $msg_id = new Horde_Mail_Rfc822_Identification( $headers_copy->getValue('message-id') ); $sentmail->log( $senttype, reset($msg_id->ids), $val['recipients'], false ); } throw new IMP_Compose_Exception(sprintf(_("There was an error sending your message: %s"), $e->getMessage())); } } $recipients = strval($recip['list']); if ($this->_replytype) { /* Log the reply. */ if ($indices = $this->getMetadata('indices')) { switch ($this->_replytype) { case self::FORWARD: case self::FORWARD_ATTACH: case self::FORWARD_BODY: case self::FORWARD_BOTH: $log = new IMP_Maillog_Log_Forward($recipients); break; case self::REPLY: case self::REPLY_SENDER: $log = new IMP_Maillog_Log_Reply(); break; case IMP_Compose::REPLY_ALL: $log = new IMP_Maillog_Log_Replyall(); break; case IMP_Compose::REPLY_LIST: $log = new IMP_Maillog_Log_Replylist(); break; } $log_msgs = array(); foreach ($indices as $val) { foreach ($val->uids as $val2) { $log_msgs[] = new IMP_Maillog_Message( new IMP_Indices($val->mbox, $val2) ); } } $injector->getInstance('IMP_Maillog')->log($log_msgs, $log); } $imp_message = $injector->getInstance('IMP_Message'); $reply_uid = new IMP_Indices($this); switch ($this->replyType(true)) { case self::FORWARD: /* Set the Forwarded flag, if possible, in the mailbox. * See RFC 5550 [5.9] */ $imp_message->flag(array( 'add' => array(Horde_Imap_Client::FLAG_FORWARDED) ), $reply_uid); break; case self::REPLY: /* Make sure to set the IMAP reply flag and unset any * 'flagged' flag. */ $imp_message->flag(array( 'add' => array(Horde_Imap_Client::FLAG_ANSWERED), 'remove' => array(Horde_Imap_Client::FLAG_FLAGGED) ), $reply_uid); break; } } Horde::log( sprintf( "Message sent to %s from %s (%s)", $recipients, $registry->getAuth(), $session->get('horde', 'auth/remoteAddr') ), 'INFO' ); /* Should we save this message in the sent mail mailbox? */ if (!empty($opts['sent_mail']) && ((!$prefs->isLocked('save_sent_mail') && !empty($opts['save_sent'])) || ($prefs->isLocked('save_sent_mail') && $prefs->getValue('save_sent_mail')))) { /* Keep Bcc: headers on saved messages. */ if ((is_array($header['bcc']) || $header['bcc'] instanceof Countable) && count($header['bcc'])) { $headers->addHeader('Bcc', $header['bcc']); } /* Strip attachments if requested. */ if (!empty($opts['strip_attachments'])) { $save_msg->buildMimeIds(); /* Don't strip any part if this is a text message with both * plaintext and HTML representation, or a signed or encrypted * message. */ if ($save_msg->getType() != 'multipart/alternative' && $save_msg->getType() != 'multipart/encrypted' && $save_msg->getType() != 'multipart/signed') { for ($i = 2; ; ++$i) { if (!($oldPart = $save_msg->getPart($i))) { break; } $replace_part = new Horde_Mime_Part(); $replace_part->setType('text/plain'); $replace_part->setCharset($this->charset); $replace_part->setLanguage($GLOBALS['language']); $replace_part->setContents('[' . _("Attachment stripped: Original attachment type") . ': "' . $oldPart->getType() . '", ' . _("name") . ': "' . $oldPart->getName(true) . '"]'); $save_msg->alterPart($i, $replace_part); } } } /* Generate the message string. */ $fcc = $save_msg->toString(array( 'defserver' => $imp_imap->config->maildomain, 'headers' => $headers, 'stream' => true )); /* Make sure sent mailbox is created. */ $sent_mail = IMP_Mailbox::get($opts['sent_mail']); $sent_mail->create(); $flags = array( Horde_Imap_Client::FLAG_SEEN, /* RFC 3503 [3.3] - MUST set MDNSent flag on sent message. */ Horde_Imap_Client::FLAG_MDNSENT ); try { $imp_imap->append($sent_mail, array(array('data' => $fcc, 'flags' => $flags))); } catch (IMP_Imap_Exception $e) { $notification->push(sprintf(_("Message sent successfully, but not saved to %s."), $sent_mail->display)); } } /* Delete the attachment data. */ $this->deleteAllAttachments(); /* Save recipients to address book? */ $this->_saveRecipients($recip['list']); /* Call post-sent hook. */ try { $injector->getInstance('Horde_Core_Hooks')->callHook( 'post_sent', 'imp', array($save_msg['msg'], $headers) ); } catch (Horde_Exception_HookNotSet $e) {} } /** * Prepare header object with basic header fields and converts headers * to the current compose charset. * * @param array $headers Array with 'from', 'to', 'cc', 'bcc', and * 'subject' values. * @param array $opts An array of options w/the following keys: * - bcc: (boolean) Add BCC header to output. * - priority: (string) The message priority ('high', 'normal', 'low'). * * @return Horde_Mime_Headers Headers object with the appropriate headers * set. */ protected function _prepareHeaders($headers, array $opts = array()) { $ob = new Horde_Mime_Headers(); $ob->addHeader('Date', date('r')); $ob->addMessageIdHeader(); if (isset($headers['from']) && strlen($headers['from'])) { $ob->addHeader('From', $headers['from']); } if (isset($headers['to']) && (is_object($headers['to']) || strlen($headers['to']))) { $ob->addHeader('To', $headers['to']); } if (isset($headers['cc']) && (is_object($headers['cc']) || strlen($headers['cc']))) { $ob->addHeader('Cc', $headers['cc']); } if (!empty($opts['bcc']) && isset($headers['bcc']) && (is_object($headers['bcc']) || strlen($headers['bcc']))) { $ob->addHeader('Bcc', $headers['bcc']); } if (isset($headers['subject']) && strlen($headers['subject'])) { $ob->addHeader('Subject', $headers['subject']); } if ($this->replyType(true) == self::REPLY) { if ($refs = $this->getMetadata('references')) { $ob->addHeader('References', implode(' ', $refs)); } if ($this->getMetadata('in_reply_to')) { $ob->addHeader('In-Reply-To', $this->getMetadata('in_reply_to')); } } /* Add priority header, if requested. */ if (!empty($opts['priority'])) { switch ($opts['priority']) { case 'high': $ob->addHeader('Importance', 'High'); $ob->addHeader('X-Priority', '1 (Highest)'); break; case 'low': $ob->addHeader('Importance', 'Low'); $ob->addHeader('X-Priority', '5 (Lowest)'); break; } } /* Add Return Receipt Headers. */ if (!empty($opts['readreceipt'])) { $from = $ob->getOb('from'); $from = $from[0]; if (is_null($from->host)) { $from->host = $GLOBALS['injector']->getInstance('IMP_Factory_Imap')->create()->config->maildomain; } $mdn = new Horde_Mime_Mdn($ob); $mdn->addMdnRequestHeaders($from); } return $ob; } /** * Sends a message. * * @param Horde_Mail_Rfc822_List $email The e-mail list to send to. * @param Horde_Mime_Headers $headers The object holding this message's * headers. * @param Horde_Mime_Part $message The object that contains the text * to send. * * @throws IMP_Compose_Exception */ public function sendMessage(Horde_Mail_Rfc822_List $email, Horde_Mime_Headers $headers, Horde_Mime_Part $message) { $email = $this->_prepSendMessage($email, $message); $opts = array(); if ($this->getMetadata('encrypt_sign')) { /* Signing requires that the body not be altered in transport. */ $opts['encode'] = Horde_Mime_Part::ENCODE_7BIT; } try { $message->send($email, $headers, $GLOBALS['injector']->getInstance('IMP_Mail'), $opts); } catch (Horde_Mime_Exception $e) { throw new IMP_Compose_Exception($e); } } /** * Sanity checking/MIME formatting before sending a message. * * @param Horde_Mail_Rfc822_List $email The e-mail list to send to. * @param Horde_Mime_Part $message The object that contains the text * to send. * * @return string The encoded $email list. * * @throws IMP_Compose_Exception */ protected function _prepSendMessage(Horde_Mail_Rfc822_List $email, $message = null) { /* Properly encode the addresses we're sending to. Always try * charset of original message as we know that the user can handle * that charset. */ try { return $this->_prepSendMessageEncode($email, is_null($message) ? 'UTF-8' : $message->getHeaderCharset()); } catch (IMP_Compose_Exception $e) { if (is_null($message)) { throw $e; } } /* Fallback to UTF-8 (if replying, original message might be in * US-ASCII, for example, but To/Subject/Etc. may contain 8-bit * characters. */ $message->setHeaderCharset('UTF-8'); return $this->_prepSendMessageEncode($email, 'UTF-8'); } /** * Additonal checks to do if this is a user-generated compose message. * * @param Horde_Mail_Rfc822_List $email The e-mail list to send to. * @param Horde_Mime_Headers $headers The object holding this message's * headers. * @param Horde_Mime_Part $message The object that contains the text * to send. * * @throws IMP_Compose_Exception */ protected function _prepSendMessageAssert(Horde_Mail_Rfc822_List $email, Horde_Mime_Headers $headers = null, Horde_Mime_Part $message = null) { global $injector; $email_count = count($email); $imp_imap = $injector->getInstance('IMP_Factory_Imap')->create(); if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_TIMELIMIT, $email_count)) { Horde::permissionDeniedError('imp', 'max_timelimit'); throw new IMP_Compose_Exception(sprintf( ngettext( "You are not allowed to send messages to more than %d recipient within %d hours.", "You are not allowed to send messages to more than %d recipients within %d hours.", $imp_imap->max_compose_timelimit ), $imp_imap->max_compose_timelimit, $injector->getInstance('IMP_Sentmail')->limit_period )); } /* Count recipients if necessary. We need to split email groups * because the group members count as separate recipients. */ if (!$imp_imap->accessCompose(IMP_Imap::ACCESS_COMPOSE_RECIPIENTS, $email_count)) { Horde::permissionDeniedError('imp', 'max_recipients'); throw new IMP_Compose_Exception(sprintf( ngettext( "You are not allowed to send messages to more than %d recipient.", "You are not allowed to send messages to more than %d recipients.", $imp_imap->max_compose_recipients ), $imp_imap->max_compose_recipients )); } /* Pass to hook to allow alteration of message details. */ if (!is_null($message)) { try { $injector->getInstance('Horde_Core_Hooks')->callHook( 'pre_sent', 'imp', array($message, $headers, $this) ); } catch (Horde_Exception_HookNotSet $e) {} } } /** * Encode address and do sanity checking on encoded address. * * @param Horde_Mail_Rfc822_List $email The e-mail list to send to. * @param string $charset The charset to encode to. * * @return string The encoded $email list. * * @throws IMP_Compose_Exception_Address */ protected function _prepSendMessageEncode(Horde_Mail_Rfc822_List $email, $charset) { global $injector; $exception = new IMP_Compose_Exception_Address(); $hook = true; $out = array(); foreach ($email as $val) { /* $email contains address objects that already have the default * maildomain appended. Need to encode personal part and encode * IDN domain names. */ try { $tmp = $val->writeAddress(array( 'encode' => $charset, 'idn' => true )); /* We have written address, but it still may not be valid. * So double-check. */ $alist = IMP::parseAddressList($tmp, array( 'validate' => true )); $error = null; if ($hook) { try { $error = $injector->getInstance('Horde_Core_Hooks')->callHook( 'compose_addr', 'imp', array($alist[0]) ); } catch (Horde_Exception_HookNotSet $e) { $hook = false; } } } catch (Horde_Idna_Exception $e) { $error = array( 'msg' => sprintf(_("Invalid e-mail address (%s): %s"), $val, $e->getMessage()) ); } catch (Horde_Mail_Exception $e) { $error = array( 'msg' => sprintf(_("Invalid e-mail address (%s)."), $val) ); } if (is_array($error)) { switch (isset($error['level']) ? $error['level'] : $exception::BAD) { case $exception::WARN: case 'warn': if (($warn = $this->getMetadata('warn_addr')) && in_array(strval($val), $warn)) { $out[] = $tmp; continue 2; } $warn[] = strval($val); $this->_setMetadata('warn_addr', $warn); $this->changed = 'changed'; $level = $exception::WARN; break; default: $level = $exception::BAD; break; } $exception->addAddress($val, $error['msg'], $level); } else { $out[] = $tmp; } } if (count($exception)) { throw $exception; } return implode(', ', $out); } /** * Save the recipients done in a sendMessage(). * * @param Horde_Mail_Rfc822_List $recipients The list of recipients. */ public function _saveRecipients(Horde_Mail_Rfc822_List $recipients) { global $notification, $prefs, $registry; if (!$prefs->getValue('save_recipients') || !$registry->hasMethod('contacts/import') || !($abook = $prefs->getValue('add_source'))) { return; } foreach ($recipients as $recipient) { $name = is_null($recipient->personal) ? $recipient->mailbox : $recipient->personal; try { $registry->call( 'contacts/import', array( array('name' => $name, 'email' => $recipient->bare_address), 'array', $abook, array('match_on_email' => true) ) ); $notification->push(sprintf(_("Entry \"%s\" was successfully added to the address book"), $name), 'horde.success'); } catch (Turba_Exception_ObjectExists $e) { } catch (Horde_Exception $e) { if ($e->getCode() == 'horde.error') { $notification->push($e, $e->getCode()); } } } } /** * Cleans up and returns the recipient list. Method designed to parse * user entered data; does not encode/validate addresses. * * @param array $hdr An array of MIME headers and/or address list * objects. Recipients will be extracted from the 'to', * 'cc', and 'bcc' entries. * * @return array An array with the following entries: * - has_input: (boolean) True if at least one of the headers contains * user input. * - header: (array) Contains the cleaned up 'to', 'cc', and 'bcc' * address list (Horde_Mail_Rfc822_List objects). * - list: (Horde_Mail_Rfc822_List) Recipient addresses. */ public function recipientList($hdr) { $addrlist = new Horde_Mail_Rfc822_List(); $has_input = false; $header = array(); foreach (array('to', 'cc', 'bcc') as $key) { if (isset($hdr[$key])) { $ob = IMP::parseAddressList($hdr[$key]); if (count($ob)) { $addrlist->add($ob); $header[$key] = $ob; $has_input = true; } else { $header[$key] = null; } } } return array( 'has_input' => $has_input, 'header' => $header, 'list' => $addrlist ); } /** * Create the base Horde_Mime_Part for sending. * * @param Horde_Mail_Rfc822_List $to The recipient list. * @param string $body Message body. * @param array $options Additional options: * - encrypt: (integer) The encryption flag. * - from: (Horde_Mail_Rfc822_Address) The outgoing from address (only * needed for multiple PGP encryption). * - html: (boolean) Is this a HTML message? * - identity: (IMP_Prefs_Identity) Identity of the sender. * - nofinal: (boolean) This is not a message which will be sent out. * - noattach: (boolean) Don't add attachment information. * - pgp_attach_pubkey: (boolean) Attach the user's PGP public key? * - signature: (IMP_Prefs_Identity|string) If set, add the signature to * the message. * - vcard_attach: (string) If set, attach user's vcard to message. * * @return Horde_Mime_Part The MIME message to send. * * @throws Horde_Exception * @throws IMP_Compose_Exception */ protected function _createMimeMessage( Horde_Mail_Rfc822_List $to, $body, array $options = array() ) { global $conf, $injector, $prefs, $registry; /* Get body text. */ if (empty($options['html'])) { $body_html = null; } else { $tfilter = $injector->getInstance('Horde_Core_Factory_TextFilter'); $body_html = $tfilter->filter( $body, 'Xss', array( 'return_dom' => true, 'strip_style_attributes' => false ) ); $body_html_body = $body_html->getBody(); $body = $tfilter->filter( $body_html->returnHtml(), 'Html2text', array( 'width' => 0 ) ); } $hooks = $injector->getInstance('Horde_Core_Hooks'); /* We need to do the attachment check before any of the body text * has been altered. */ if (!count($this) && !$this->getMetadata('attach_body_check')) { $this->_setMetadata('attach_body_check', true); try { $check = $hooks->callHook( 'attach_body_check', 'imp', array($body) ); } catch (Horde_Exception_HookNotSet $e) { $check = array(); } if (!empty($check) && preg_match('/\b(' . implode('|', array_map('preg_quote', $check)) . ')\b/i', $body, $matches)) { 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])); } } /* Add signature data. */ if (!empty($options['signature'])) { if (is_string($options['signature'])) { if (empty($options['html'])) { $body .= "\n\n" . trim($options['signature']); } else { $html_sig = trim($options['signature']); $body .= "\n" . $tfilter->filter($html_sig, 'Html2text'); } } else { $sig = $options['signature']->getSignature('text'); $body .= $sig; if (!empty($options['html'])) { $html_sig = $options['signature']->getSignature('html'); if (!strlen($html_sig) && strlen($sig)) { $html_sig = $this->text2html($sig); } } } if (!empty($options['html'])) { try { $sig_ob = new IMP_Compose_HtmlSignature($html_sig); } catch (IMP_Exception $e) { throw new IMP_Compose_Exception($e); } foreach ($sig_ob->dom->getBody()->childNodes as $child) { $body_html_body->appendChild( $body_html->dom->importNode($child, true) ); } } } /* Add linked attachments. */ if (empty($options['nofinal'])) { $this->_linkAttachments($body, $body_html); } /* Get trailer text (if any). */ if (empty($options['nofinal'])) { try { $trailer = $hooks->callHook( 'trailer', 'imp', array(false, $options['identity'], $to) ); $html_trailer = $hooks->callHook( 'trailer', 'imp', array(true, $options['identity'], $to) ); } catch (Horde_Exception_HookNotSet $e) { $trailer = $html_trailer = null; } $body .= strval($trailer); if (!empty($options['html'])) { if (is_null($html_trailer) && strlen($trailer)) { $html_trailer = $this->text2html($trailer); } if (strlen($html_trailer)) { $t_dom = new Horde_Domhtml($html_trailer, 'UTF-8'); foreach ($t_dom->getBody()->childNodes as $child) { $body_html_body->appendChild($body_html->dom->importNode($child, true)); } } } } /* Convert text to sending charset. HTML text will be converted * via Horde_Domhtml. */ $body = Horde_String::convertCharset($body, 'UTF-8', $this->charset); /* Set up the body part now. */ $textBody = new Horde_Mime_Part(); $textBody->setType('text/plain'); $textBody->setCharset($this->charset); $textBody->setDisposition('inline'); /* Send in flowed format. */ $flowed = new Horde_Text_Flowed($body, $this->charset); $flowed->setDelSp(true); $textBody->setContentTypeParameter('format', 'flowed'); $textBody->setContentTypeParameter('DelSp', 'Yes'); $text_contents = $flowed->toFlowed(); $textBody->setContents($text_contents); /* Determine whether or not to send a multipart/alternative * message with an HTML part. */ if (!empty($options['html'])) { $htmlBody = new Horde_Mime_Part(); $htmlBody->setType('text/html'); $htmlBody->setCharset($this->charset); $htmlBody->setDisposition('inline'); $htmlBody->setDescription(Horde_String::convertCharset(_("HTML Message"), 'UTF-8', $this->charset)); /* Add default font CSS information here. */ $styles = array(); if ($font_family = $prefs->getValue('compose_html_font_family')) { $styles[] = 'font-family:' . $font_family; } if ($font_size = intval($prefs->getValue('compose_html_font_size'))) { $styles[] = 'font-size:' . $font_size . 'px'; } if (!empty($styles)) { $body_html_body->setAttribute('style', implode(';', $styles)); } if (empty($options['nofinal'])) { $this->_cleanHtmlOutput($body_html); } $to_add = $this->_convertToRelated($body_html, $htmlBody); /* Now, all parts referred to in the HTML data have been added * to the attachment list. Convert to multipart/related if * this is the case. Exception: if text representation is empty, * just send HTML part. */ if (strlen(trim($text_contents))) { $textpart = new Horde_Mime_Part(); $textpart->setType('multipart/alternative'); $textpart->addPart($textBody); $textpart->addPart($to_add); $textpart->setHeaderCharset($this->charset); $textBody->setDescription(Horde_String::convertCharset(_("Plaintext Message"), 'UTF-8', $this->charset)); } else { $textpart = $to_add; } $htmlBody->setContents( $tfilter->filter( $body_html->returnHtml(array( 'charset' => $this->charset, 'metacharset' => true )), 'Cleanhtml', array( 'charset' => $this->charset ) ) ); } else { $textpart = $textBody; } /* Add attachments. */ $base = $textpart; if (empty($options['noattach'])) { $parts = array(); foreach ($this as $val) { if (!$val->related && !$val->linked) { $parts[] = $val->getPart(true); } } if (!empty($options['pgp_attach_pubkey'])) { $parts[] = $injector->getInstance('IMP_Crypt_Pgp')->publicKeyMIMEPart(); } if (!empty($options['vcard_attach'])) { try { $vpart = new Horde_Mime_Part(); $vpart->setType('text/x-vcard'); $vpart->setCharset('UTF-8'); $vpart->setContents($registry->call('contacts/ownVCard')); $vpart->setName($options['vcard_attach']); $parts[] = $vpart; } catch (Horde_Exception $e) { throw new IMP_Compose_Exception(sprintf(_("Can't attach contact information: %s"), $e->getMessage())); } } if (!empty($parts)) { $base = new Horde_Mime_Part(); $base->setType('multipart/mixed'); $base->addPart($textpart); foreach ($parts as $val) { $base->addPart($val); } } } /* Set up the base message now. */ $encrypt = empty($options['encrypt']) ? IMP::ENCRYPT_NONE : $options['encrypt']; if ($prefs->getValue('use_pgp') && !empty($conf['gnupg']['path']) && 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))) { $imp_pgp = $injector->getInstance('IMP_Crypt_Pgp'); $symmetric_passphrase = null; switch ($encrypt) { case IMP_Crypt_Pgp::SIGN: case IMP_Crypt_Pgp::SIGNENC: case IMP_Crypt_Pgp::SYM_SIGNENC: /* Check to see if we have the user's passphrase yet. */ $passphrase = $imp_pgp->getPassphrase('personal'); if (empty($passphrase)) { $e = new IMP_Compose_Exception(_("PGP: Need passphrase for personal private key.")); $e->encrypt = 'pgp_passphrase_dialog'; throw $e; } break; case IMP_Crypt_Pgp::SYM_ENCRYPT: case IMP_Crypt_Pgp::SYM_SIGNENC: /* Check to see if we have the user's symmetric passphrase * yet. */ $symmetric_passphrase = $imp_pgp->getPassphrase('symmetric', 'imp_compose_' . $this->_cacheid); if (empty($symmetric_passphrase)) { $e = new IMP_Compose_Exception(_("PGP: Need passphrase to encrypt your message with.")); $e->encrypt = 'pgp_symmetric_passphrase_dialog'; throw $e; } break; } /* Do the encryption/signing requested. */ try { switch ($encrypt) { case IMP_Crypt_Pgp::SIGN: $base = $imp_pgp->impSignMimePart($base); $this->_setMetadata('encrypt_sign', true); break; case IMP_Crypt_Pgp::ENCRYPT: case IMP_Crypt_Pgp::SYM_ENCRYPT: $to_list = clone $to; if (count($options['from'])) { $to_list->add($options['from']); } $base = $imp_pgp->IMPencryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_ENCRYPT) ? $symmetric_passphrase : null); break; case IMP_Crypt_Pgp::SIGNENC: case IMP_Crypt_Pgp::SYM_SIGNENC: $to_list = clone $to; if (count($options['from'])) { $to_list->add($options['from']); } $base = $imp_pgp->IMPsignAndEncryptMIMEPart($base, $to_list, ($encrypt == IMP_Crypt_Pgp::SYM_SIGNENC) ? $symmetric_passphrase : null); break; } } catch (Horde_Exception $e) { throw new IMP_Compose_Exception(_("PGP Error: ") . $e->getMessage(), $e->getCode()); } } elseif ($prefs->getValue('use_smime') && in_array($encrypt, array(IMP_Crypt_Smime::ENCRYPT, IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) { $imp_smime = $injector->getInstance('IMP_Crypt_Smime'); /* Check to see if we have the user's passphrase yet. */ if (in_array($encrypt, array(IMP_Crypt_Smime::SIGN, IMP_Crypt_Smime::SIGNENC))) { $passphrase = $imp_smime->getPassphrase(); if ($passphrase === false) { $e = new IMP_Compose_Exception(_("S/MIME Error: Need passphrase for personal private key.")); $e->encrypt = 'smime_passphrase_dialog'; throw $e; } } /* Do the encryption/signing requested. */ try { switch ($encrypt) { case IMP_Crypt_Smime::SIGN: $base = $imp_smime->IMPsignMIMEPart($base); $this->_setMetadata('encrypt_sign', true); break; case IMP_Crypt_Smime::ENCRYPT: $base = $imp_smime->IMPencryptMIMEPart($base, $to[0]); break; case IMP_Crypt_Smime::SIGNENC: $base = $imp_smime->IMPsignAndEncryptMIMEPart($base, $to[0]); break; } } catch (Horde_Exception $e) { throw new IMP_Compose_Exception(_("S/MIME Error: ") . $e->getMessage(), $e->getCode()); } } /* Flag this as the base part and rebuild MIME IDs. */ $base->isBasePart(true); $base->buildMimeIds(); return $base; } /** * Determines the reply text and headers for a message. * * @param integer $type The reply type (self::REPLY* constant). * @param IMP_Contents $contents An IMP_Contents object. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * - to: (string) The recipient of the reply. Overrides the * automatically determined value. * * @return array An array with the following keys: * - addr: (array) Address lists (to, cc, bcc; Horde_Mail_Rfc822_List * objects). * - body: (string) The text of the body part. * - format: (string) The format of the body message (html, text). * - identity: (integer) The identity to use for the reply based on the * original message's addresses. * - lang: (array) Language code (keys)/language name (values) of the * original sender's preferred language(s). * - reply_list_id: (string) List ID label. * - reply_recip: (integer) Number of recipients in reply list. * - subject: (string) Formatted subject. * - type: (integer) The reply type used (either self::REPLY_ALL, * self::REPLY_LIST, or self::REPLY_SENDER). * @throws IMP_Exception */ public function replyMessage($type, $contents, array $opts = array()) { global $injector, $language, $prefs; if (!($contents instanceof IMP_Contents)) { throw new IMP_Exception( _("Could not retrieve message data from the mail server.") ); } $alist = new Horde_Mail_Rfc822_List(); $addr = array( 'to' => clone $alist, 'cc' => clone $alist, 'bcc' => clone $alist ); $h = $contents->getHeader(); $match_identity = $this->_getMatchingIdentity($h); $reply_type = self::REPLY_SENDER; if (!$this->_replytype) { $this->_setMetadata('indices', $contents->getIndicesOb()); /* Set the Message-ID related headers (RFC 5322 [3.6.4]). */ $msg_id = new Horde_Mail_Rfc822_Identification( $h->getValue('message-id') ); if (count($msg_id->ids)) { $this->_setMetadata('in_reply_to', reset($msg_id->ids)); } $ref_ob = new Horde_Mail_Rfc822_Identification( $h->getValue('references') ); if (!count($ref_ob->ids)) { $ref_ob = new Horde_Mail_Rfc822_Identification( $h->getValue('in-reply-to') ); if (count($ref_ob->ids) > 1) { $ref_ob->ids = array(); } } if (count($ref_ob->ids)) { $this->_setMetadata( 'references', array_merge($ref_ob->ids, array(reset($msg_id->ids))) ); } } $subject = strlen($s = $h->getValue('subject')) ? 'Re: ' . strval(new Horde_Imap_Client_Data_BaseSubject($s, array('keepblob' => true))) : 'Re: '; $force = false; if (in_array($type, array(self::REPLY_AUTO, self::REPLY_SENDER))) { if (isset($opts['to'])) { $addr['to']->add($opts['to']); $force = true; } elseif ($tmp = $h->getOb('reply-to')) { $addr['to']->add($tmp); $force = true; } else { $addr['to']->add($h->getOb('from')); } } elseif ($type === self::REPLY_ALL) { $force = isset($h['reply-to']); } /* We might need $list_info in the reply_all section. */ $list_info = in_array($type, array(self::REPLY_AUTO, self::REPLY_LIST)) ? $injector->getInstance('IMP_Message_Ui')->getListInformation($h) : null; if (!is_null($list_info) && !empty($list_info['reply_list'])) { /* If To/Reply-To and List-Reply address are the same, no need * to handle these address separately. */ $rlist = new Horde_Mail_Rfc822_Address($list_info['reply_list']); if (!$rlist->match($addr['to'])) { $addr['to'] = clone $alist; $addr['to']->add($rlist); $reply_type = self::REPLY_LIST; } } elseif (in_array($type, array(self::REPLY_ALL, self::REPLY_AUTO))) { /* Clear the To field if we are auto-determining addresses. */ if ($type == self::REPLY_AUTO) { $addr['to'] = clone $alist; } /* Filter out our own address from the addresses we reply to. */ $identity = $injector->getInstance('IMP_Identity'); $all_addrs = $identity->getAllFromAddresses(); /* Build the To: header. It is either: * 1) the Reply-To address (if not a personal address) * 2) the From address(es) (if it doesn't contain a personal * address) * 3) all remaining Cc addresses. */ $to_fields = array('from', 'reply-to'); foreach (array('reply-to', 'from', 'to', 'cc') as $val) { /* If either a reply-to or $to is present, we use this address * INSTEAD of the from address. */ if (($force && ($val == 'from')) || !($ob = $h->getOb($val))) { continue; } /* For From: need to check if at least one of the addresses is * personal. */ if ($val == 'from') { foreach ($ob->raw_addresses as $addr_ob) { if ($all_addrs->contains($addr_ob)) { /* The from field contained a personal address. * Use the 'To' header as the primary reply-to * address instead. */ $to_fields[] = 'to'; /* Add other non-personal from addresses to the * list of CC addresses. */ $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs); $addr['cc']->add($ob); $all_addrs->add($ob); continue 2; } } } $ob->setIteratorFilter($ob::BASE_ELEMENTS, $all_addrs); foreach ($ob as $hdr_ob) { if ($hdr_ob instanceof Horde_Mail_Rfc822_Group) { $addr['cc']->add($hdr_ob); $all_addrs->add($hdr_ob->addresses); } elseif (($val != 'to') || is_null($list_info) || !$force || empty($list_info['exists'])) { /* Don't add as To address if this is a list that * doesn't have a post address but does have a * reply-to address. */ if (in_array($val, $to_fields)) { /* If from/reply-to doesn't have personal * information, check from address. */ if (is_null($hdr_ob->personal) && ($to_ob = $h->getOb('from')) && !is_null($to_ob[0]->personal) && ($hdr_ob->match($to_ob[0]))) { $addr['to']->add($to_ob); } else { $addr['to']->add($hdr_ob); } } else { $addr['cc']->add($hdr_ob); } $all_addrs->add($hdr_ob); } } } /* Build the Cc: (or possibly the To:) header. If this is a * reply to a message that was already replied to by the user, * this reply will go to the original recipients (Request * #8485). */ if (count($addr['cc'])) { $reply_type = self::REPLY_ALL; } if (!count($addr['to'])) { $addr['to'] = $addr['cc']; $addr['cc'] = clone $alist; } /* Build the Bcc: header. */ if ($bcc = $h->getOb('bcc')) { $bcc->add($identity->getBccAddresses()); $bcc->setIteratorFilter(0, $all_addrs); foreach ($bcc as $val) { $addr['bcc']->add($val); } } } if (!$this->_replytype || ($reply_type != $this->_replytype)) { $this->_replytype = $reply_type; $this->changed = 'changed'; } $ret = $this->replyMessageText($contents, array( 'format' => isset($opts['format']) ? $opts['format'] : null )); if ($prefs->getValue('reply_charset') && ($ret['charset'] != $this->charset)) { $this->charset = $ret['charset']; $this->changed = 'changed'; } unset($ret['charset']); if ($type == self::REPLY_AUTO) { switch ($reply_type) { case self::REPLY_ALL: try { $recip_list = $this->recipientList($addr); $ret['reply_recip'] = count($recip_list['list']); } catch (IMP_Compose_Exception $e) { $ret['reply_recip'] = 0; } break; case self::REPLY_LIST: if (($list_parse = $injector->getInstance('Horde_ListHeaders')->parse('list-id', $h->getValue('list-id'))) && !is_null($list_parse->label)) { $ret['reply_list_id'] = $list_parse->label; } break; } } if (($lang = $h->getValue('accept-language')) || ($lang = $h->getValue('x-accept-language'))) { $langs = array(); foreach (explode(',', $lang) as $val) { if (($name = Horde_Nls::getLanguageISO($val)) !== null) { $langs[trim($val)] = $name; } } $ret['lang'] = array_unique($langs); /* Don't show display if original recipient is asking for reply in * the user's native language. */ if ((count($ret['lang']) == 1) && reset($ret['lang']) && (substr(key($ret['lang']), 0, 2) == substr($language, 0, 2))) { unset($ret['lang']); } } return array_merge(array( 'addr' => $addr, 'identity' => $match_identity, 'subject' => $subject, 'type' => $reply_type ), $ret); } /** * Returns the reply text for a message. * * @param IMP_Contents $contents An IMP_Contents object. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return array An array with the following keys: * - body: (string) The text of the body part. * - charset: (string) The guessed charset to use for the reply. * - format: (string) The format of the body message ('html', 'text'). */ public function replyMessageText($contents, array $opts = array()) { global $prefs; if (!$prefs->getValue('reply_quote')) { return array( 'body' => '', 'charset' => '', 'format' => 'text' ); } $h = $contents->getHeader(); $from = strval($h->getOb('from')); if ($prefs->getValue('reply_headers') && !empty($h)) { $msg_pre = '----- ' . ($from ? sprintf(_("Message from %s"), $from) : _("Message")) . /* Extra '-'s line up with "End Message" below. */ " ---------\n" . $this->_getMsgHeaders($h); $msg_post = "\n\n----- " . ($from ? sprintf(_("End message from %s"), $from) : _("End message")) . " -----\n"; } else { $msg_pre = strval(new IMP_Prefs_AttribText($from, $h)); $msg_post = ''; } list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'reply_format'); $msg_text = $this->_getMessageText($contents, array( 'html' => $compose_html, 'replylimit' => true, 'toflowed' => true )); if (!empty($msg_text) && (($msg_text['mode'] == 'html') || $force_html)) { $msg = '<p>' . $this->text2html(trim($msg_pre)) . '</p>' . self::HTML_BLOCKQUOTE . (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['flowed'] ? $msg_text['flowed'] : $msg_text['text']) : $msg_text['text']) . '</blockquote><br />' . ($msg_post ? $this->text2html($msg_post) : '') . '<br />'; $msg_text['mode'] = 'html'; } else { $msg = empty($msg_text['text']) ? '[' . _("No message body text") . ']' : $msg_pre . "\n\n" . $msg_text['text'] . $msg_post; $msg_text['mode'] = 'text'; } // Bug #10148: Message text might be us-ascii, but reply headers may // contain 8-bit characters. if (($msg_text['charset'] == 'us-ascii') && (Horde_Mime::is8bit($msg_pre, 'UTF-8') || Horde_Mime::is8bit($msg_post, 'UTF-8'))) { $msg_text['charset'] = 'UTF-8'; } return array( 'body' => $msg . "\n", 'charset' => $msg_text['charset'], 'format' => $msg_text['mode'] ); } /** * Determine text editor format. * * @param array $opts Options (contains 'format' param). * @param string $pref_name The pref name that controls formatting. * * @return array Use HTML? and Force HTML? */ protected function _msgTextFormat($opts, $pref_name) { if (!empty($opts['format'])) { $compose_html = $force_html = ($opts['format'] == 'html'); } elseif ($GLOBALS['prefs']->getValue('compose_html')) { $compose_html = $force_html = true; } else { $compose_html = $GLOBALS['prefs']->getValue($pref_name); $force_html = false; } return array($compose_html, $force_html); } /** * Determine the text and headers for a forwarded message. * * @param integer $type The forward type (self::FORWARD* * constant). * @param IMP_Contents $contents An IMP_Contents object. * @param boolean $attach Attach the forwarded message? * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return array An array with the following keys: * - attach: (boolean) True if original message was attached. * - body: (string) The text of the body part. * - format: (string) The format of the body message ('html', 'text'). * - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity(). * - subject: (string) Formatted subject. * - title: (string) Title to use on page. * - type: (integer) - The compose type. * @throws IMP_Exception */ public function forwardMessage($type, $contents, $attach = true, array $opts = array()) { global $prefs; if (!($contents instanceof IMP_Contents)) { throw new IMP_Exception( _("Could not retrieve message data from the mail server.") ); } if ($type == self::FORWARD_AUTO) { switch ($prefs->getValue('forward_default')) { case 'body': $type = self::FORWARD_BODY; break; case 'both': $type = self::FORWARD_BOTH; break; case 'editasnew': $ret = $this->editAsNew(new IMP_Indices($contents)); $ret['title'] = _("New Message"); return $ret; case 'attach': default: $type = self::FORWARD_ATTACH; break; } } $h = $contents->getHeader(); $this->_replytype = $type; $this->_setMetadata('indices', $contents->getIndicesOb()); if (strlen($s = $h->getValue('subject'))) { $s = strval(new Horde_Imap_Client_Data_BaseSubject($s, array( 'keepblob' => true ))); $subject = 'Fwd: ' . $s; $title = _("Forward") . ': ' . $s; } else { $subject = 'Fwd:'; $title = _("Forward"); } $fwd_attach = false; if ($attach && in_array($type, array(self::FORWARD_ATTACH, self::FORWARD_BOTH))) { try { $this->attachImapMessage(new IMP_Indices($contents)); $fwd_attach = true; } catch (IMP_Exception $e) {} } if (in_array($type, array(self::FORWARD_BODY, self::FORWARD_BOTH))) { $ret = $this->forwardMessageText($contents, array( 'format' => isset($opts['format']) ? $opts['format'] : null )); unset($ret['charset']); } else { $ret = array( 'body' => '', 'format' => $prefs->getValue('compose_html') ? 'html' : 'text' ); } return array_merge(array( 'attach' => $fwd_attach, 'identity' => $this->_getMatchingIdentity($h), 'subject' => $subject, 'title' => $title, 'type' => $type ), $ret); } /** * Returns the forward text for a message. * * @param IMP_Contents $contents An IMP_Contents object. * @param array $opts Additional options: * - format: (string) Force to this format. * DEFAULT: Auto-determine. * * @return array An array with the following keys: * - body: (string) The text of the body part. * - charset: (string) The guessed charset to use for the forward. * - format: (string) The format of the body message ('html', 'text'). */ public function forwardMessageText($contents, array $opts = array()) { $h = $contents->getHeader(); $from = strval($h->getOb('from')); $msg_pre = "\n----- " . ($from ? sprintf(_("Forwarded message from %s"), $from) : _("Forwarded message")) . " -----\n" . $this->_getMsgHeaders($h) . "\n"; $msg_post = "\n\n----- " . _("End forwarded message") . " -----\n"; list($compose_html, $force_html) = $this->_msgTextFormat($opts, 'forward_format'); $msg_text = $this->_getMessageText($contents, array( 'html' => $compose_html )); if (!empty($msg_text) && (($msg_text['mode'] == 'html') || $force_html)) { $msg = $this->text2html($msg_pre) . (($msg_text['mode'] == 'text') ? $this->text2html($msg_text['text']) : $msg_text['text']) . $this->text2html($msg_post); $format = 'html'; } else { $msg = $msg_pre . $msg_text['text'] . $msg_post; $format = 'text'; } // Bug #10148: Message text might be us-ascii, but forward headers may // contain 8-bit characters. if (($msg_text['charset'] == 'us-ascii') && (Horde_Mime::is8bit($msg_pre, 'UTF-8') || Horde_Mime::is8bit($msg_post, 'UTF-8'))) { $msg_text['charset'] = 'UTF-8'; } return array( 'body' => $msg, 'charset' => $msg_text['charset'], 'format' => $format ); } /** * Prepares a forwarded message using multiple messages. * * @param IMP_Indices $indices An indices object containing the indices * of the forwarded messages. * * @return array An array with the following keys: * - body: (string) The text of the body part. * - format: (string) The format of the body message ('html', 'text'). * - identity: (mixed) See IMP_Prefs_Identity#getMatchingIdentity(). * - subject: (string) Formatted subject. * - title: (string) Title to use on page. * - type: (integer) The compose type. */ public function forwardMultipleMessages(IMP_Indices $indices) { global $injector, $prefs, $session; $this->_setMetadata('indices', $indices); $this->_replytype = self::FORWARD_ATTACH; $subject = $this->attachImapMessage($indices); return array( 'body' => '', 'format' => ($prefs->getValue('compose_html') && $session->get('imp', 'rteavail')) ? 'html' : 'text', 'identity' => $injector->getInstance('IMP_Identity')->getDefault(), 'subject' => $subject, 'title' => $subject, 'type' => self::FORWARD ); } /** * Prepare a redirect message. * * @param IMP_Indices $indices An indices object. */ public function redirectMessage(IMP_Indices $indices) { $this->_setMetadata('redirect_indices', $indices); $this->_replytype = self::REDIRECT; } /** * Send a redirect (a/k/a resent) message. See RFC 5322 [3.6.6]. * * @param mixed $to The addresses to redirect to. * @param boolean $log Whether to log the resending in the history and * sentmail log. * * @return array An object with the following properties for each * redirected message: * - contents: (IMP_Contents) The contents object. * - headers: (Horde_Mime_Headers) The header object. * - mbox: (IMP_Mailbox) Mailbox of the message. * - uid: (string) UID of the message. * * @throws IMP_Compose_Exception */ public function sendRedirectMessage($to, $log = true) { global $injector, $registry; $recip = $this->recipientList(array('to' => $to)); if (!count($recip['list'])) { if ($recip['has_input']) { throw new IMP_Compose_Exception(_("Invalid e-mail address.")); } throw new IMP_Compose_Exception(_("Need at least one message recipient.")); } $identity = $injector->getInstance('IMP_Identity'); $from_addr = $identity->getFromAddress(); $out = array(); foreach ($this->getMetadata('redirect_indices') as $val) { foreach ($val->uids as $val2) { try { $contents = $injector->getInstance('IMP_Factory_Contents')->create($val->mbox->getIndicesOb($val2)); } catch (IMP_Exception $e) { throw new IMP_Compose_Exception(_("Error when redirecting message.")); } $headers = $contents->getHeader(); /* We need to set the Return-Path header to the current user - * see RFC 2821 [4.4]. */ $headers->removeHeader('return-path'); $headers->addHeader('Return-Path', $from_addr); /* Generate the 'Resent' headers (RFC 5322 [3.6.6]). These * headers are prepended to the message. */ $resent_headers = new Horde_Mime_Headers(); $resent_headers->addHeader('Resent-Date', date('r')); $resent_headers->addHeader('Resent-From', $from_addr); $resent_headers->addHeader('Resent-To', $recip['header']['to']); $resent_headers->addHeader('Resent-Message-ID', Horde_Mime::generateMessageId()); $header_text = trim($resent_headers->toString(array('encode' => 'UTF-8'))) . "\n" . trim($contents->getHeader(IMP_Contents::HEADER_TEXT)); $this->_prepSendMessageAssert($recip['list']); $to = $this->_prepSendMessage($recip['list']); $hdr_array = $headers->toArray(array('charset' => 'UTF-8')); $hdr_array['_raw'] = $header_text; try { $injector->getInstance('IMP_Mail')->send($to, $hdr_array, $contents->getBody()); } catch (Horde_Mail_Exception $e) { $e2 = new IMP_Compose_Exception($e); if (($prev = $e->getPrevious()) && ($prev instanceof Horde_Smtp_Exception)) { Horde::log( sprintf( "SMTP Error: %s (%u; %s)", $prev->raw_msg, $prev->getCode(), $prev->getEnhancedSmtpCode() ?: 'N/A' ), 'ERR' ); $e2->logged = true; } throw $e2; } $recipients = strval($recip['list']); Horde::log(sprintf("%s Redirected message sent to %s from %s", $_SERVER['REMOTE_ADDR'], $recipients, $registry->getAuth()), 'INFO'); if ($log) { /* Store history information. */ $msg_id = new Horde_Mail_Rfc822_Identification( $headers->getValue('message-id') ); $injector->getInstance('IMP_Maillog')->log( new IMP_Maillog_Message(reset($msg_id->ids)), new IMP_Maillog_Log_Redirect($recipients) ); $injector->getInstance('IMP_Sentmail')->log( IMP_Sentmail::REDIRECT, reset($msg_id->ids), $recipients ); } $tmp = new stdClass; $tmp->contents = $contents; $tmp->headers = $headers; $tmp->mbox = $val->mbox; $tmp->uid = $val2; $out[] = $tmp; } } return $out; } /** * Get "tieto" identity information. * * @param Horde_Mime_Headers $h The headers object for the message. * @param array $only Only use these headers. * * @return integer The matching identity. If no exact match, returns the * default identity. */ protected function _getMatchingIdentity($h, array $only = array()) { global $injector; $identity = $injector->getInstance('IMP_Identity'); $msgAddresses = array(); if (empty($only)) { /* Bug #9271: Check 'from' address first; if replying to a message * originally sent by user, this should be the identity used for * the reply also. */ $only = array('from', 'to', 'cc', 'bcc'); } foreach ($only as $val) { $msgAddresses[] = $h->getValue($val); } $match = $identity->getMatchingIdentity($msgAddresses); return is_null($match) ? $identity->getDefault() : $match; } /** * Add mail message(s) from the mail server as a message/rfc822 * attachment. * * @param IMP_Indices $indices An indices object. * * @return string Subject string. * * @throws IMP_Exception */ public function attachImapMessage($indices) { if (!count($indices)) { return false; } $attached = 0; foreach ($indices as $ob) { foreach ($ob->uids as $idx) { ++$attached; $contents = $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create(new IMP_Indices($ob->mbox, $idx)); $headerob = $contents->getHeader(); $part = new Horde_Mime_Part(); $part->setCharset('UTF-8'); $part->setType('message/rfc822'); $part->setName(_("Forwarded Message")); $part->setContents($contents->fullMessageText(array( 'stream' => true )), array( 'usestream' => true )); // Throws IMP_Compose_Exception. $this->addAttachmentFromPart($part); $part->clearContents(); } } if ($attached > 1) { return 'Fwd: ' . sprintf(_("%u Forwarded Messages"), $attached); } if ($name = $headerob->getValue('subject')) { $name = Horde_String::truncate($name, 80); } else { $name = _("[No Subject]"); } return 'Fwd: ' . strval(new Horde_Imap_Client_Data_BaseSubject($name, array('keepblob' => true))); } /** * Determine the header information to display in the forward/reply. * * @param Horde_Mime_Headers $h The headers object for the message. * * @return string The header information for the original message. */ protected function _getMsgHeaders($h) { $tmp = array(); if (($ob = $h->getValue('date'))) { $tmp[_("Date")] = $ob; } if (($ob = strval($h->getOb('from')))) { $tmp[_("From")] = $ob; } if (($ob = strval($h->getOb('reply-to')))) { $tmp[_("Reply-To")] = $ob; } if (($ob = $h->getValue('subject'))) { $tmp[_("Subject")] = $ob; } if (($ob = strval($h->getOb('to')))) { $tmp[_("To")] = $ob; } if (($ob = strval($h->getOb('cc')))) { $tmp[_("Cc")] = $ob; } $text = ''; if (!empty($tmp)) { $max = max(array_map(array('Horde_String', 'length'), array_keys($tmp))) + 2; foreach ($tmp as $key => $val) { $text .= Horde_String::pad($key . ': ', $max, ' ', STR_PAD_LEFT) . $val . "\n"; } } return $text; } /** * Add an attachment referred to in a related part. * * @param IMP_Compose_Attachment $act_ob Attachment data. * @param DOMElement $node Node element containg the * related reference. * @param string $attribute Element attribute containing the * related reference. */ public function addRelatedAttachment(IMP_Compose_Attachment $atc_ob, DOMElement $node, $attribute) { $atc_ob->related = true; $node->setAttribute(self::RELATED_ATTR, $attribute . ';' . $atc_ob->id); } /** * Deletes all attachments. */ public function deleteAllAttachments() { foreach (array_keys($this->_atc) as $key) { unset($this[$key]); } } /** * Obtains the cache ID for the session object. * * @return string The message cache ID. */ public function getCacheId() { return $this->_cacheid; } /** * Generate HMAC hash used to validate data on a session expiration. Uses * the unique compose cache ID of the expired message, the username, and * the secret key of the server to generate a reproducible value that can * be validated if session data doesn't exist. * * @param string $cacheid The cache ID to use. If null, uses cache ID of * the compose object. * @param string $user The user ID to use. If null, uses the current * authenticated username. * * @return string The HMAC hash string. */ public function getHmac($cacheid = null, $user = null) { global $conf, $registry; return hash_hmac( 'sha1', (is_null($cacheid) ? $this->getCacheId() : $cacheid) . '|' . (is_null($user) ? $registry->getAuth() : $user), $conf['secret_key'] ); } /** * How many more attachments are allowed? * * @return mixed Returns true if no attachment limit. * Else returns the number of additional attachments * allowed. */ public function additionalAttachmentsAllowed() { global $conf; return empty($conf['compose']['attach_count_limit']) ? true : ($conf['compose']['attach_count_limit'] - count($this)); } /** * What is the maximum attachment size? * * @return integer The maximum attachment size (in bytes). */ public function maxAttachmentSize() { $size = $GLOBALS['session']->get('imp', 'file_upload'); return empty($GLOBALS['conf']['compose']['attach_size_limit']) ? $size : min($size, $GLOBALS['conf']['compose']['attach_size_limit']); } /** * Clean outgoing HTML (remove unexpected data URLs). * * @param Horde_Domhtml $html The HTML data. */ protected function _cleanHtmlOutput(Horde_Domhtml $html) { global $registry; $xpath = new DOMXPath($html->dom); foreach ($xpath->query('//*[@src]') as $node) { $src = $node->getAttribute('src'); /* Check for attempts to sneak data URL information into the * output. */ if (Horde_Url_Data::isData($src)) { if (IMP_Compose_HtmlSignature::isSigImage($node, true)) { /* This is HTML signature image data. Convert to an * attachment. */ $sig_img = new Horde_Url_Data($src); if ($sig_img->data) { $data_part = new Horde_Mime_Part(); $data_part->setContents($sig_img->data); $data_part->setType($sig_img->type); try { $this->addRelatedAttachment( $this->addAttachmentFromPart($data_part), $node, 'src' ); } catch (IMP_Compose_Exception $e) { // Remove image on error. } } } $node->removeAttribute('src'); } elseif (strcasecmp($node->tagName, 'IMG') === 0) { /* Check for smileys. They live in the JS directory, under * the base ckeditor directory, so search for that and replace * with the filesystem information if found (Request * #13051). Need to ignore other image links that may have * been explicitly added by the user. */ $js_path = strval(Horde::url($registry->get('jsuri', 'horde'), true)); if (stripos($src, $js_path . '/ckeditor') === 0) { $file = str_replace( $js_path, $registry->get('jsfs', 'horde'), $src ); if (is_readable($file)) { $data_part = new Horde_Mime_Part(); $data_part->setContents(file_get_contents($file)); $data_part->setName(basename($file)); try { $this->addRelatedAttachment( $this->addAttachmentFromPart($data_part), $node, 'src' ); } catch (IMP_Compose_Exception $e) { // Keep existing data on error. } } } } } } /** * Converts an HTML part to a multipart/related part, if necessary. * * @param Horde_Domhtml $html HTML data. * @param Horde_Mime_Part $part The HTML part. * * @return Horde_Mime_Part The part to add to the compose output. */ protected function _convertToRelated(Horde_Domhtml $html, Horde_Mime_Part $part) { $r_part = false; foreach ($this as $atc) { if ($atc->related) { $r_part = true; break; } } if (!$r_part) { return $part; } /* Create new multipart/related part. */ $related = new Horde_Mime_Part(); $related->setType('multipart/related'); /* Get the CID for the 'root' part. Although by default the first part * is the root part (RFC 2387 [3.2]), we may as well be explicit and * put the CID in the 'start' parameter. */ $related->setContentTypeParameter('start', $part->setContentId()); $related->addPart($part); /* HTML iteration is from child->parent, so need to gather related * parts and add at end after sorting to generate a more sensible * attachment list. */ $add = array(); foreach ($html as $node) { if (($node instanceof DOMElement) && $node->hasAttribute(self::RELATED_ATTR)) { list($attr_name, $atc_id) = explode(';', $node->getAttribute(self::RELATED_ATTR)); /* If attachment can't be found, ignore. */ if ($r_atc = $this[$atc_id]) { if ($r_atc->linked) { $attr = strval($r_atc->link_url); } else { $related_part = $r_atc->getPart(true); $attr = 'cid:' . $related_part->setContentId(); $add[] = $related_part; } $node->setAttribute($attr_name, $attr); } $node->removeAttribute(self::RELATED_ATTR); } } array_map(array($related, 'addPart'), array_reverse($add)); return $related; } /** * Adds linked attachments to message. * * @param string &$body Plaintext data. * @param mixed $html HTML data (Horde_Domhtml) or null. * * @throws IMP_Compose_Exception */ protected function _linkAttachments(&$body, $html) { global $conf; if (empty($conf['compose']['link_attachments'])) { return; } $link_all = false; $linked = array(); if (!empty($conf['compose']['link_attach_size_hard'])) { $limit = intval($conf['compose']['link_attach_size_hard']); foreach ($this as $val) { if (($limit -= $val->getPart()->getBytes()) < 0) { $link_all = true; break; } } } foreach (iterator_to_array($this) as $key => $val) { if ($link_all && !$val->linked) { $val = new IMP_Compose_Attachment($this, $val->getPart(), $val->storage->getTempFile()); $val->forceLinked = true; unset($this[$key]); $this[$key] = $val; } if ($val->linked && !$val->related) { $linked[] = $val; } } if (empty($linked)) { return; } if ($del_time = IMP_Compose_LinkedAttachment::keepDate(false)) { /* Subtract 1 from time to get the last day of the previous * month. */ $expire = ' (' . sprintf(_("links will expire on %s"), strftime('%x', $del_time - 1)) . ')'; } $body .= "\n-----\n" . _("Attachments") . $expire . ":\n"; if ($html) { $body = $html->getBody(); $dom = $html->dom; $body->appendChild($dom->createElement('HR')); $body->appendChild($div = $dom->createElement('DIV')); $div->appendChild($dom->createElement('H4', _("Attachments") . $expire . ':')); $div->appendChild($ol = $dom->createElement('OL')); } $i = 0; foreach ($linked as $val) { $apart = $val->getPart(); $name = $apart->getName(true); $size = IMP::sizeFormat($apart->getBytes()); $url = strval($val->link_url->setRaw(true)); $body .= "\n" . (++$i) . '. ' . $name . ' (' . $size . ') [' . $apart->getType() . "]\n" . sprintf(_("Download link: %s"), $url) . "\n"; if ($html) { $ol->appendChild($li = $dom->createElement('LI')); $li->appendChild($dom->createElement('STRONG', $name)); $li->appendChild($dom->createTextNode(' (' . $size . ') [' . htmlspecialchars($apart->getType()) . ']')); $li->appendChild($dom->createElement('BR')); $li->appendChild($dom->createTextNode(_("Download link") . ': ')); $li->appendChild($a = $dom->createElement('A', htmlspecialchars($url))); $a->setAttribute('href', $url); } } } /** * Regenerates body text for use in the compose screen from IMAP data. * * @param IMP_Contents $contents An IMP_Contents object. * @param array $options Additional options: * <ul> * <li>html: (boolean) Return text/html part, if available.</li> * <li>imp_msg: (integer) If non-empty, the message data was created by * IMP. Either: * <ul> * <li>self::COMPOSE</li> * <li>self::FORWARD</li> * <li>self::REPLY</li> * </ul> * </li> * <li>replylimit: (boolean) Enforce length limits?</li> * <li>toflowed: (boolean) Do flowed conversion?</li> * </ul> * * @return mixed Null if bodypart not found, or array with the following * keys: * - charset: (string) The guessed charset to use. * - flowed: (Horde_Text_Flowed) A flowed object, if the text is flowed. * Otherwise, null. * - id: (string) The MIME ID of the bodypart. * - mode: (string) Either 'text' or 'html'. * - text: (string) The body text. */ protected function _getMessageText($contents, array $options = array()) { global $conf, $injector, $notification, $prefs, $session; $body_id = null; $mode = 'text'; $options = array_merge(array( 'imp_msg' => self::COMPOSE ), $options); if (!empty($options['html']) && $session->get('imp', 'rteavail') && (($body_id = $contents->findBody('html')) !== null)) { $mime_message = $contents->getMIMEMessage(); switch ($mime_message->getPrimaryType()) { case 'multipart': if (($body_id != '1') && ($mime_message->getSubType() == 'mixed') && !Horde_Mime::isChild('1', $body_id)) { $body_id = null; } else { $mode = 'html'; } break; default: if (strval($body_id) != '1') { $body_id = null; } else { $mode = 'html'; } break; } } if (is_null($body_id)) { $body_id = $contents->findBody(); if (is_null($body_id)) { return null; } } $part = $contents->getMIMEPart($body_id); $type = $part->getType(); $part_charset = $part->getCharset(); $msg = Horde_String::convertCharset($part->getContents(), $part_charset, 'UTF-8'); /* Enforce reply limits. */ if (!empty($options['replylimit']) && !empty($conf['compose']['reply_limit'])) { $limit = $conf['compose']['reply_limit']; if (Horde_String::length($msg) > $limit) { $msg = Horde_String::substr($msg, 0, $limit) . "\n" . _("[Truncated Text]"); } } if ($mode == 'html') { $dom = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter( $msg, 'Xss', array( 'charset' => $this->charset, 'return_dom' => true, 'strip_style_attributes' => false ) ); /* If we are replying to a related part, and this part refers * to local message parts, we need to move those parts into this * message (since the original message may disappear during the * compose process). */ if ($related_part = $contents->findMimeType($body_id, 'multipart/related')) { $this->_setMetadata('related_contents', $contents); $related_ob = new Horde_Mime_Related($related_part); $related_ob->cidReplace($dom, array($this, '_getMessageTextCallback'), $part_charset); $this->_setMetadata('related_contents', null); } /* Convert any Data URLs to attachments. */ $xpath = new DOMXPath($dom->dom); foreach ($xpath->query('//*[@src]') as $val) { $data_url = new Horde_Url_Data($val->getAttribute('src')); if (strlen($data_url->data)) { $data_part = new Horde_Mime_Part(); $data_part->setContents($data_url->data); $data_part->setType($data_url->type); try { $atc = $this->addAttachmentFromPart($data_part); $val->setAttribute('src', $atc->viewUrl()); $this->addRelatedAttachment($atc, $val, 'src'); } catch (IMP_Compose_Exception $e) { $notification->push($e, 'horde.warning'); } } } $msg = $dom->returnBody(); } elseif ($type == 'text/html') { $msg = $injector->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Html2text'); $type = 'text/plain'; } /* Always remove leading/trailing whitespace. The data in the * message body is not intended to be the exact representation of the * original message (use forward as message/rfc822 part for that). */ $msg = trim($msg); if ($type == 'text/plain') { if ($prefs->getValue('reply_strip_sig') && (($pos = strrpos($msg, "\n-- ")) !== false)) { $msg = rtrim(substr($msg, 0, $pos)); } /* Remove PGP armored text. */ $pgp = $injector->getInstance('Horde_Crypt_Pgp_Parse')->parseToPart($msg); if (!is_null($pgp)) { $msg = ''; $pgp->buildMimeIds(); foreach ($pgp->contentTypeMap() as $key => $val) { if (strpos($val, 'text/') === 0) { $msg .= $pgp[$key]->getContents(); } } } if ($part->getContentTypeParameter('format') == 'flowed') { $flowed = new Horde_Text_Flowed($msg, 'UTF-8'); if (Horde_String::lower($part->getContentTypeParameter('delsp')) == 'yes') { $flowed->setDelSp(true); } $flowed->setMaxLength(0); $msg = $flowed->toFixed(false); } else { /* If the input is *not* in flowed format, make sure there is * no padding at the end of lines. */ $msg = preg_replace("/\s*\n/U", "\n", $msg); } if (isset($options['toflowed'])) { $flowed = new Horde_Text_Flowed($msg, 'UTF-8'); $msg = $options['toflowed'] ? $flowed->toFlowed(true) : $flowed->toFlowed(false, array('nowrap' => true)); } } if (strcasecmp($part->getCharset(), 'windows-1252') === 0) { $part_charset = 'ISO-8859-1'; } return array( 'charset' => $part_charset, 'flowed' => isset($flowed) ? $flowed : null, 'id' => $body_id, 'mode' => $mode, 'text' => $msg ); } /** * Callback used in _getMessageText(). * * @return Horde_Url */ public function _getMessageTextCallback($id, $attribute, $node) { $atc = $this->addAttachmentFromPart($this->getMetadata('related_contents')->getMIMEPart($id)); $this->addRelatedAttachment($atc, $node, $attribute); return $atc->viewUrl(); } /** * Adds an attachment from Horde_Mime_Part data. * * @param Horde_Mime_Part $part The object that contains the attachment * data. * * @return IMP_Compose_Attachment Attachment object. * @throws IMP_Compose_Exception */ public function addAttachmentFromPart($part) { /* Extract the data from the Horde_Mime_Part. */ $atc_file = Horde::getTempFile('impatt'); $stream = $part->getContents(array( 'stream' => true )); rewind($stream); $dest_handle = fopen($atc_file, 'w+b'); while (!feof($stream)) { fwrite($dest_handle, fread($stream, 1024)); } fclose($dest_handle); $size = ftell($stream); if ($size === false) { throw new IMP_Compose_Exception(sprintf(_("Could not attach %s to the message."), $part->getName())); } return $this->_addAttachment( $atc_file, $size, $part->getName(true), $part->getType() ); } /** * Add attachment from uploaded (form) data. * * @param string $field The form field name. * * @return array A list of IMP_Compose_Attachment objects (if * successfully attached) or IMP_Compose_Exception objects * (if error when attaching). * @throws IMP_Compose_Exception */ public function addAttachmentFromUpload($field) { global $browser; try { $browser->wasFileUploaded($field, _("attachment")); } catch (Horde_Browser_Exception $e) { throw new IMP_Compose_Exception($e); } $finfo = array(); if (is_array($_FILES[$field]['size'])) { for ($i = 0; $i < count($_FILES[$field]['size']); ++$i) { $tmp = array(); foreach ($_FILES[$field] as $key => $val) { $tmp[$key] = $val[$i]; } $finfo[] = $tmp; } } else { $finfo[] = $_FILES[$field]; } $out = array(); foreach ($finfo as $val) { switch (empty($val['type']) ? $val['type'] : '') { case 'application/unknown': case '': $type = 'application/octet-stream'; break; default: $type = $val['type']; break; } try { $out[] = $this->_addAttachment( $val['tmp_name'], $val['size'], Horde_Util::dispelMagicQuotes($val['name']), $type ); } catch (IMP_Compose_Exception $e) { $out[] = $e; } } return $out; } /** * Adds an attachment to the outgoing compose message. * * @param string $atc_file Temporary file containing attachment contents. * @param integer $bytes Size of data, in bytes. * @param string $filename Filename of data. * @param string $type MIME type of data. * * @return IMP_Compose_Attachment Attachment object. * @throws IMP_Compose_Exception */ protected function _addAttachment($atc_file, $bytes, $filename, $type) { global $conf, $injector; $atc = new Horde_Mime_Part(); $atc->setBytes($bytes); /* Try to determine the MIME type from 1) the extension and * then 2) analysis of the file (if available). */ if (strlen($filename)) { $atc->setName($filename); if ($type == 'application/octet-stream') { $type = Horde_Mime_Magic::filenameToMIME($filename, false); } } $atc->setType($type); $atc->setHeaderCharset('UTF-8'); if (($atc->getType() == 'application/octet-stream') || ($atc->getPrimaryType() == 'text')) { $analyze = Horde_Mime_Magic::analyzeFile($atc_file, empty($conf['mime']['magic_db']) ? null : $conf['mime']['magic_db'], array( 'nostrip' => true )); if ($analyze) { $analyze = Horde_Mime::decodeParam('Content-Type', $analyze); $atc->setType($analyze['val']); $atc->setCharset(isset($analyze['params']['charset']) ? $analyze['params']['charset'] : 'UTF-8'); } else { $atc->setCharset('UTF-8'); } } $atc_ob = new IMP_Compose_Attachment($this, $atc, $atc_file); /* Check for attachment size limitations. */ $size_limit = null; if ($atc_ob->linked) { if (!empty($conf['compose']['link_attach_size_limit'])) { $linked = true; $size_limit = 'link_attach_size_limit'; } } elseif (!empty($conf['compose']['attach_size_limit'])) { $linked = false; $size_limit = 'attach_size_limit'; } if (!is_null($size_limit)) { $total_size = $conf['compose'][$size_limit] - $bytes; foreach ($this as $val) { if ($val->linked == $linked) { $total_size -= $val->getPart()->getBytes(); } } if ($total_size < 0) { 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.")); } } try { $injector->getInstance('Horde_Core_Hooks')->callHook( 'compose_attachment', 'imp', array($atc_ob) ); } catch (Horde_Exception_HookNotSet $e) {} $this->_atc[$atc_ob->id] = $atc_ob; $this->changed = 'changed'; return $atc_ob; } /** * Store draft compose data if session expires. * * @param Horde_Variables $vars Object with the form data. */ public function sessionExpireDraft(Horde_Variables $vars) { global $conf, $injector; if (empty($conf['compose']['use_vfs']) || !isset($vars->composeCache) || !isset($vars->composeHmac) || !isset($vars->user) || ($this->getHmac($vars->composeCache, $vars->user) != $vars->composeHmac)) { return; } $headers = array(); foreach (array('to', 'cc', 'bcc', 'subject') as $val) { $headers[$val] = $vars->$val; } try { $body = $this->_saveDraftMsg($headers, $vars->message, array( 'html' => $vars->rtemode, 'priority' => $vars->priority, 'readreceipt' => $vars->request_read_receipt )); $injector->getInstance('Horde_Core_Factory_Vfs')->create()->writeData(self::VFS_DRAFTS_PATH, hash('sha1', $vars->user), $body, true); } catch (Exception $e) {} } /** * Restore session expiration draft compose data. */ public function recoverSessionExpireDraft() { global $conf, $injector, $notification; if (empty($conf['compose']['use_vfs'])) { return; } $filename = hash('sha1', $GLOBALS['registry']->getAuth()); try { $vfs = $injector->getInstance('Horde_Core_Factory_Vfs')->create(); if ($vfs->exists(self::VFS_DRAFTS_PATH, $filename)) { $data = $vfs->read(self::VFS_DRAFTS_PATH, $filename); $this->_saveDraftServer($data); $vfs->deleteFile(self::VFS_DRAFTS_PATH, $filename); $notification->push( _("A message you were composing when your session expired has been recovered. You may resume composing your message by going to your Drafts mailbox."), 'horde.message', array('sticky') ); } } catch (Exception $e) {} } /** * If this object contains sufficient metadata, return an IMP_Contents * object reflecting that metadata. * * @return mixed Either an IMP_Contents object or null. */ public function getContentsOb() { return ($this->_replytype && ($indices = $this->getMetadata('indices')) && (count($indices) === 1)) ? $GLOBALS['injector']->getInstance('IMP_Factory_Contents')->create($indices) : null; } /** * Return the reply type. * * @param boolean $base Return the base reply type? * * @return string The reply type, or null if not a reply. */ public function replyType($base = false) { switch ($this->_replytype) { case self::FORWARD: case self::FORWARD_ATTACH: case self::FORWARD_BODY: case self::FORWARD_BOTH: return $base ? self::FORWARD : $this->_replytype; case self::REPLY: case self::REPLY_ALL: case self::REPLY_LIST: case self::REPLY_SENDER: return $base ? self::REPLY : $this->_replytype; case self::REDIRECT: return $this->_replytype; default: return null; } } /* Static methods. */ /** * Is composing messages allowed? * * @return boolean True if compose allowed. * @throws Horde_Exception */ static public function canCompose() { try { return !$GLOBALS['injector']->getInstance('Horde_Core_Hooks')->callHook('disable_compose', 'imp'); } catch (Horde_Exception_HookNotSet $e) { return true; } } /** * Can attachments be uploaded? * * @return boolean True if attachments can be uploaded. */ static public function canUploadAttachment() { return ($GLOBALS['session']->get('imp', 'file_upload') != 0); } /** * Shortcut function to convert text -> HTML for purposes of composition. * * @param string $msg The message text. * * @return string HTML text. */ static public function text2html($msg) { return $GLOBALS['injector']->getInstance('Horde_Core_Factory_TextFilter')->filter($msg, 'Text2html', array( 'always_mailto' => true, 'flowed' => self::HTML_BLOCKQUOTE, 'parselevel' => Horde_Text_Filter_Text2html::MICRO )); } /* ArrayAccess methods. */ public function offsetExists($offset) { return isset($this->_atc[$offset]); } public function offsetGet($offset) { return isset($this->_atc[$offset]) ? $this->_atc[$offset] : null; } public function offsetSet($offset, $value) { $this->_atc[$offset] = $value; $this->changed = 'changed'; } public function offsetUnset($offset) { if (($atc = $this->_atc[$offset]) === null) { return; } $atc->delete(); unset($this->_atc[$offset]); $this->changed = 'changed'; } /* Magic methods. */ /** * String representation: the cache ID. */ public function __toString() { return $this->getCacheId(); } /* Countable method. */ /** * Returns the number of attachments currently in this message. * * @return integer The number of attachments in this message. */ public function count() { return count($this->_atc); } /* IteratorAggregate method. */ /** */ public function getIterator() { return new ArrayIterator($this->_atc); } }
Simpan