Files
server/usr/share/psa-horde/imp/lib/Compose.php
2026-01-07 20:52:11 +01:00

3549 lines
125 KiB
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);
}
}