';
/**
* 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:
*
* - 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?
*
*
* @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
* - IMP_Crypt_Pgp::SIGNENC
* - IMP_Crypt_Smime::ENCRYPT
* - IMP_Crypt_Smime::SIGNENC
* - 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 = '' . $this->text2html(trim($msg_pre)) . '
' .
self::HTML_BLOCKQUOTE .
(($msg_text['mode'] == 'text') ? $this->text2html($msg_text['flowed'] ? $msg_text['flowed'] : $msg_text['text']) : $msg_text['text']) .
'