Page MenuHomePhorge

No OneTemporary

diff --git a/program/include/rcube_ldap.php b/program/include/rcube_ldap.php
index 90ce73ae2..7cef25556 100644
--- a/program/include/rcube_ldap.php
+++ b/program/include/rcube_ldap.php
@@ -1,2350 +1,2351 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/include/rcube_ldap.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2006-2012, The Roundcube Dev Team |
| Copyright (C) 2011, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Interface to an LDAP address directory |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Andreas Dick <andudi (at) gmx (dot) ch> |
| Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
/**
* Model class to access an LDAP address directory
*
* @package Addressbook
*/
class rcube_ldap extends rcube_addressbook
{
/** public properties */
public $primary_key = 'ID';
public $groups = false;
public $readonly = true;
public $ready = false;
public $group_id = 0;
public $coltypes = array();
/** private properties */
protected $conn;
protected $prop = array();
protected $fieldmap = array();
protected $sub_filter;
protected $filter = '';
protected $result = null;
protected $ldap_result = null;
protected $mail_domain = '';
protected $debug = false;
private $base_dn = '';
private $groups_base_dn = '';
private $group_url = null;
private $cache;
private $vlv_active = false;
private $vlv_count = 0;
/**
* Object constructor
*
* @param array $p LDAP connection properties
* @param boolean $debug Enables debug mode
* @param string $mail_domain Current user mail domain name
*/
function __construct($p, $debug = false, $mail_domain = null)
{
$this->prop = $p;
if (isset($p['searchonly']))
$this->searchonly = $p['searchonly'];
// check if groups are configured
if (is_array($p['groups']) && count($p['groups'])) {
$this->groups = true;
// set member field
if (!empty($p['groups']['member_attr']))
$this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
else if (empty($p['member_attr']))
$this->prop['member_attr'] = 'member';
// set default name attribute to cn
if (empty($this->prop['groups']['name_attr']))
$this->prop['groups']['name_attr'] = 'cn';
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
}
// fieldmap property is given
if (is_array($p['fieldmap'])) {
foreach ($p['fieldmap'] as $rf => $lf)
$this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
}
else if (!empty($p)) {
// read deprecated *_field properties to remain backwards compatible
foreach ($p as $prop => $value)
if (preg_match('/^(.+)_field$/', $prop, $matches))
$this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
}
// use fieldmap to advertise supported coltypes to the application
foreach ($this->fieldmap as $colv => $lfv) {
list($col, $type) = explode(':', $colv);
list($lf, $limit, $delim) = explode(':', $lfv);
if ($limit == '*') $limit = null;
else $limit = max(1, intval($limit));
if (!is_array($this->coltypes[$col])) {
$subtypes = $type ? array($type) : null;
$this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf));
}
elseif ($type) {
$this->coltypes[$col]['subtypes'][] = $type;
$this->coltypes[$col]['attributes'][] = $lf;
$this->coltypes[$col]['limit'] += $limit;
}
if ($delim)
$this->coltypes[$col]['serialized'][$type] = $delim;
$this->fieldmap[$colv] = $lf;
}
// support for composite address
if ($this->coltypes['street'] && $this->coltypes['locality']) {
$this->coltypes['address'] = array(
'limit' => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']),
'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']),
'childs' => array(),
) + (array)$this->coltypes['address'];
foreach (array('street','locality','zipcode','region','country') as $childcol) {
if ($this->coltypes[$childcol]) {
$this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
unset($this->coltypes[$childcol]); // remove address child col from global coltypes list
}
}
// at least one address type must be specified
if (empty($this->coltypes['address']['subtypes'])) {
$this->coltypes['address']['subtypes'] = array('home');
}
}
else if ($this->coltypes['address']) {
$this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40);
// 'serialized' means the UI has to present a composite address field
if ($this->coltypes['address']['serialized']) {
$childprop = array('type' => 'text');
$this->coltypes['address']['type'] = 'composite';
$this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop);
}
}
// make sure 'required_fields' is an array
if (!is_array($this->prop['required_fields'])) {
$this->prop['required_fields'] = (array) $this->prop['required_fields'];
}
// make sure LDAP_rdn field is required
- if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields']) && !in_array($this->prop['LDAP_rdn'], array_keys($this->prop['autovalues']))) {
+ if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])
+ && !in_array($this->prop['LDAP_rdn'], array_keys((array)$this->prop['autovalues']))) {
$this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
}
foreach ($this->prop['required_fields'] as $key => $val) {
$this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
}
// Build sub_fields filter
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
$this->sub_filter = '';
foreach ($this->prop['sub_fields'] as $attr => $class) {
if (!empty($class)) {
$class = is_array($class) ? array_pop($class) : $class;
$this->sub_filter .= '(objectClass=' . $class . ')';
}
}
if (count($this->prop['sub_fields']) > 1) {
$this->sub_filter = '(|' . $this->sub_filter . ')';
}
}
$this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
$this->debug = $debug;
$this->mail_domain = $mail_domain;
// initialize cache
$rcube = rcube::get_instance();
$this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
$this->_connect();
}
/**
* Establish a connection to the LDAP server
*/
private function _connect()
{
$rcube = rcube::get_instance();
if (!function_exists('ldap_connect'))
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "No ldap support in this installation of PHP"),
true, true);
if (is_resource($this->conn))
return true;
if (!is_array($this->prop['hosts']))
$this->prop['hosts'] = array($this->prop['hosts']);
if (empty($this->prop['ldap_version']))
$this->prop['ldap_version'] = 3;
foreach ($this->prop['hosts'] as $host)
{
$host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
$hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
$this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
if ($lc = @ldap_connect($host, $this->prop['port']))
{
if ($this->prop['use_tls'] === true)
if (!ldap_start_tls($lc))
continue;
$this->_debug("S: OK");
ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
$this->prop['host'] = $host;
$this->conn = $lc;
if (isset($this->prop['referrals']))
ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
break;
}
$this->_debug("S: NOT OK");
}
// See if the directory is writeable.
if ($this->prop['writable']) {
$this->readonly = false;
}
if (!is_resource($this->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
return false;
}
$bind_pass = $this->prop['bind_pass'];
$bind_user = $this->prop['bind_user'];
$bind_dn = $this->prop['bind_dn'];
$this->base_dn = $this->prop['base_dn'];
$this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
$this->prop['groups']['base_dn'] : $this->base_dn;
// User specific access, generate the proper values to use.
if ($this->prop['user_specific']) {
// No password set, use the session password
if (empty($bind_pass)) {
$bind_pass = $rcube->decrypt($_SESSION['password']);
}
// Get the pieces needed for variable replacement.
if ($fu = $rcube->get_user_name())
list($u, $d) = explode('@', $fu);
else
$d = $this->mail_domain;
$dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
$this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
}
// Search for the dn to use to authenticate
$this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
$this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
$this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
$res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
if ($res) {
if (($entry = ldap_first_entry($this->conn, $res))
&& ($bind_dn = ldap_get_dn($this->conn, $entry))
) {
$this->_debug("S: search returned dn: $bind_dn");
$dn = ldap_explode_dn($bind_dn, 1);
$replaces['%dn'] = $dn[0];
}
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
// DN not found
if (empty($replaces['%dn'])) {
if (!empty($this->prop['search_dn_default']))
$replaces['%dn'] = $this->prop['search_dn_default'];
else {
rcube::raise_error(array(
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
return false;
}
}
}
// Replace the bind_dn and base_dn variables.
$bind_dn = strtr($bind_dn, $replaces);
$this->base_dn = strtr($this->base_dn, $replaces);
$this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
if (empty($bind_user)) {
$bind_user = $u;
}
}
if (empty($bind_pass)) {
$this->ready = true;
}
else {
if (!empty($bind_dn)) {
$this->ready = $this->bind($bind_dn, $bind_pass);
}
else if (!empty($this->prop['auth_cid'])) {
$this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
}
else {
$this->ready = $this->sasl_bind($bind_user, $bind_pass);
}
}
return $this->ready;
}
/**
* Bind connection with (SASL-) user and password
*
* @param string $authc Authentication user
* @param string $pass Bind password
* @param string $authz Autorization user
*
* @return boolean True on success, False on error
*/
public function sasl_bind($authc, $pass, $authz=null)
{
if (!$this->conn) {
return false;
}
if (!function_exists('ldap_sasl_bind')) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Unable to bind: ldap_sasl_bind() not exists"),
true, true);
}
if (!empty($authz)) {
$authz = 'u:' . $authz;
}
if (!empty($this->prop['auth_method'])) {
$method = $this->prop['auth_method'];
}
else {
$method = 'DIGEST-MD5';
}
$this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
$this->_debug("S: OK");
return true;
}
$this->_debug("S: ".ldap_error($this->conn));
rcube::raise_error(array(
'code' => ldap_errno($this->conn), 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
true);
return false;
}
/**
* Bind connection with DN and password
*
* @param string Bind DN
* @param string Bind password
*
* @return boolean True on success, False on error
*/
public function bind($dn, $pass)
{
if (!$this->conn) {
return false;
}
$this->_debug("C: Bind [dn: $dn] [pass: $pass]");
if (@ldap_bind($this->conn, $dn, $pass)) {
$this->_debug("S: OK");
return true;
}
$this->_debug("S: ".ldap_error($this->conn));
rcube::raise_error(array(
'code' => ldap_errno($this->conn), 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
true);
return false;
}
/**
* Close connection to LDAP server
*/
function close()
{
if ($this->conn)
{
$this->_debug("C: Close");
ldap_unbind($this->conn);
$this->conn = null;
}
}
/**
* Returns address book name
*
* @return string Address book name
*/
function get_name()
{
return $this->prop['name'];
}
/**
* Set internal sort settings
*
* @param string $sort_col Sort column
* @param string $sort_order Sort order
*/
function set_sort_order($sort_col, $sort_order = null)
{
if ($this->coltypes[$sort_col]['attributes'])
$this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
}
/**
* Save a search string for future listings
*
* @param string $filter Filter string
*/
function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
function get_search_set()
{
return $this->filter;
}
/**
* Reset all saved results and search parameters
*/
function reset()
{
$this->result = null;
$this->ldap_result = null;
$this->filter = '';
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records
*
* @return array Indexed list of contact records, each a hash array
*/
function list_records($cols=null, $subset=0)
{
if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
{
$this->result = new rcube_result_set(0);
$this->result->searchonly = true;
return $this->result;
}
// fetch group members recursively
if ($this->group_id && $this->group_data['dn'])
{
$entries = $this->list_group_members($this->group_data['dn']);
// make list of entries unique and sort it
$seen = array();
foreach ($entries as $i => $rec) {
if ($seen[$rec['dn']]++)
unset($entries[$i]);
}
usort($entries, array($this, '_entry_sort_cmp'));
$entries['count'] = count($entries);
$this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
}
else
{
// add general filter to query
if (!empty($this->prop['filter']) && empty($this->filter))
$this->set_search_set($this->prop['filter']);
// exec LDAP search if no result resource is stored
if ($this->conn && !$this->ldap_result)
$this->_exec_search();
// count contacts for this user
$this->result = $this->count();
// we have a search result resource
if ($this->ldap_result && $this->result->count > 0)
{
// sorting still on the ldap server
if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
// get all entries from the ldap server
$entries = ldap_get_entries($this->conn, $this->ldap_result);
}
} // end else
// start and end of the page
$start_row = $this->vlv_active ? 0 : $this->result->first;
$start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
$last_row = $this->result->first + $this->page_size;
$last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
// filter entries for this page
for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
$this->result->add($this->_ldap2result($entries[$i]));
return $this->result;
}
/**
* Get all members of the given group
*
* @param string Group DN
* @param array Group entries (if called recursively)
* @return array Accumulated group members
*/
function list_group_members($dn, $count = false, $entries = null)
{
$group_members = array();
// fetch group object
if (empty($entries)) {
$result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
if ($result === false)
{
$this->_debug("S: ".ldap_error($this->conn));
return $group_members;
}
$entries = @ldap_get_entries($this->conn, $result);
}
for ($i=0; $i < $entries['count']; $i++)
{
$entry = $entries[$i];
if (empty($entry['objectclass']))
continue;
foreach ((array)$entry['objectclass'] as $objectclass)
{
switch (strtolower($objectclass)) {
case "group":
case "groupofnames":
case "kolabgroupofnames":
$group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
break;
case "groupofuniquenames":
case "kolabgroupofuniquenames":
$group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
break;
case "groupofurls":
$group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
break;
}
}
if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
break;
}
return array_filter($group_members);
}
/**
* Fetch members of the given group entry from server
*
* @param string Group DN
* @param array Group entry
* @param string Member attribute to use
* @return array Accumulated group members
*/
private function _list_group_members($dn, $entry, $attr, $count)
{
// Use the member attributes to return an array of member ldap objects
// NOTE that the member attribute is supposed to contain a DN
$group_members = array();
if (empty($entry[$attr]))
return $group_members;
// read these attributes for all members
$attrib = $count ? array('dn') : array_values($this->fieldmap);
$attrib[] = 'objectClass';
$attrib[] = 'member';
$attrib[] = 'uniqueMember';
$attrib[] = 'memberURL';
for ($i=0; $i < $entry[$attr]['count']; $i++)
{
if (empty($entry[$attr][$i]))
continue;
$result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
$attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
$members = @ldap_get_entries($this->conn, $result);
if ($members == false)
{
$this->_debug("S: ".ldap_error($this->conn));
$members = array();
}
// for nested groups, call recursively
$nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
unset($members['count']);
$group_members = array_merge($group_members, array_filter($members), $nested_group_members);
}
return $group_members;
}
/**
* List members of group class groupOfUrls
*
* @param string Group DN
* @param array Group entry
* @param boolean True if only used for counting
* @return array Accumulated group members
*/
private function _list_group_memberurl($dn, $entry, $count)
{
$group_members = array();
for ($i=0; $i < $entry['memberurl']['count']; $i++)
{
// extract components from url
if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
continue;
// add search filter if any
$filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
$func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
$attrib = $count ? array('dn') : array_values($this->fieldmap);
if ($result = @$func($this->conn, $m[1], $filter,
$attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
) {
$this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
}
else {
$this->_debug("S: ".ldap_error($this->conn));
return $group_members;
}
$entries = @ldap_get_entries($this->conn, $result);
for ($j = 0; $j < $entries['count']; $j++)
{
if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
$group_members = array_merge($group_members, $nested_group_members);
else
$group_members[] = $entries[$j];
}
}
return $group_members;
}
/**
* Callback for sorting entries
*/
function _entry_sort_cmp($a, $b)
{
return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
}
/**
* Search contacts
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount (Not used)
* @param array $required List of fields that cannot be empty
*
* @return array Indexed list of contact records and 'count' value
*/
function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
$mode = intval($mode);
// special treatment for ID-based search
if ($fields == 'ID' || $fields == $this->primary_key)
{
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id)
{
if ($rec = $this->get_record($id, true))
{
$result->add($rec);
$result->count++;
}
}
return $result;
}
// use VLV pseudo-search for autocompletion
$rcube = rcube::get_instance();
$list_fields = $rcube->config->get('contactlist_fields');
if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields))
{
// add general filter to query
if (!empty($this->prop['filter']) && empty($this->filter))
$this->set_search_set($this->prop['filter']);
// set VLV controls with encoded search string
$this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
$function = $this->_scope2func($this->prop['scope']);
$this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
$this->result = new rcube_result_set(0);
if (!$this->ldap_result) {
$this->_debug("S: ".ldap_error($this->conn));
return $this->result;
}
$this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
// get all entries of this page and post-filter those that really match the query
$search = mb_strtolower($value);
$entries = ldap_get_entries($this->conn, $this->ldap_result);
for ($i = 0; $i < $entries['count']; $i++) {
$rec = $this->_ldap2result($entries[$i]);
foreach ($fields as $f) {
foreach ((array)$rec[$f] as $val) {
$val = mb_strtolower($val);
switch ($mode) {
case 1:
$got = ($val == $search);
break;
case 2:
$got = ($search == substr($val, 0, strlen($search)));
break;
default:
$got = (strpos($val, $search) !== false);
break;
}
if ($got) {
$this->result->add($rec);
$this->result->count++;
break 2;
}
}
}
}
return $this->result;
}
// use AND operator for advanced searches
$filter = is_array($value) ? '(&' : '(|';
// set wildcards
$wp = $ws = '';
if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
$ws = '*';
if (!$mode) {
$wp = '*';
}
}
if ($fields == '*')
{
// search_fields are required for fulltext search
if (empty($this->prop['search_fields']))
{
$this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
$this->result = new rcube_result_set();
return $this->result;
}
if (is_array($this->prop['search_fields']))
{
foreach ($this->prop['search_fields'] as $field) {
$filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
}
}
}
else
{
foreach ((array)$fields as $idx => $field) {
$val = is_array($value) ? $value[$idx] : $value;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
$filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
}
}
$filter .= ')';
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$req_filter .= '(|';
foreach ($attrs as $f)
$req_filter .= "($f=*)";
if (count($attrs) > 1)
$req_filter .= ')';
}
}
if (!empty($req_filter))
$filter = '(&' . $req_filter . $filter . ')';
// avoid double-wildcard if $value is empty
$filter = preg_replace('/\*+/', '*', $filter);
// add general filter to query
if (!empty($this->prop['filter']))
$filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
// set filter string and execute search
$this->set_search_set($filter);
$this->_exec_search();
if ($select)
$this->list_records();
else
$this->result = $this->count();
return $this->result;
}
/**
* Count number of available contacts in database
*
* @return object rcube_result_set Resultset with values for 'count' and 'first'
*/
function count()
{
$count = 0;
if ($this->conn && $this->ldap_result) {
$count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
}
else if ($this->group_id && $this->group_data['dn']) {
$count = count($this->list_group_members($this->group_data['dn'], true));
}
else if ($this->conn) {
// We have a connection but no result set, attempt to get one.
if (empty($this->filter)) {
// The filter is not set, set it.
$this->filter = $this->prop['filter'];
}
$count = (int) $this->_exec_search(true);
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return object rcube_result_set Current resultset or NULL if nothing selected yet
*/
function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed Record identifier
* @param boolean Return as associative array
*
* @return mixed Hash array or rcube_result_set with all record fields
*/
function get_record($dn, $assoc=false)
{
$res = $this->result = null;
if ($this->conn && $dn)
{
$dn = self::dn_decode($dn);
$this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
$this->_debug("S: OK");
$entry = ldap_first_entry($this->conn, $ldap_result);
if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
$rec = array_change_key_case($rec, CASE_LOWER);
}
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
foreach ($entries as $entry) {
$lrec = array_change_key_case($entry, CASE_LOWER);
$rec = array_merge($lrec, $rec);
}
}
}
if (!empty($rec)) {
// Add in the dn for the entry.
$rec['dn'] = $dn;
$res = $this->_ldap2result($rec);
$this->result = new rcube_result_set();
$this->result->add($res);
}
}
return $assoc ? $res : $this->result;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Assoziative array with data to save
* @param boolean Try to fix/complete record automatically
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
if (!parent::validate($save_data, $autofix)) {
return false;
}
// check for name input
if (empty($save_data['name'])) {
$this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
return false;
}
// Verify that the required fields are set.
$missing = null;
$ldap_data = $this->_map_data($save_data);
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
$missing[$fld] = 1;
}
}
if ($missing) {
// try to complete record automatically
if ($autofix) {
$sn_field = $this->fieldmap['surname'];
$fn_field = $this->fieldmap['firstname'];
$mail_field = $this->fieldmap['email'];
// try to extract surname and firstname from displayname
$reverse_map = array_flip($this->fieldmap);
$name_parts = preg_split('/[\s,.]+/', $save_data['name']);
if ($sn_field && $missing[$sn_field]) {
$save_data['surname'] = array_pop($name_parts);
unset($missing[$sn_field]);
}
if ($fn_field && $missing[$fn_field]) {
$save_data['firstname'] = array_shift($name_parts);
unset($missing[$fn_field]);
}
// try to fix missing e-mail, very often on import
// from vCard we have email:other only defined
if ($mail_field && $missing[$mail_field]) {
$emails = $this->get_col_values('email', $save_data, true);
if (!empty($emails) && ($email = array_shift($emails))) {
$save_data['email'] = $email;
unset($missing[$mail_field]);
}
}
}
// TODO: generate message saying which fields are missing
if (!empty($missing)) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
}
return true;
}
/**
* Create a new contact record
*
* @param array Hash array with save data
*
* @return encoded record ID on success, False on error
*/
function insert($save_cols)
{
// Map out the column names to their LDAP ones to build the new entry.
$newentry = $this->_map_data($save_cols);
$newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
// add automatically generated attributes
$this->add_autovalues($newentry);
// Verify that the required fields are set.
$missing = null;
foreach ($this->prop['required_fields'] as $fld) {
if (!isset($newentry[$fld])) {
$missing[] = $fld;
}
}
// abort process if requiered fields are missing
// TODO: generate message saying which fields are missing
if ($missing) {
$this->set_error(self::ERROR_VALIDATE, 'formincomplete');
return false;
}
// Build the new entries DN.
$dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
// Remove attributes that need to be added separately (child objects)
$xfields = array();
if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
foreach ($this->prop['sub_fields'] as $xf => $xclass) {
if (!empty($newentry[$xf])) {
$xfields[$xf] = $newentry[$xf];
unset($newentry[$xf]);
}
}
}
if (!$this->ldap_add($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
$xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
$this->ldap_add($xdn, $xf);
}
$dn = self::dn_encode($dn);
// add new contact to the selected group
if ($this->group_id)
$this->add_to_group($this->group_id, $dn);
return $dn;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Hash array with save data
*
* @return boolean True on success, False on error
*/
function update($id, $save_cols)
{
$record = $this->get_record($id, true);
$newdata = array();
$replacedata = array();
$deletedata = array();
$subdata = array();
$subdeldata = array();
$subnewdata = array();
$ldap_data = $this->_map_data($save_cols);
$old_data = $record['_raw_attrib'];
// special handling of photo col
if ($photo_fld = $this->fieldmap['photo']) {
// undefined means keep old photo
if (!array_key_exists('photo', $save_cols)) {
$ldap_data[$photo_fld] = $record['photo'];
}
}
foreach ($this->fieldmap as $col => $fld) {
if ($fld) {
$val = $ldap_data[$fld];
$old = $old_data[$fld];
// remove empty array values
if (is_array($val))
$val = array_filter($val);
// $this->_map_data() result and _raw_attrib use different format
// make sure comparing array with one element with a string works as expected
if (is_array($old) && count($old) == 1 && !is_array($val)) {
$old = array_pop($old);
}
if (is_array($val) && count($val) == 1 && !is_array($old)) {
$val = array_pop($val);
}
// Subentries must be handled separately
if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
if ($old != $val) {
if ($old !== null) {
$subdeldata[$fld] = $old;
}
if ($val) {
$subnewdata[$fld] = $val;
}
}
else if ($old !== null) {
$subdata[$fld] = $old;
}
continue;
}
// The field does exist compare it to the ldap record.
if ($old != $val) {
// Changed, but find out how.
if ($old === null) {
// Field was not set prior, need to add it.
$newdata[$fld] = $val;
}
else if ($val == '') {
// Field supplied is empty, verify that it is not required.
if (!in_array($fld, $this->prop['required_fields'])) {
// ...It is not, safe to clear.
// #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
// jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
$deletedata[$fld] = array();
//$deletedata[$fld] = $old_data[$fld];
}
}
else {
// The data was modified, save it out.
$replacedata[$fld] = $val;
}
} // end if
} // end if
} // end foreach
// console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
$dn = self::dn_decode($id);
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
if (!$this->ldap_mod_del($dn, $deletedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
if (!empty($replacedata)) {
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
.$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->base_dn;
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
.$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
if (!$this->ldap_mod_replace($dn, $replacedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
} // end if
// RDN change, we need to remove all sub-entries
if (!empty($newrdn)) {
$subdeldata = array_merge($subdeldata, $subdata);
$subnewdata = array_merge($subnewdata, $subdata);
}
// remove sub-entries
if (!empty($subdeldata)) {
foreach ($subdeldata as $fld => $val) {
$subdn = $fld.'='.$this->_quote_string($val).','.$dn;
if (!$this->ldap_delete($subdn)) {
return false;
}
}
}
if (!empty($newdata)) {
// Add the fields.
if (!$this->ldap_mod_add($dn, $newdata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
} // end if
// Handle RDN change
if (!empty($newrdn)) {
if (!$this->ldap_rename($dn, $newrdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$dn = self::dn_encode($dn);
$newdn = self::dn_encode($newdn);
// change the group membership of the contact
if ($this->groups) {
$group_ids = $this->get_record_groups($dn);
foreach ($group_ids as $group_id)
{
$this->remove_from_group($group_id, $dn);
$this->add_to_group($group_id, $newdn);
}
}
$dn = self::dn_decode($newdn);
}
// add sub-entries
if (!empty($subnewdata)) {
foreach ($subnewdata as $fld => $val) {
$subdn = $fld.'='.$this->_quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
$this->ldap_add($subdn, $xf);
}
}
return $newdn ? $newdn : true;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (unsupported)
*
* @return boolean True on success, False on error
*/
function delete($ids, $force=true)
{
if (!is_array($ids)) {
// Not an array, break apart the encoded DNs.
$ids = explode(',', $ids);
} // end if
foreach ($ids as $id) {
$dn = self::dn_decode($id);
// Need to delete all sub-entries first
if ($this->sub_filter) {
if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
if (!$this->ldap_delete($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
}
}
}
// Delete the record.
if (!$this->ldap_delete($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
// remove contact from all groups where he was member
if ($this->groups) {
$dn = self::dn_encode($dn);
$group_ids = $this->get_record_groups($dn);
foreach ($group_ids as $group_id) {
$this->remove_from_group($group_id, $dn);
}
}
} // end foreach
return count($ids);
}
/**
* Remove all contact records
*/
function delete_all()
{
//searching for contact entries
$dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
$dn_list[$idx] = self::dn_encode($entry['dn']);
}
$this->delete($dn_list);
}
}
/**
* Generate missing attributes as configured
*
* @param array LDAP record attributes
*/
protected function add_autovalues(&$attrs)
{
$attrvals = array();
foreach ($attrs as $k => $v) {
$attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v;
}
foreach ((array)$this->prop['autovalues'] as $lf => $templ) {
if (empty($attrs[$lf])) {
// replace {attr} placeholders with concrete attribute values
$templ = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
if (strpos($templ, '(') !== false)
$attrs[$lf] = eval("return ($templ);");
else
$attrs[$lf] = $templ;
}
}
}
/**
* Execute the LDAP search based on the stored credentials
*/
private function _exec_search($count = false)
{
if ($this->ready)
{
$filter = $this->filter ? $this->filter : '(objectclass=*)';
$function = $this->_scope2func($this->prop['scope'], $ns_function);
$this->_debug("C: Search [$filter][dn: $this->base_dn]");
// when using VLV, we get the total count by...
if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
// ...either reading numSubOrdinates attribute
if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
$counts = ldap_get_entries($this->conn, $result_count);
for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
$this->vlv_count += $counts[$j]['numsubordinates'][0];
$this->_debug("D: total numsubordinates = " . $this->vlv_count);
}
else if (!function_exists('ldap_parse_virtuallist_control')) // ...or by fetching all records dn and count them
$this->vlv_count = $this->_exec_search(true);
$this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
}
// only fetch dn for count (should keep the payload low)
$attrs = $count ? array('dn') : array_values($this->fieldmap);
if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
$attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
) {
// when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
if (ldap_parse_result($this->conn, $this->ldap_result,
$errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
) {
ldap_parse_virtuallist_control($this->conn, $serverctrls,
$last_offset, $this->vlv_count, $vresult);
$this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
}
else {
$this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
}
}
$entries_count = ldap_count_entries($this->conn, $this->ldap_result);
$this->_debug("S: $entries_count record(s)");
return $count ? $entries_count : true;
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
}
return false;
}
/**
* Choose the right PHP function according to scope property
*/
private function _scope2func($scope, &$ns_function = null)
{
switch ($scope) {
case 'sub':
$function = $ns_function = 'ldap_search';
break;
case 'base':
$function = $ns_function = 'ldap_read';
break;
default:
$function = 'ldap_list';
$ns_function = 'ldap_read';
break;
}
return $function;
}
/**
* Set server controls for Virtual List View (paginated listing)
*/
private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
{
$sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort']));
$vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
$sort = (array)$prop['sort'];
$this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
. " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
$this->_debug("S: ".ldap_error($this->conn));
$this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
return false;
}
return true;
}
/**
* Converts LDAP entry into an array
*/
private function _ldap2result($rec)
{
$out = array();
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
foreach ($this->fieldmap as $rf => $lf)
{
for ($i=0; $i < $rec[$lf]['count']; $i++) {
if (!($value = $rec[$lf][$i]))
continue;
list($col, $subtype) = explode(':', $rf);
$out['_raw_attrib'][$lf][$i] = $value;
if ($col == 'email' && $this->mail_domain && !strpos($value, '@'))
$out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
else if (in_array($col, array('street','zipcode','locality','country','region')))
$out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
else if ($col == 'address' && strpos($value, '$') !== false) // address data is represented as string separated with $
list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $value);
else if ($rec[$lf]['count'] > 1)
$out[$rf][] = $value;
else
$out[$rf] = $value;
}
// Make sure name fields aren't arrays (#1488108)
if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
$out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
}
}
return $out;
}
/**
* Return LDAP attribute(s) for the given field
*/
private function _map_field($field)
{
return (array)$this->coltypes[$field]['attributes'];
}
/**
* Convert a record data set into LDAP field attributes
*/
private function _map_data($save_cols)
{
// flatten composite fields first
foreach ($this->coltypes as $col => $colprop) {
if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
foreach ($values as $subtype => $childs) {
$subtype = $subtype ? ':'.$subtype : '';
foreach ($childs as $i => $child_values) {
foreach ((array)$child_values as $childcol => $value) {
$save_cols[$childcol.$subtype][$i] = $value;
}
}
}
}
// if addresses are to be saved as serialized string, do so
if (is_array($colprop['serialized'])) {
foreach ($colprop['serialized'] as $subtype => $delim) {
$key = $col.':'.$subtype;
foreach ((array)$save_cols[$key] as $i => $val)
$save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country']));
}
}
}
$ldap_data = array();
foreach ($this->fieldmap as $rf => $fld) {
$val = $save_cols[$rf];
// check for value in base field (eg.g email instead of email:foo)
list($col, $subtype) = explode(':', $rf);
if (!$val && !empty($save_cols[$col])) {
$val = $save_cols[$col];
unset($save_cols[$col]); // only use this value once
}
else if (!$val && !$subtype) { // extract values from subtype cols
$val = $this->get_col_values($col, $save_cols, true);
}
if (is_array($val))
$val = array_filter($val); // remove empty entries
if ($fld && $val) {
// The field does exist, add it to the entry.
$ldap_data[$fld] = $val;
}
}
return $ldap_data;
}
/**
* Returns unified attribute name (resolving aliases)
*/
private static function _attr_name($namev)
{
// list of known attribute aliases
static $aliases = array(
'gn' => 'givenname',
'rfc822mailbox' => 'email',
'userid' => 'uid',
'emailaddress' => 'email',
'pkcs9email' => 'email',
);
list($name, $limit) = explode(':', $namev, 2);
$suffix = $limit ? ':'.$limit : '';
return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
}
/**
* Prints debug info to the log
*/
private function _debug($str)
{
if ($this->debug) {
rcube::write_log('ldap', $str);
}
}
/**
* Activate/deactivate debug mode
*
* @param boolean $dbg True if LDAP commands should be logged
* @access public
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
}
/**
* Quotes attribute value string
*
* @param string $str Attribute value
* @param bool $dn True if the attribute is a DN
*
* @return string Quoted string
*/
private static function _quote_string($str, $dn=false)
{
// take firt entry if array given
if (is_array($str))
$str = reset($str);
if ($dn)
$replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
'>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
else
$replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
'/'=>'\2f');
return strtr($str, $replace);
}
/**
* Setter for the current group
* (empty, has to be re-implemented by extending class)
*/
function set_group($group_id)
{
if ($group_id)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$this->group_id = $group_id;
$this->group_data = $group_cache[$group_id];
}
else
{
$this->group_id = 0;
$this->group_data = null;
}
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
if (!$this->groups)
return array();
// use cached list for searching
$this->cache->expunge();
if (!$search || ($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$groups = array();
if ($search) {
$search = mb_strtolower($search);
foreach ($group_cache as $group) {
if (strpos(mb_strtolower($group['name']), $search) !== false)
$groups[] = $group;
}
}
else
$groups = $group_cache;
return array_values($groups);
}
/**
* Fetch groups from server
*/
private function _fetch_groups($vlv_page = 0)
{
$base_dn = $this->groups_base_dn;
$filter = $this->prop['groups']['filter'];
$name_attr = $this->prop['groups']['name_attr'];
$email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
$sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
$sort_attr = $sort_attrs[0];
$this->_debug("C: Search [$filter][dn: $base_dn]");
// use vlv to list groups
if ($this->prop['groups']['vlv']) {
$page_size = 200;
if (!$this->prop['groups']['sort'])
$this->prop['groups']['sort'] = $sort_attrs;
$vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
}
$function = $this->_scope2func($this->prop['groups']['scope'], $ns_function);
$res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
if ($res === false)
{
$this->_debug("S: ".ldap_error($this->conn));
return array();
}
$ldap_data = ldap_get_entries($this->conn, $res);
$this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
$groups = array();
$group_sortnames = array();
$group_count = $ldap_data["count"];
for ($i=0; $i < $group_count; $i++)
{
$group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
$group_id = self::dn_encode($group_name);
$groups[$group_id]['ID'] = $group_id;
$groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
$groups[$group_id]['name'] = $group_name;
$groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
// list email attributes of a group
for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
$groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
}
$group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
}
// recursive call can exit here
if ($vlv_page > 0)
return $groups;
// call recursively until we have fetched all groups
while ($vlv_active && $group_count == $page_size)
{
$next_page = $this->_fetch_groups(++$vlv_page);
$groups = array_merge($groups, $next_page);
$group_count = count($next_page);
}
// when using VLV the list of groups is already sorted
if (!$this->prop['groups']['vlv'])
array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
// cache this
$this->cache->set('groups', $groups);
return $groups;
}
/**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
* @return array Group properties as hash array
*/
function get_group($group_id)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$group_data = $group_cache[$group_id];
unset($group_data['dn'], $group_data['member_attr']);
return $group_data;
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($group_name)
{
$base_dn = $this->groups_base_dn;
$new_dn = "cn=$group_name,$base_dn";
$new_gid = self::dn_encode($group_name);
$member_attr = $this->get_group_member_attr();
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
$new_entry = array(
'objectClass' => $this->prop['groups']['object_classes'],
$name_attr => $group_name,
$member_attr => '',
);
if (!$this->ldap_add($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->cache->remove('groups');
return array('id' => $new_gid, 'name' => $group_name);
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($group_id)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$del_dn = "cn=$group_name,$base_dn";
if (!$this->ldap_delete($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->cache->remove('groups');
return true;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
* @return boolean New name on success, false if no data was changed
*/
function rename_group($group_id, $new_name, &$new_gid)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$old_dn = "cn=$group_name,$base_dn";
$new_rdn = "cn=$new_name";
$new_gid = self::dn_encode($new_name);
if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
$this->cache->remove('groups');
return $new_name;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($group_id, $contact_ids)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = "cn=$group_name,$base_dn";
$new_attrs = array();
foreach ($contact_ids as $id)
$new_attrs[$member_attr][] = self::dn_decode($id);
if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
$this->cache->remove('groups');
return count($new_attrs['member']);
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($group_id, $contact_ids)
{
if (($group_cache = $this->cache->get('groups')) === null)
$group_cache = $this->_fetch_groups();
$base_dn = $this->groups_base_dn;
$group_name = $group_cache[$group_id]['name'];
$member_attr = $group_cache[$group_id]['member_attr'];
$group_dn = "cn=$group_name,$base_dn";
$del_attrs = array();
foreach (explode(",", $contact_ids) as $id)
$del_attrs[$member_attr][] = self::dn_decode($id);
if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
$this->cache->remove('groups');
return count($del_attrs['member']);
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array List of assigned groups as ID=>Name pairs
* @since 0.5-beta
*/
function get_record_groups($contact_id)
{
if (!$this->groups)
return array();
$base_dn = $this->groups_base_dn;
$contact_dn = self::dn_decode($contact_id);
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
$member_attr = $this->get_group_member_attr();
$add_filter = '';
if ($member_attr != 'member' && $member_attr != 'uniqueMember')
$add_filter = "($member_attr=$contact_dn)";
$filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
$this->_debug("C: Search [$filter][dn: $base_dn]");
$res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
if ($res === false)
{
$this->_debug("S: ".ldap_error($this->conn));
return array();
}
$ldap_data = ldap_get_entries($this->conn, $res);
$this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
$groups = array();
for ($i=0; $i<$ldap_data["count"]; $i++)
{
$group_name = $ldap_data[$i][$name_attr][0];
$group_id = self::dn_encode($group_name);
$groups[$group_id] = $group_id;
}
return $groups;
}
/**
* Detects group member attribute name
*/
private function get_group_member_attr($object_classes = array())
{
if (empty($object_classes)) {
$object_classes = $this->prop['groups']['object_classes'];
}
if (!empty($object_classes)) {
foreach ((array)$object_classes as $oc) {
switch (strtolower($oc)) {
case 'group':
case 'groupofnames':
case 'kolabgroupofnames':
$member_attr = 'member';
break;
case 'groupofuniquenames':
case 'kolabgroupofuniquenames':
$member_attr = 'uniqueMember';
break;
}
}
}
if (!empty($member_attr)) {
return $member_attr;
}
if (!empty($this->prop['groups']['member_attr'])) {
return $this->prop['groups']['member_attr'];
}
return 'member';
}
/**
* Generate BER encoded string for Virtual List View option
*
* @param integer List offset (first record)
* @param integer Records per page
* @return string BER encoded option value
*/
private function _vlv_ber_encode($offset, $rpp, $search = '')
{
# this string is ber-encoded, php will prefix this value with:
# 04 (octet string) and 10 (length of 16 bytes)
# the code behind this string is broken down as follows:
# 30 = ber sequence with a length of 0e (14) bytes following
# 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
# 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
# a0 = type context-specific/constructed with a length of 06 (6) bytes following
# 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
# 02 = type integer with 2 bytes following (contentCount): 01 00
# whith a search string present:
# 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
# 81 indicates a user string is present where as a a0 indicates just a offset search
# 81 = type context-specific/constructed with a length of 06 (6) bytes following
# the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
# encoding of integer values (note: these values are in
# two-complement form so since offset will never be negative bit 8 of the
# leftmost octet should never by set to 1):
# 8.3.2: If the contents octets of an integer value encoding consist
# of more than one octet, then the bits of the first octet (rightmost) and bit 8
# of the second (to the left of first octet) octet:
# a) shall not all be ones; and
# b) shall not all be zero
if ($search)
{
$search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
$ber_val = self::_string2hex($search);
$str = self::_ber_addseq($ber_val, '81');
}
else
{
# construct the string from right to left
$str = "020100"; # contentCount
$ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format
// calculate octet length of $ber_val
$str = self::_ber_addseq($ber_val, '02') . $str;
// now compute length over $str
$str = self::_ber_addseq($str, 'a0');
}
// now tack on records per page
$str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
// now tack on sequence identifier and length
$str = self::_ber_addseq($str, '30');
return pack('H'.strlen($str), $str);
}
/**
* create ber encoding for sort control
*
* @param array List of cols to sort by
* @return string BER encoded option value
*/
private function _sort_ber_encode($sortcols)
{
$str = '';
foreach (array_reverse((array)$sortcols) as $col) {
$ber_val = self::_string2hex($col);
# 30 = ber sequence with a length of octet value
# 04 = octet string with a length of the ascii value
$oct = self::_ber_addseq($ber_val, '04');
$str = self::_ber_addseq($oct, '30') . $str;
}
// now tack on sequence identifier and length
$str = self::_ber_addseq($str, '30');
return pack('H'.strlen($str), $str);
}
/**
* Add BER sequence with correct length and the given identifier
*/
private static function _ber_addseq($str, $identifier)
{
$len = dechex(strlen($str)/2);
if (strlen($len) % 2 != 0)
$len = '0'.$len;
return $identifier . $len . $str;
}
/**
* Returns BER encoded integer value in hex format
*/
private static function _ber_encode_int($offset)
{
$val = dechex($offset);
$prefix = '';
// check if bit 8 of high byte is 1
if (preg_match('/^[89abcdef]/', $val))
$prefix = '00';
if (strlen($val)%2 != 0)
$prefix .= '0';
return $prefix . $val;
}
/**
* Returns ascii string encoded in hex
*/
private static function _string2hex($str)
{
$hex = '';
for ($i=0; $i < strlen($str); $i++)
$hex .= dechex(ord($str[$i]));
return $hex;
}
/**
* HTML-safe DN string encoding
*
* @param string $str DN string
*
* @return string Encoded HTML identifier string
*/
static function dn_encode($str)
{
// @TODO: to make output string shorter we could probably
// remove dc=* items from it
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
/**
* Decodes DN string encoded with _dn_encode()
*
* @param string $str Encoded HTML identifier string
*
* @return string DN string
*/
static function dn_decode($str)
{
$str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
return base64_decode($str);
}
/**
* Wrapper for ldap_add()
*/
protected function ldap_add($dn, $entry)
{
$this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
$res = ldap_add($this->conn, $dn, $entry);
if ($res === false) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_delete()
*/
protected function ldap_delete($dn)
{
$this->_debug("C: Delete [dn: $dn]");
$res = ldap_delete($this->conn, $dn);
if ($res === false) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_replace()
*/
protected function ldap_mod_replace($dn, $entry)
{
$this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
if (!ldap_mod_replace($this->conn, $dn, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_add()
*/
protected function ldap_mod_add($dn, $entry)
{
$this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
if (!ldap_mod_add($this->conn, $dn, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_mod_del()
*/
protected function ldap_mod_del($dn, $entry)
{
$this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
if (!ldap_mod_del($this->conn, $dn, $entry)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_rename()
*/
protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
{
$this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
$this->_debug("S: ".ldap_error($this->conn));
return false;
}
$this->_debug("S: OK");
return true;
}
/**
* Wrapper for ldap_list()
*/
protected function ldap_list($dn, $filter, $attrs = array(''))
{
$list = array();
$this->_debug("C: List [dn: $dn] [{$filter}]");
if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
$list = ldap_get_entries($this->conn, $result);
if ($list === false) {
$this->_debug("S: ".ldap_error($this->conn));
return array();
}
$count = $list['count'];
unset($list['count']);
$this->_debug("S: $count record(s)");
}
else {
$this->_debug("S: ".ldap_error($this->conn));
}
return $list;
}
}
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index 5e24a4311..01d95e059 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -1,1859 +1,1860 @@
<?php
/*
+-----------------------------------------------------------------------+
| program/steps/mail/func.inc |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2010, The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide webmail functionality and GUI objects |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
// setup some global vars used by mail steps
$SENT_MBOX = $RCMAIL->config->get('sent_mbox');
$DRAFTS_MBOX = $RCMAIL->config->get('drafts_mbox');
$SEARCH_MODS_DEFAULT = array(
'*' => array('subject'=>1, 'from'=>1),
$SENT_MBOX => array('subject'=>1, 'to'=>1),
$DRAFTS_MBOX => array('subject'=>1, 'to'=>1)
);
// always instantiate storage object (but not connect to server yet)
$RCMAIL->storage_init();
// set imap properties and session vars
if (strlen(trim($mbox = get_input_value('_mbox', RCUBE_INPUT_GPC, true))))
$RCMAIL->storage->set_folder(($_SESSION['mbox'] = $mbox));
else if ($RCMAIL->storage)
$_SESSION['mbox'] = $RCMAIL->storage->get_folder();
if (!empty($_GET['_page']))
$RCMAIL->storage->set_page(($_SESSION['page'] = intval($_GET['_page'])));
// set default sort col/order to session
if (!isset($_SESSION['sort_col']))
$_SESSION['sort_col'] = !empty($CONFIG['message_sort_col']) ? $CONFIG['message_sort_col'] : '';
if (!isset($_SESSION['sort_order']))
$_SESSION['sort_order'] = strtoupper($CONFIG['message_sort_order']) == 'ASC' ? 'ASC' : 'DESC';
// set threads mode
$a_threading = $RCMAIL->config->get('message_threading', array());
if (isset($_GET['_threads'])) {
if ($_GET['_threads'])
$a_threading[$_SESSION['mbox']] = true;
else
unset($a_threading[$_SESSION['mbox']]);
$RCMAIL->user->save_prefs(array('message_threading' => $a_threading));
}
$RCMAIL->storage->set_threading($a_threading[$_SESSION['mbox']]);
// set message set for search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'])
&& $_SESSION['search_request'] == $_REQUEST['_search']
) {
$RCMAIL->storage->set_search_set($_SESSION['search']);
$OUTPUT->set_env('search_request', $_REQUEST['_search']);
$OUTPUT->set_env('search_text', $_SESSION['last_text_search']);
}
// set main env variables, labels and page title
if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {
// connect to storage server and trigger error on failure
$RCMAIL->storage_connect();
$mbox_name = $RCMAIL->storage->get_folder();
if (empty($RCMAIL->action)) {
// initialize searching result if search_filter is used
if ($_SESSION['search_filter'] && $_SESSION['search_filter'] != 'ALL') {
$search_request = md5($mbox_name.$_SESSION['search_filter']);
$RCMAIL->storage->search($mbox_name, $_SESSION['search_filter'], RCMAIL_CHARSET, rcmail_sort_column());
$_SESSION['search'] = $RCMAIL->storage->get_search_set();
$_SESSION['search_request'] = $search_request;
$OUTPUT->set_env('search_request', $search_request);
}
$search_mods = $RCMAIL->config->get('search_mods', $SEARCH_MODS_DEFAULT);
$OUTPUT->set_env('search_mods', $search_mods);
}
$threading = (bool) $RCMAIL->storage->get_threading();
// set current mailbox and some other vars in client environment
$OUTPUT->set_env('mailbox', $mbox_name);
$OUTPUT->set_env('pagesize', $RCMAIL->storage->get_pagesize());
$OUTPUT->set_env('delimiter', $RCMAIL->storage->get_hierarchy_delimiter());
$OUTPUT->set_env('threading', $threading);
$OUTPUT->set_env('threads', $threading || $RCMAIL->storage->get_capability('THREAD'));
$OUTPUT->set_env('preview_pane_mark_read', $RCMAIL->config->get('preview_pane_mark_read', 0));
if ($RCMAIL->storage->get_capability('QUOTA')) {
$OUTPUT->set_env('quota', true);
}
foreach (array('delete_junk','flag_for_deletion','read_when_deleted','skip_deleted','display_next','message_extwin','compose_extwin','forward_attachment') as $prop) {
if ($CONFIG[$prop])
$OUTPUT->set_env($prop, true);
}
if ($CONFIG['trash_mbox'])
$OUTPUT->set_env('trash_mailbox', $CONFIG['trash_mbox']);
if ($CONFIG['drafts_mbox'])
$OUTPUT->set_env('drafts_mailbox', $CONFIG['drafts_mbox']);
if ($CONFIG['junk_mbox'])
$OUTPUT->set_env('junk_mailbox', $CONFIG['junk_mbox']);
if (!empty($_SESSION['browser_caps']))
$OUTPUT->set_env('browser_capabilities', $_SESSION['browser_caps']);
if (!$OUTPUT->ajax_call)
$OUTPUT->add_label('checkingmail', 'deletemessage', 'movemessagetotrash',
'movingmessage', 'copyingmessage', 'deletingmessage', 'markingmessage',
'copy', 'move', 'quota');
$OUTPUT->set_pagetitle(rcmail_localize_foldername($RCMAIL->storage->mod_folder($mbox_name)));
}
/**
* Returns 'to' if current folder is configured Sent or Drafts
* or their subfolders, otherwise returns 'from'.
*
* @return string Column name
*/
function rcmail_message_list_smart_column_name()
{
global $RCMAIL;
$delim = $RCMAIL->storage->get_hierarchy_delimiter();
$mbox = $RCMAIL->storage->get_folder();
$sent_mbox = $RCMAIL->config->get('sent_mbox');
$drafts_mbox = $RCMAIL->config->get('drafts_mbox');
if (strpos($mbox.$delim, $sent_mbox.$delim) === 0 || strpos($mbox.$delim, $drafts_mbox.$delim) === 0) {
return 'to';
}
return 'from';
}
/**
* Returns configured messages list sorting column name
* The name is context-sensitive, which means if sorting is set to 'fromto'
* it will return 'from' or 'to' according to current folder type.
*
* @return string Column name
*/
function rcmail_sort_column()
{
global $RCMAIL;
if (isset($_SESSION['sort_col'])) {
$column = $_SESSION['sort_col'];
}
else {
$column = $RCMAIL->config->get('message_sort_col');
}
// get name of smart From/To column in folder context
if ($column == 'fromto') {
$column = rcmail_message_list_smart_column_name();
}
return $column;
}
/**
* Returns configured message list sorting order
*
* @return string Sorting order (ASC|DESC)
*/
function rcmail_sort_order()
{
global $RCMAIL;
if (isset($_SESSION['sort_order'])) {
return $_SESSION['sort_order'];
}
return $RCMAIL->config->get('message_sort_order');
}
/**
* return the message list as HTML table
*/
function rcmail_message_list($attrib)
{
global $CONFIG, $OUTPUT;
// add some labels to client
$OUTPUT->add_label('from', 'to');
// add id to message list table if not specified
if (!strlen($attrib['id']))
$attrib['id'] = 'rcubemessagelist';
// define list of cols to be displayed based on parameter or config
if (empty($attrib['columns'])) {
$a_show_cols = is_array($CONFIG['list_cols']) ? $CONFIG['list_cols'] : array('subject');
$OUTPUT->set_env('col_movable', !in_array('list_cols', (array)$CONFIG['dont_override']));
}
else {
$a_show_cols = preg_split('/[\s,;]+/', strip_quotes($attrib['columns']));
$attrib['columns'] = $a_show_cols;
}
// save some variables for use in ajax list
$_SESSION['list_attrib'] = $attrib;
// make sure 'threads' and 'subject' columns are present
if (!in_array('subject', $a_show_cols))
array_unshift($a_show_cols, 'subject');
if (!in_array('threads', $a_show_cols))
array_unshift($a_show_cols, 'threads');
$skin_path = $_SESSION['skin_path'] = $CONFIG['skin_path'];
// set client env
$OUTPUT->add_gui_object('messagelist', $attrib['id']);
$OUTPUT->set_env('autoexpand_threads', intval($CONFIG['autoexpand_threads']));
$OUTPUT->set_env('sort_col', $_SESSION['sort_col']);
$OUTPUT->set_env('sort_order', $_SESSION['sort_order']);
$OUTPUT->set_env('messages', array());
$OUTPUT->set_env('coltypes', $a_show_cols);
$OUTPUT->include_script('list.js');
$thead = '';
foreach (rcmail_message_list_head($attrib, $a_show_cols) as $cell)
$thead .= html::tag('td', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
return html::tag('table',
$attrib,
html::tag('thead', null, html::tag('tr', null, $thead)) .
html::tag('tbody', null, ''),
array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary'));
}
/**
* return javascript commands to add rows to the message list
*/
function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null)
{
global $CONFIG, $RCMAIL, $OUTPUT;
if (empty($a_show_cols)) {
if (!empty($_SESSION['list_attrib']['columns']))
$a_show_cols = $_SESSION['list_attrib']['columns'];
else
$a_show_cols = is_array($CONFIG['list_cols']) ? $CONFIG['list_cols'] : array('subject');
}
else {
if (!is_array($a_show_cols))
$a_show_cols = preg_split('/[\s,;]+/', strip_quotes($a_show_cols));
$head_replace = true;
}
$mbox = $RCMAIL->storage->get_folder();
// make sure 'threads' and 'subject' columns are present
if (!in_array('subject', $a_show_cols))
array_unshift($a_show_cols, 'subject');
if (!in_array('threads', $a_show_cols))
array_unshift($a_show_cols, 'threads');
$_SESSION['list_attrib']['columns'] = $a_show_cols;
// Make sure there are no duplicated columns (#1486999)
$a_show_cols = array_unique($a_show_cols);
// Plugins may set header's list_cols/list_flags and other rcube_message_header variables
// and list columns
$plugin = $RCMAIL->plugins->exec_hook('messages_list',
array('messages' => $a_headers, 'cols' => $a_show_cols));
$a_show_cols = $plugin['cols'];
$a_headers = $plugin['messages'];
$thead = $head_replace ? rcmail_message_list_head($_SESSION['list_attrib'], $a_show_cols) : NULL;
// get name of smart From/To column in folder context
if (($f = array_search('fromto', $a_show_cols)) !== false) {
$smart_col = rcmail_message_list_smart_column_name();
}
$OUTPUT->command('set_message_coltypes', $a_show_cols, $thead, $smart_col);
if (empty($a_headers))
return;
// remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here
foreach (array('threads', 'attachment', 'flag', 'status', 'priority') as $col) {
if (($key = array_search($col, $a_show_cols)) !== FALSE)
unset($a_show_cols[$key]);
}
// loop through message headers
foreach ($a_headers as $n => $header) {
if (empty($header))
continue;
$a_msg_cols = array();
$a_msg_flags = array();
// format each col; similar as in rcmail_message_list()
foreach ($a_show_cols as $col) {
$col_name = $col == 'fromto' ? $smart_col : $col;
if (in_array($col_name, array('from', 'to', 'cc', 'replyto')))
$cont = rcmail_address_string($header->$col_name, 3, false, null, $header->charset);
else if ($col == 'subject') {
$cont = trim(rcube_mime::decode_header($header->$col, $header->charset));
if (!$cont) $cont = rcube_label('nosubject');
$cont = Q($cont);
}
else if ($col == 'size')
$cont = show_bytes($header->$col);
else if ($col == 'date')
$cont = format_date($header->date);
else
$cont = Q($header->$col);
$a_msg_cols[$col] = $cont;
}
$a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags));
if ($header->depth)
$a_msg_flags['depth'] = $header->depth;
else if ($header->has_children)
$roots[] = $header->uid;
if ($header->parent_uid)
$a_msg_flags['parent_uid'] = $header->parent_uid;
if ($header->has_children)
$a_msg_flags['has_children'] = $header->has_children;
if ($header->unread_children)
$a_msg_flags['unread_children'] = $header->unread_children;
if ($header->others['list-post'])
$a_msg_flags['ml'] = 1;
if ($header->priority)
$a_msg_flags['prio'] = (int) $header->priority;
$a_msg_flags['ctype'] = Q($header->ctype);
$a_msg_flags['mbox'] = $mbox;
// merge with plugin result (Deprecated, use $header->flags)
if (!empty($header->list_flags) && is_array($header->list_flags))
$a_msg_flags = array_merge($a_msg_flags, $header->list_flags);
if (!empty($header->list_cols) && is_array($header->list_cols))
$a_msg_cols = array_merge($a_msg_cols, $header->list_cols);
$OUTPUT->command('add_message_row',
$header->uid,
$a_msg_cols,
$a_msg_flags,
$insert_top);
}
if ($RCMAIL->storage->get_threading()) {
$OUTPUT->command('init_threads', (array) $roots, $mbox);
}
}
/*
* Creates <THEAD> for message list table
*/
function rcmail_message_list_head($attrib, $a_show_cols)
{
global $RCMAIL;
$skin_path = $_SESSION['skin_path'];
$image_tag = html::img(array('src' => "%s%s", 'alt' => "%s"));
// check to see if we have some settings for sorting
$sort_col = $_SESSION['sort_col'];
$sort_order = $_SESSION['sort_order'];
$dont_override = (array)$RCMAIL->config->get('dont_override');
$disabled_sort = in_array('message_sort_col', $dont_override);
$disabled_order = in_array('message_sort_order', $dont_override);
$RCMAIL->output->set_env('disabled_sort_col', $disabled_sort);
$RCMAIL->output->set_env('disabled_sort_order', $disabled_order);
// define sortable columns
if ($disabled_sort)
$a_sort_cols = $sort_col && !$disabled_order ? array($sort_col) : array();
else
$a_sort_cols = array('subject', 'date', 'from', 'to', 'fromto', 'size', 'cc');
if (!empty($attrib['optionsmenuicon'])) {
$onclick = 'return ' . JS_OBJECT_NAME . ".command('menu-open', 'messagelistmenu')";
if ($attrib['optionsmenuicon'] === true || $attrib['optionsmenuicon'] == 'true')
$list_menu = html::div(array('onclick' => $onclick, 'class' => 'listmenu',
'id' => 'listmenulink', 'title' => rcube_label('listoptions')));
else
$list_menu = html::a(array('href' => '#', 'onclick' => $onclick),
html::img(array('src' => $skin_path . $attrib['optionsmenuicon'],
'id' => 'listmenulink', 'title' => rcube_label('listoptions')))
);
}
else
$list_menu = '';
$cells = array();
// get name of smart From/To column in folder context
if (($f = array_search('fromto', $a_show_cols)) !== false) {
$smart_col = rcmail_message_list_smart_column_name();
}
foreach ($a_show_cols as $col) {
// get column name
switch ($col) {
case 'flag':
$col_name = '<span class="flagged">&nbsp;</span>';
break;
case 'attachment':
case 'priority':
case 'status':
$col_name = '<span class="' . $col .'">&nbsp;</span>';
break;
case 'threads':
$col_name = $list_menu;
break;
case 'fromto':
$col_name = Q(rcube_label($smart_col));
break;
default:
$col_name = Q(rcube_label($col));
}
// make sort links
if (in_array($col, $a_sort_cols))
$col_name = html::a(array('href'=>"./#sort", 'onclick' => 'return '.JS_OBJECT_NAME.".command('sort','".$col."',this)", 'title' => rcube_label('sortby')), $col_name);
else if ($col_name[0] != '<')
$col_name = '<span class="' . $col .'">' . $col_name . '</span>';
$sort_class = $col == $sort_col && !$disabled_order ? " sorted$sort_order" : '';
$class_name = $col.$sort_class;
// put it all together
$cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name);
}
return $cells;
}
/**
* return an HTML iframe for loading mail content
*/
function rcmail_messagecontent_frame($attrib)
{
global $OUTPUT, $RCMAIL;
if (empty($attrib['id']))
$attrib['id'] = 'rcmailcontentwindow';
$attrib['name'] = $attrib['id'];
if ($RCMAIL->config->get('preview_pane'))
$OUTPUT->set_env('contentframe', $attrib['id']);
$OUTPUT->set_env('blankpage', $attrib['src'] ? $OUTPUT->abs_url($attrib['src']) : 'program/resources/blank.gif');
return $OUTPUT->frame($attrib, true);
}
function rcmail_messagecount_display($attrib)
{
global $RCMAIL;
if (!$attrib['id'])
$attrib['id'] = 'rcmcountdisplay';
$RCMAIL->output->add_gui_object('countdisplay', $attrib['id']);
$content = $RCMAIL->action != 'show' ? rcmail_get_messagecount_text() : rcube_label('loading');
return html::span($attrib, $content);
}
function rcmail_get_messagecount_text($count=NULL, $page=NULL)
{
global $RCMAIL;
if ($page === NULL) {
$page = $RCMAIL->storage->get_page();
}
$page_size = $RCMAIL->storage->get_pagesize();
$start_msg = ($page-1) * $page_size + 1;
if ($count!==NULL)
$max = $count;
else if ($RCMAIL->action)
$max = $RCMAIL->storage->count(NULL, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL');
if ($max==0)
$out = rcube_label('mailboxempty');
else
$out = rcube_label(array('name' => $RCMAIL->storage->get_threading() ? 'threadsfromto' : 'messagesfromto',
'vars' => array('from' => $start_msg,
'to' => min($max, $start_msg + $page_size - 1),
'count' => $max)));
return Q($out);
}
function rcmail_mailbox_name_display($attrib)
{
global $RCMAIL;
if (!$attrib['id'])
$attrib['id'] = 'rcmmailboxname';
$RCMAIL->output->add_gui_object('mailboxname', $attrib['id']);
return html::span($attrib, rcmail_get_mailbox_name_text());
}
function rcmail_get_mailbox_name_text()
{
global $RCMAIL;
return rcmail_localize_foldername($RCMAIL->storage->get_folder());
}
function rcmail_send_unread_count($mbox_name, $force=false, $count=null, $mark='')
{
global $RCMAIL;
$old_unseen = rcmail_get_unseen_count($mbox_name);
if ($count === null)
$unseen = $RCMAIL->storage->count($mbox_name, 'UNSEEN', $force);
else
$unseen = $count;
if ($unseen != $old_unseen || ($mbox_name == 'INBOX'))
$RCMAIL->output->command('set_unread_count', $mbox_name, $unseen,
($mbox_name == 'INBOX'), $unseen && $mark ? $mark : '');
rcmail_set_unseen_count($mbox_name, $unseen);
return $unseen;
}
function rcmail_set_unseen_count($mbox_name, $count)
{
// @TODO: this data is doubled (session and cache tables) if caching is enabled
// Make sure we have an array here (#1487066)
if (!is_array($_SESSION['unseen_count']))
$_SESSION['unseen_count'] = array();
$_SESSION['unseen_count'][$mbox_name] = $count;
}
function rcmail_get_unseen_count($mbox_name)
{
if (is_array($_SESSION['unseen_count']) && array_key_exists($mbox_name, $_SESSION['unseen_count']))
return $_SESSION['unseen_count'][$mbox_name];
else
return null;
}
/**
* Sets message is_safe flag according to 'show_images' option value
*
* @param object rcube_message Message
*/
function rcmail_check_safe(&$message)
{
global $RCMAIL;
if (!$message->is_safe
&& ($show_images = $RCMAIL->config->get('show_images'))
&& $message->has_html_part()
) {
switch ($show_images) {
case 1: // known senders only
// get default addressbook, like in addcontact.inc
$CONTACTS = $RCMAIL->get_address_book(-1, true);
if ($CONTACTS) {
$result = $CONTACTS->search('email', $message->sender['mailto'], 1, false);
if ($result->count) {
$message->set_safe(true);
}
}
break;
case 2: // always
$message->set_safe(true);
break;
}
}
}
/**
* Cleans up the given message HTML Body (for displaying)
*
* @param string HTML
* @param array Display parameters
* @param array CID map replaces (inline images)
* @return string Clean HTML
*/
function rcmail_wash_html($html, $p, $cid_replaces)
{
global $REMOTE_OBJECTS;
$p += array('safe' => false, 'inline_html' => true);
// special replacements (not properly handled by washtml class)
$html_search = array(
'/(<\/nobr>)(\s+)(<nobr>)/i', // space(s) between <NOBR>
'/<title[^>]*>[^<]*<\/title>/i', // PHP bug #32547 workaround: remove title tag
'/^(\0\0\xFE\xFF|\xFF\xFE\0\0|\xFE\xFF|\xFF\xFE|\xEF\xBB\xBF)/', // byte-order mark (only outlook?)
'/<html\s[^>]+>/i', // washtml/DOMDocument cannot handle xml namespaces
);
$html_replace = array(
'\\1'.' &nbsp; '.'\\3',
'',
'',
'<html>',
);
$html = preg_replace($html_search, $html_replace, trim($html));
// PCRE errors handling (#1486856), should we use something like for every preg_* use?
if ($html === null && ($preg_error = preg_last_error()) != PREG_NO_ERROR) {
$errstr = "Could not clean up HTML message! PCRE Error: $preg_error.";
if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR)
$errstr .= " Consider raising pcre.backtrack_limit!";
if ($preg_error == PREG_RECURSION_LIMIT_ERROR)
$errstr .= " Consider raising pcre.recursion_limit!";
raise_error(array('code' => 620, 'type' => 'php',
'line' => __LINE__, 'file' => __FILE__,
'message' => $errstr), true, false);
return '';
}
// fix (unknown/malformed) HTML tags before "wash"
$html = preg_replace_callback('/(<[\/]*)([^\s>]+)/', 'rcmail_html_tag_callback', $html);
// charset was converted to UTF-8 in rcube_storage::get_message_part(),
// change/add charset specification in HTML accordingly,
// washtml cannot work without that
$meta = '<meta http-equiv="Content-Type" content="text/html; charset='.RCMAIL_CHARSET.'" />';
// remove old meta tag and add the new one, making sure
// that it is placed in the head (#1488093)
$html = preg_replace('/<meta[^>]+charset=[a-z0-9-_]+[^>]*>/Ui', '', $html);
$html = preg_replace('/(<head[^>]*>)/Ui', '\\1'.$meta, $html, -1, $rcount);
if (!$rcount) {
$html = '<head>' . $meta . '</head>' . $html;
}
// turn relative into absolute urls
$html = rcmail_resolve_base($html);
// clean HTML with washhtml by Frederic Motte
$wash_opts = array(
'show_washed' => false,
'allow_remote' => $p['safe'],
'blocked_src' => "./program/resources/blocked.gif",
'charset' => RCMAIL_CHARSET,
'cid_map' => $cid_replaces,
'html_elements' => array('body'),
);
if (!$p['inline_html']) {
$wash_opts['html_elements'] = array('html','head','title','body');
}
if ($p['safe']) {
$wash_opts['html_elements'][] = 'link';
$wash_opts['html_attribs'] = array('rel','type');
}
// overwrite washer options with options from plugins
if (isset($p['html_elements']))
$wash_opts['html_elements'] = $p['html_elements'];
if (isset($p['html_attribs']))
$wash_opts['html_attribs'] = $p['html_attribs'];
// initialize HTML washer
$washer = new washtml($wash_opts);
if (!$p['skip_washer_form_callback'])
$washer->add_callback('form', 'rcmail_washtml_callback');
// allow CSS styles, will be sanitized by rcmail_washtml_callback()
if (!$p['skip_washer_style_callback'])
$washer->add_callback('style', 'rcmail_washtml_callback');
// Remove non-UTF8 characters (#1487813)
$html = rc_utf8_clean($html);
$html = $washer->wash($html);
$REMOTE_OBJECTS = $washer->extlinks;
return $html;
}
/**
* Convert the given message part to proper HTML
* which can be displayed the message view
*
* @param object rcube_message_part Message part
* @param array Display parameters array
* @return string Formatted HTML string
*/
function rcmail_print_body($part, $p = array())
{
global $RCMAIL;
// trigger plugin hook
$data = $RCMAIL->plugins->exec_hook('message_part_before',
array('type' => $part->ctype_secondary, 'body' => $part->body, 'id' => $part->mime_id)
+ $p + array('safe' => false, 'plain' => false, 'inline_html' => true));
// convert html to text/plain
if ($data['type'] == 'html' && $data['plain']) {
$txt = new html2text($data['body'], false, true);
$body = $txt->get_text();
$part->ctype_secondary = 'plain';
}
// text/html
else if ($data['type'] == 'html') {
$body = rcmail_wash_html($data['body'], $data, $part->replaces);
$part->ctype_secondary = $data['type'];
}
// text/enriched
else if ($data['type'] == 'enriched') {
$part->ctype_secondary = 'html';
require_once(INSTALL_PATH . 'program/lib/enriched.inc');
$body = Q(enriched_to_html($data['body']), 'show');
}
else {
// assert plaintext
$body = $part->body;
$part->ctype_secondary = $data['type'] = 'plain';
}
// free some memory (hopefully)
unset($data['body']);
// plaintext postprocessing
if ($part->ctype_secondary == 'plain')
$body = rcmail_plain_body($body, $part->ctype_parameters['format'] == 'flowed');
// allow post-processing of the message body
$data = $RCMAIL->plugins->exec_hook('message_part_after',
array('type' => $part->ctype_secondary, 'body' => $body, 'id' => $part->mime_id) + $data);
return $data['type'] == 'html' ? $data['body'] : html::tag('pre', array(), $data['body']);
}
/**
* Handle links and citation marks in plain text message
*
* @param string Plain text string
* @param boolean Text uses format=flowed
*
* @return string Formatted HTML string
*/
function rcmail_plain_body($body, $flowed=false)
{
global $RCMAIL;
// make links and email-addresses clickable
$replacer = new rcube_string_replacer;
// search for patterns like links and e-mail addresses and replace with tokens
$body = $replacer->replace($body);
// split body into single lines
$body = preg_split('/\r?\n/', $body);
$quote_level = 0;
$last = -1;
// find/mark quoted lines...
for ($n=0, $cnt=count($body); $n < $cnt; $n++) {
if ($body[$n][0] == '>' && preg_match('/^(>+ {0,1})+/', $body[$n], $regs)) {
$q = substr_count($regs[0], '>');
$body[$n] = substr($body[$n], strlen($regs[0]));
if ($q > $quote_level) {
$body[$n] = $replacer->get_replacement($replacer->add(
str_repeat('<blockquote>', $q - $quote_level))) . $body[$n];
}
else if ($q < $quote_level) {
$body[$n] = $replacer->get_replacement($replacer->add(
str_repeat('</blockquote>', $quote_level - $q))) . $body[$n];
}
else if ($flowed) {
// previous line is flowed
if (isset($body[$last]) && $body[$n]
&& $body[$last][strlen($body[$last])-1] == ' ') {
// merge lines
$body[$last] .= $body[$n];
unset($body[$n]);
}
else {
$last = $n;
}
}
}
else {
$q = 0;
if ($flowed) {
// sig separator - line is fixed
if ($body[$n] == '-- ') {
$last = $last_sig = $n;
}
else {
// remove space-stuffing
if ($body[$n][0] == ' ')
$body[$n] = substr($body[$n], 1);
// previous line is flowed?
if (isset($body[$last]) && $body[$n]
&& $last !== $last_sig
&& $body[$last][strlen($body[$last])-1] == ' '
) {
$body[$last] .= $body[$n];
unset($body[$n]);
}
else {
$last = $n;
}
}
if ($quote_level > 0)
$body[$last] = $replacer->get_replacement($replacer->add(
str_repeat('</blockquote>', $quote_level))) . $body[$last];
}
else if ($quote_level > 0)
$body[$n] = $replacer->get_replacement($replacer->add(
str_repeat('</blockquote>', $quote_level))) . $body[$n];
}
$quote_level = $q;
}
$body = join("\n", $body);
// quote plain text (don't use Q() here, to display entities "as is")
$table = get_html_translation_table(HTML_SPECIALCHARS);
unset($table['?']);
$body = strtr($body, $table);
// colorize signature (up to <sig_max_lines> lines)
$len = strlen($body);
$sig_max_lines = $RCMAIL->config->get('sig_max_lines', 15);
while (($sp = strrpos($body, "-- \n", $sp ? -$len+$sp-1 : 0)) !== false) {
if ($sp == 0 || $body[$sp-1] == "\n") {
// do not touch blocks with more that X lines
if (substr_count($body, "\n", $sp) < $sig_max_lines)
$body = substr($body, 0, max(0, $sp))
.'<span class="sig">'.substr($body, $sp).'</span>';
break;
}
}
// insert url/mailto links and citation tags
$body = $replacer->resolve($body);
return $body;
}
/**
* Callback function for washtml cleaning class
*/
function rcmail_washtml_callback($tagname, $attrib, $content, $washtml)
{
switch ($tagname) {
case 'form':
$out = html::div('form', $content);
break;
case 'style':
// decode all escaped entities and reduce to ascii strings
$stripped = preg_replace('/[^a-zA-Z\(:;]/', '', rcmail_xss_entity_decode($content));
// now check for evil strings like expression, behavior or url()
if (!preg_match('/expression|behavior|javascript:|import[^a]/i', $stripped)) {
if (!$washtml->get_config('allow_remote') && stripos($stripped, 'url('))
$washtml->extlinks = true;
else
$out = html::tag('style', array('type' => 'text/css'), $content);
break;
}
default:
$out = '';
}
return $out;
}
/**
* Callback function for HTML tags fixing
*/
function rcmail_html_tag_callback($matches)
{
$tagname = $matches[2];
$tagname = preg_replace(array(
'/:.*$/', // Microsoft's Smart Tags <st1:xxxx>
'/[^a-z0-9_\[\]\!-]/i', // forbidden characters
), '', $tagname);
return $matches[1].$tagname;
}
/**
* return table with message headers
*/
function rcmail_message_headers($attrib, $headers=NULL)
{
global $OUTPUT, $MESSAGE, $PRINT_MODE, $RCMAIL;
static $sa_attrib;
// keep header table attrib
if (is_array($attrib) && !$sa_attrib)
$sa_attrib = $attrib;
else if (!is_array($attrib) && is_array($sa_attrib))
$attrib = $sa_attrib;
if (!isset($MESSAGE))
return FALSE;
// get associative array of headers object
if (!$headers)
$headers = is_object($MESSAGE->headers) ? get_object_vars($MESSAGE->headers) : $MESSAGE->headers;
// show these headers
$standard_headers = array('subject', 'from', 'to', 'cc', 'bcc', 'replyto',
'mail-reply-to', 'mail-followup-to', 'date', 'priority');
$exclude_headers = $attrib['exclude'] ? explode(',', $attrib['exclude']) : array();
$output_headers = array();
foreach ($standard_headers as $hkey) {
$ishtml = false;
if ($headers[$hkey])
$value = $headers[$hkey];
else if ($headers['others'][$hkey])
$value = $headers['others'][$hkey];
else
continue;
if (in_array($hkey, $exclude_headers))
continue;
$header_title = rcube_label(preg_replace('/(^mail-|-)/', '', $hkey));
if ($hkey == 'date') {
if ($PRINT_MODE)
$header_value = format_date($value, $RCMAIL->config->get('date_long', 'x'));
else
$header_value = format_date($value);
}
else if ($hkey == 'priority') {
if ($value) {
$header_value = html::span('prio' . $value, rcmail_localized_priority($value));
}
else
continue;
}
else if ($hkey == 'replyto') {
if ($headers['replyto'] != $headers['from']) {
$header_value = rcmail_address_string($value, $attrib['max'], true, $attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else
continue;
}
else if ($hkey == 'mail-reply-to') {
if ($headers['mail-replyto'] != $headers['reply-to']
&& $headers['reply-to'] != $headers['from']
) {
$header_value = rcmail_address_string($value, $attrib['max'], true, $attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else
continue;
}
else if ($hkey == 'mail-followup-to') {
$header_value = rcmail_address_string($value, $attrib['max'], true, $attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else if (in_array($hkey, array('from', 'to', 'cc', 'bcc'))) {
$header_value = rcmail_address_string($value, $attrib['max'], true, $attrib['addicon'], $headers['charset'], $header_title);
$ishtml = true;
}
else if ($hkey == 'subject' && empty($value))
$header_value = rcube_label('nosubject');
else
$header_value = trim(rcube_mime::decode_header($value, $headers['charset']));
$output_headers[$hkey] = array(
'title' => $header_title,
'value' => $header_value,
'raw' => $value,
'html' => $ishtml,
);
}
$plugin = $RCMAIL->plugins->exec_hook('message_headers_output',
array('output' => $output_headers, 'headers' => $MESSAGE->headers, 'exclude' => $exclude_headers));
// single header value is requested
if (!empty($attrib['valueof']))
return Q($plugin['output'][$attrib['valueof']]['value'], ($attrib['valueof'] == 'subject' ? 'strict' : 'show'));
// compose html table
$table = new html_table(array('cols' => 2));
foreach ($plugin['output'] as $hkey => $row) {
$table->add(array('class' => 'header-title'), Q($row['title']));
$table->add(array('class' => 'header '.$hkey), $row['html'] ? $row['value'] : Q($row['value'], ($hkey == 'subject' ? 'strict' : 'show')));
}
return $table->show($attrib);
}
/**
* Convert Priority header value into a localized string
*/
function rcmail_localized_priority($value)
{
$labels_map = array(
'1' => 'highest',
'2' => 'high',
'3' => 'normal',
'4' => 'low',
'5' => 'lowest',
);
if ($value && $labels_map[$value])
return rcube_label($labels_map[$value]);
return '';
}
/**
* return block to show full message headers
*/
function rcmail_message_full_headers($attrib, $headers=NULL)
{
global $OUTPUT;
$html = html::div(array('id' => "all-headers", 'class' => "all", 'style' => 'display:none'), html::div(array('id' => 'headers-source'), ''));
$html .= html::div(array('class' => "more-headers show-headers", 'onclick' => "return ".JS_OBJECT_NAME.".command('show-headers','',this)", 'title' => rcube_label('togglefullheaders')), '');
$OUTPUT->add_gui_object('all_headers_row', 'all-headers');
$OUTPUT->add_gui_object('all_headers_box', 'headers-source');
return html::div($attrib, $html);
}
/**
* Handler for the 'messagebody' GUI object
*
* @param array Named parameters
* @return string HTML content showing the message body
*/
function rcmail_message_body($attrib)
{
global $CONFIG, $OUTPUT, $MESSAGE, $RCMAIL, $REMOTE_OBJECTS;
if (!is_array($MESSAGE->parts) && empty($MESSAGE->body))
return '';
if (!$attrib['id'])
$attrib['id'] = 'rcmailMsgBody';
$safe_mode = $MESSAGE->is_safe || intval($_GET['_safe']);
$out = '';
$header_attrib = array();
foreach ($attrib as $attr => $value)
if (preg_match('/^headertable([a-z]+)$/i', $attr, $regs))
$header_attrib[$regs[1]] = $value;
if (!empty($MESSAGE->parts)) {
foreach ($MESSAGE->parts as $i => $part) {
+ console($part);
if ($part->type == 'headers')
$out .= rcmail_message_headers(sizeof($header_attrib) ? $header_attrib : NULL, $part->headers);
else if ($part->type == 'content') {
// unsapported
if ($part->realtype) {
if ($part->realtype == 'multipart/encrypted') {
$out .= html::span('part-notice', rcube_label('encryptedmessage'));
}
continue;
}
else if (!$part->size) {
continue;
}
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
else if (!rcmail_mem_check($part->size * 10)) {
$out .= html::span('part-notice', rcube_label('messagetoobig'). ' '
. html::a('?_task=mail&_action=get&_download=1&_uid='.$MESSAGE->uid.'&_part='.$part->mime_id
.'&_mbox='. urlencode($RCMAIL->storage->get_folder()), rcube_label('download')));
continue;
}
if (empty($part->ctype_parameters) || empty($part->ctype_parameters['charset']))
$part->ctype_parameters['charset'] = $MESSAGE->headers->charset;
// fetch part if not available
if (!isset($part->body))
$part->body = $MESSAGE->get_part_content($part->mime_id);
// message is cached but not exists (#1485443), or other error
if ($part->body === false) {
rcmail_message_error($MESSAGE->uid);
}
$plugin = $RCMAIL->plugins->exec_hook('message_body_prefix', array(
'part' => $part, 'prefix' => ''));
$body = rcmail_print_body($part, array('safe' => $safe_mode, 'plain' => !$CONFIG['prefer_html']));
if ($part->ctype_secondary == 'html') {
$body = rcmail_html4inline($body, $attrib['id'], 'rcmBody', $attrs, $safe_mode);
$div_attr = array('class' => 'message-htmlpart');
$style = array();
if (!empty($attrs)) {
foreach ($attrs as $a_idx => $a_val)
$style[] = $a_idx . ': ' . $a_val;
if (!empty($style))
$div_attr['style'] = implode('; ', $style);
}
$out .= html::div($div_attr, $plugin['prefix'] . $body);
}
else
$out .= html::div('message-part', $plugin['prefix'] . $body);
}
}
}
else {
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcmail_mem_check(strlen($MESSAGE->body) * 10)) {
$out .= html::span('part-notice', rcube_label('messagetoobig'). ' '
. html::a('?_task=mail&_action=get&_download=1&_uid='.$MESSAGE->uid.'&_part=0'
.'&_mbox='. urlencode($RCMAIL->storage->get_folder()), rcube_label('download')));
}
else {
$plugin = $RCMAIL->plugins->exec_hook('message_body_prefix', array(
'part' => $MESSAGE, 'prefix' => ''));
$out .= html::div('message-part', $plugin['prefix'] . html::tag('pre', array(),
rcmail_plain_body(Q($MESSAGE->body, 'strict', false))));
}
}
// list images after mail body
if ($RCMAIL->config->get('inline_images', true) && !empty($MESSAGE->attachments)) {
$thumbnail_size = $RCMAIL->config->get('image_thumbnail_size', 240);
foreach ($MESSAGE->attachments as $attach_prop) {
// skip inline images
if ($attach_prop->content_id && $attach_prop->disposition == 'inline') {
continue;
}
// Content-Type: image/*...
if (rcmail_part_image_type($attach_prop)) {
// display thumbnails
if ($thumbnail_size) {
$show_link = array(
'href' => $MESSAGE->get_part_url($attach_prop->mime_id, false),
'onclick' => sprintf(
'return %s.command(\'load-attachment\',{part:\'%s\', mimetype:\'%s\'},this)',
JS_OBJECT_NAME,
$attach_prop->mime_id,
rcmail_fix_mimetype($attach_prop->mimetype))
);
$out .= html::p('image-attachment',
html::a($show_link + array('class' => 'image-link'),
html::img(array(
'class' => 'image-thumbnail',
'src' => $MESSAGE->get_part_url($attach_prop->mime_id, true) . '&_thumb=1',
'title' => $attach_prop->filename,
'alt' => $attach_prop->filename,
'style' => sprintf('max-width:%dpx; max-height:%dpx', $thumbnail_size, $thumbnail_size),
))
) .
html::span('image-filename', Q($attach_prop->filename)) .
html::span('image-filesize', Q($RCMAIL->show_bytes($attach_prop->size))) .
html::span('attachment-links',
html::a($show_link['href'] . '&_download=1', rcube_label('download'))
) .
html::br(array('style' => 'clear:both'))
);
}
else {
$out .= html::tag('fieldset', 'image-attachment',
html::tag('legend', 'image-filename', Q($attach_prop->filename)) .
html::p(array('align' => "center"),
html::img(array(
'src' => $MESSAGE->get_part_url($attach_prop->mime_id, true),
'title' => $attach_prop->filename,
'alt' => $attach_prop->filename,
)))
);
}
}
}
}
// tell client that there are blocked remote objects
if ($REMOTE_OBJECTS && !$safe_mode)
$OUTPUT->set_env('blockedobjects', true);
return html::div($attrib, $out);
}
function rcmail_part_image_type($part)
{
$rcmail = rcmail::get_instance();
// Skip TIFF images if browser doesn't support this format...
$tiff_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['tif']);
// until we can convert them to JPEG
$tiff_support = $tiff_support || $rcmail->config->get('im_convert_path');
// Content-type regexp
$mime_regex = $tiff_support ? '/^image\//i' : '/^image\/(?!tif)/i';
// Content-Type: image/*...
if (preg_match($mime_regex, $part->mimetype)) {
return $part->mimetype;
}
// Many clients use application/octet-stream, we'll detect mimetype
// by checking filename extension
// Supported image filename extensions to image type map
$types = array(
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
);
if ($tiff_support) {
$types['tif'] = 'image/tiff';
$types['tiff'] = 'image/tiff';
}
if ($part->filename
&& preg_match('/^application\/octet-stream$/i', $part->mimetype)
&& preg_match('/\.([^.]+)$/i', $part->filename, $m)
&& ($extension = strtolower($m[1]))
&& isset($types[$extension])
) {
return $types[$extension];
}
}
/**
* Convert all relative URLs according to a <base> in HTML
*/
function rcmail_resolve_base($body)
{
// check for <base href=...>
if (preg_match('!(<base.*href=["\']?)([hftps]{3,5}://[a-z0-9/.%-]+)!i', $body, $regs)) {
$replacer = new rcube_base_replacer($regs[2]);
$body = $replacer->replace($body);
}
return $body;
}
/**
* modify a HTML message that it can be displayed inside a HTML page
*/
function rcmail_html4inline($body, $container_id, $body_id='', &$attributes=null, $allow_remote=false)
{
$last_style_pos = 0;
$cont_id = $container_id.($body_id ? ' div.'.$body_id : '');
// find STYLE tags
while (($pos = stripos($body, '<style', $last_style_pos)) && ($pos2 = stripos($body, '</style>', $pos)))
{
$pos = strpos($body, '>', $pos) + 1;
$len = $pos2 - $pos;
// replace all css definitions with #container [def]
$styles = substr($body, $pos, $len);
$styles = rcmail_mod_css_styles($styles, $cont_id, $allow_remote);
$body = substr_replace($body, $styles, $pos, $len);
$last_style_pos = $pos2 + strlen($styles) - $len;
}
// modify HTML links to open a new window if clicked
$GLOBALS['rcmail_html_container_id'] = $container_id;
$body = preg_replace_callback('/<(a|link|area)\s+([^>]+)>/Ui', 'rcmail_alter_html_link', $body);
unset($GLOBALS['rcmail_html_container_id']);
$body = preg_replace(array(
// add comments arround html and other tags
'/(<!DOCTYPE[^>]*>)/i',
'/(<\?xml[^>]*>)/i',
'/(<\/?html[^>]*>)/i',
'/(<\/?head[^>]*>)/i',
'/(<title[^>]*>.*<\/title>)/Ui',
'/(<\/?meta[^>]*>)/i',
// quote <? of php and xml files that are specified as text/html
'/<\?/',
'/\?>/',
// replace <body> with <div>
'/<body([^>]*)>/i',
'/<\/body>/i',
),
array(
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'<!--\\1-->',
'&lt;?',
'?&gt;',
'<div class="'.$body_id.'"\\1>',
'</div>',
),
$body);
$attributes = array();
// Handle body attributes that doesn't play nicely with div elements
$regexp = '/<div class="' . preg_quote($body_id, '/') . '"([^>]*)/';
if (preg_match($regexp, $body, $m)) {
$attrs = $m[0];
// Get bgcolor, we'll set it as background-color of the message container
if ($m[1] && preg_match('/bgcolor=["\']*([a-z0-9#]+)["\']*/', $attrs, $mb)) {
$attributes['background-color'] = $mb[1];
$attrs = preg_replace('/bgcolor=["\']*([a-z0-9#]+)["\']*/', '', $attrs);
}
// Get background, we'll set it as background-image of the message container
if ($m[1] && preg_match('/background=["\']*([^"\'>\s]+)["\']*/', $attrs, $mb)) {
$attributes['background-image'] = 'url('.$mb[1].')';
$attrs = preg_replace('/background=["\']*([^"\'>\s]+)["\']*/', '', $attrs);
}
if (!empty($attributes)) {
$body = preg_replace($regexp, rtrim($attrs), $body, 1);
}
// handle body styles related to background image
if ($attributes['background-image']) {
// get body style
if (preg_match('/#'.preg_quote($cont_id, '/').'\s+\{([^}]+)}/i', $body, $m)) {
// get background related style
if (preg_match_all('/(background-position|background-repeat)\s*:\s*([^;]+);/i', $m[1], $ma, PREG_SET_ORDER)) {
foreach ($ma as $style)
$attributes[$style[1]] = $style[2];
}
}
}
}
// make sure there's 'rcmBody' div, we need it for proper css modification
// its name is hardcoded in rcmail_message_body() also
else {
$body = '<div class="' . $body_id . '">' . $body . '</div>';
}
return $body;
}
/**
* parse link attributes and set correct target
*/
function rcmail_alter_html_link($matches)
{
global $RCMAIL;
// Support unicode/punycode in top-level domain part
$EMAIL_PATTERN = '([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))';
$tag = $matches[1];
$attrib = parse_attrib_string($matches[2]);
$end = '>';
// Remove non-printable characters in URL (#1487805)
if ($attrib['href'])
$attrib['href'] = preg_replace('/[\x00-\x1F]/', '', $attrib['href']);
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) {
$tempurl = 'tmp-' . md5($attrib['href']) . '.css';
$_SESSION['modcssurls'][$tempurl] = $attrib['href'];
$attrib['href'] = $RCMAIL->url(array('task' => 'utils', 'action' => 'modcss', 'u' => $tempurl, 'c' => $GLOBALS['rcmail_html_container_id']));
$end = ' />';
}
else if (preg_match('/^mailto:'.$EMAIL_PATTERN.'(\?[^"\'>]+)?/i', $attrib['href'], $mailto)) {
$attrib['href'] = $mailto[0];
$attrib['onclick'] = sprintf(
"return %s.command('compose','%s',this)",
JS_OBJECT_NAME,
JQ($mailto[1].$mailto[3]));
}
else if (empty($attrib['href']) && !$attrib['name']) {
$attrib['href'] = './#NOP';
$attrib['onclick'] = 'return false';
}
else if (!empty($attrib['href']) && $attrib['href'][0] != '#') {
$attrib['target'] = '_blank';
}
// allowed attributes for a|link|area tags
$allow = array('href','name','target','onclick','id','class','style','title',
'rel','type','media','alt','coords','nohref','hreflang','shape');
return "<$tag" . html::attrib_string($attrib, $allow) . $end;
}
/**
* decode address string and re-format it as HTML links
*/
function rcmail_address_string($input, $max=null, $linked=false, $addicon=null, $default_charset=null, $title=null)
{
global $RCMAIL, $PRINT_MODE, $CONFIG;
$a_parts = rcube_mime::decode_address_list($input, null, true, $default_charset);
if (!sizeof($a_parts))
return $input;
$c = count($a_parts);
$j = 0;
$out = '';
$allvalues = array();
if ($addicon && !isset($_SESSION['writeable_abook'])) {
$_SESSION['writeable_abook'] = $RCMAIL->get_address_sources(true) ? true : false;
}
foreach ($a_parts as $part) {
$j++;
$name = $part['name'];
$mailto = $part['mailto'];
$string = $part['string'];
// IDNA ASCII to Unicode
if ($name == $mailto)
$name = rcube_idn_to_utf8($name);
if ($string == $mailto)
$string = rcube_idn_to_utf8($string);
$mailto = rcube_idn_to_utf8($mailto);
if ($PRINT_MODE) {
$out .= sprintf('%s &lt;%s&gt;', Q($name), $mailto);
}
else if (check_email($part['mailto'], false)) {
if ($linked) {
$address = html::a(array(
'href' => 'mailto:'.$mailto,
'onclick' => sprintf("return %s.command('compose','%s',this)", JS_OBJECT_NAME, JQ($mailto)),
'title' => $mailto,
'class' => "rcmContactAddress",
),
Q($name ? $name : $mailto));
}
else {
$address = html::span(array('title' => $mailto, 'class' => "rcmContactAddress"),
Q($name ? $name : $mailto));
}
if ($addicon && $_SESSION['writeable_abook']) {
$address .= html::a(array(
'href' => "#add",
'onclick' => sprintf("return %s.command('add-contact','%s',this)", JS_OBJECT_NAME, $string),
'title' => rcube_label('addtoaddressbook'),
'class' => 'rcmaddcontact',
),
html::img(array(
'src' => $CONFIG['skin_path'] . $addicon,
'alt' => "Add contact",
)));
}
}
else {
$address = '';
if ($name)
$address .= Q($name);
if ($mailto)
$address .= (strlen($address) ? ' ' : '') . sprintf('&lt;%s&gt;', Q($mailto));
}
$address = html::span('adr', $address);
$allvalues[] = $address;
if (!$moreadrs)
$out .= ($out ? ', ' : '') . $address;
if ($max && $j == $max && $c > $j) {
if ($linked) {
$moreadrs = $c - $j;
}
else {
$out .= '...';
break;
}
}
}
if ($moreadrs) {
$out .= ' ' . html::a(array(
'href' => '#more',
'class' => 'morelink',
'onclick' => sprintf("return %s.show_popup_dialog('%s','%s')",
JS_OBJECT_NAME,
JQ(join(', ', $allvalues)),
JQ($title))
),
Q(rcube_label(array('name' => 'andnmore', 'vars' => array('nr' => $moreadrs)))));
}
return $out;
}
/**
* Wrap text to a given number of characters per line
* but respect the mail quotation of replies messages (>).
* Finally add another quotation level by prpending the lines
* with >
*
* @param string Text to wrap
* @param int The line width
* @return string The wrapped text
*/
function rcmail_wrap_and_quote($text, $length = 72)
{
// Rebuild the message body with a maximum of $max chars, while keeping quoted message.
$max = max(75, $length + 8);
$lines = preg_split('/\r?\n/', trim($text));
$out = '';
foreach ($lines as $line) {
// don't wrap already quoted lines
if ($line[0] == '>')
$line = '>' . rtrim($line);
else if (mb_strlen($line) > $max) {
$newline = '';
foreach(explode("\n", rc_wordwrap($line, $length - 2)) as $l) {
if (strlen($l))
$newline .= '> ' . $l . "\n";
else
$newline .= ">\n";
}
$line = rtrim($newline);
}
else
$line = '> ' . $line;
// Append the line
$out .= $line . "\n";
}
return rtrim($out, "\n");
}
function rcmail_draftinfo_encode($p)
{
$parts = array();
foreach ($p as $key => $val)
$parts[] = $key . '=' . ($key == 'folder' ? base64_encode($val) : $val);
return join('; ', $parts);
}
function rcmail_draftinfo_decode($str)
{
$info = array();
foreach (preg_split('/;\s+/', $str) as $part) {
list($key, $val) = explode('=', $part, 2);
if ($key == 'folder')
$val = base64_decode($val);
$info[$key] = $val;
}
return $info;
}
function rcmail_message_part_controls($attrib)
{
global $MESSAGE, $RCMAIL;
$part = asciiwords(get_input_value('_part', RCUBE_INPUT_GPC));
if (!is_object($MESSAGE) || !is_array($MESSAGE->parts) || !($_GET['_uid'] && $_GET['_part']) || !$MESSAGE->mime_parts[$part])
return '';
$part = $MESSAGE->mime_parts[$part];
$table = new html_table(array('cols' => 3));
$filename = $part->filename;
if (empty($filename) && $attach_prop->mimetype == 'text/html') {
$filename = rcube_label('htmlmessage');
}
if (!empty($filename)) {
$table->add('title', Q(rcube_label('filename')));
$table->add('header', Q($filename));
$table->add('download-link', html::a(array('href' => './?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING'])), Q(rcube_label('download'))));
}
$table->add('title', Q(rcube_label('filesize')));
$table->add('header', Q($RCMAIL->message_part_size($part)));
return $table->show($attrib);
}
function rcmail_message_part_frame($attrib)
{
global $MESSAGE;
$part = $MESSAGE->mime_parts[asciiwords(get_input_value('_part', RCUBE_INPUT_GPC))];
$ctype_primary = strtolower($part->ctype_primary);
$attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary=='text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
return html::iframe($attrib);
}
/**
* clear message composing settings
*/
function rcmail_compose_cleanup($id)
{
if (!isset($_SESSION['compose_data_'.$id]))
return;
$rcmail = rcmail::get_instance();
$rcmail->plugins->exec_hook('attachments_cleanup', array('group' => $id));
$rcmail->session->remove('compose_data_'.$id);
}
/**
* Send the MDN response
*
* @param mixed $message Original message object (rcube_message) or UID
* @param array $smtp_error SMTP error array (reference)
*
* @return boolean Send status
*/
function rcmail_send_mdn($message, &$smtp_error)
{
global $RCMAIL;
if (!is_object($message) || !is_a($message, 'rcube_message'))
$message = new rcube_message($message);
if ($message->headers->mdn_to && empty($message->headers->flags['MDNSENT']) &&
($RCMAIL->storage->check_permflag('MDNSENT') || $RCMAIL->storage->check_permflag('*')))
{
$identity = $RCMAIL->user->get_identity();
$sender = format_email_recipient($identity['email'], $identity['name']);
$recipient = array_shift(rcube_mime::decode_address_list(
$message->headers->mdn_to, 1, true, $message->headers->charset));
$mailto = $recipient['mailto'];
$compose = new Mail_mime("\r\n");
$compose->setParam('text_encoding', 'quoted-printable');
$compose->setParam('html_encoding', 'quoted-printable');
$compose->setParam('head_encoding', 'quoted-printable');
$compose->setParam('head_charset', RCMAIL_CHARSET);
$compose->setParam('html_charset', RCMAIL_CHARSET);
$compose->setParam('text_charset', RCMAIL_CHARSET);
// compose headers array
$headers = array(
'Date' => rcmail_user_date(),
'From' => $sender,
'To' => $message->headers->mdn_to,
'Subject' => rcube_label('receiptread') . ': ' . $message->subject,
'Message-ID' => rcmail_gen_message_id(),
'X-Sender' => $identity['email'],
'References' => trim($message->headers->references . ' ' . $message->headers->messageID),
);
if ($agent = $RCMAIL->config->get('useragent'))
$headers['User-Agent'] = $agent;
$body = rcube_label("yourmessage") . "\r\n\r\n" .
"\t" . rcube_label("to") . ': ' . rcube_mime::decode_mime_string($message->headers->to, $message->headers->charset) . "\r\n" .
"\t" . rcube_label("subject") . ': ' . $message->subject . "\r\n" .
"\t" . rcube_label("sent") . ': ' . format_date($message->headers->date, $RCMAIL->config->get('date_long')) . "\r\n" .
"\r\n" . rcube_label("receiptnote") . "\r\n";
$ua = $RCMAIL->config->get('useragent', "Roundcube Webmail (Version ".RCMAIL_VERSION.")");
$report = "Reporting-UA: $ua\r\n";
if ($message->headers->to)
$report .= "Original-Recipient: {$message->headers->to}\r\n";
$report .= "Final-Recipient: rfc822; {$identity['email']}\r\n" .
"Original-Message-ID: {$message->headers->messageID}\r\n" .
"Disposition: manual-action/MDN-sent-manually; displayed\r\n";
$compose->headers($headers);
$compose->setContentType('multipart/report', array('report-type'=> 'disposition-notification'));
$compose->setTXTBody(rc_wordwrap($body, 75, "\r\n"));
$compose->addAttachment($report, 'message/disposition-notification', 'MDNPart2.txt', false, '7bit', 'inline');
$sent = rcmail_deliver_message($compose, $identity['email'], $mailto, $smtp_error, $body_file);
if ($sent)
{
$RCMAIL->storage->set_flag($message->uid, 'MDNSENT');
return true;
}
}
return false;
}
// Fixes some content-type names
function rcmail_fix_mimetype($name)
{
// Some versions of Outlook create garbage Content-Type:
// application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608
if (preg_match('/^application\/pdf.+/', $name))
$name = 'application/pdf';
return $name;
}
function rcmail_search_filter($attrib)
{
global $OUTPUT, $CONFIG;
if (!strlen($attrib['id']))
$attrib['id'] = 'rcmlistfilter';
$attrib['onchange'] = JS_OBJECT_NAME.'.filter_mailbox(this.value)';
/*
RFC3501 (6.4.4): 'ALL', 'RECENT',
'ANSWERED', 'DELETED', 'FLAGGED', 'SEEN',
'UNANSWERED', 'UNDELETED', 'UNFLAGGED', 'UNSEEN',
'NEW', // = (RECENT UNSEEN)
'OLD' // = NOT RECENT
*/
$select_filter = new html_select($attrib);
$select_filter->add(rcube_label('all'), 'ALL');
$select_filter->add(rcube_label('unread'), 'UNSEEN');
$select_filter->add(rcube_label('flagged'), 'FLAGGED');
$select_filter->add(rcube_label('unanswered'), 'UNANSWERED');
if (!$CONFIG['skip_deleted']) {
$select_filter->add(rcube_label('deleted'), 'DELETED');
$select_filter->add(rcube_label('undeleted'), 'UNDELETED');
}
$select_filter->add(rcube_label('priority').': '.rcube_label('highest'), 'HEADER X-PRIORITY 1');
$select_filter->add(rcube_label('priority').': '.rcube_label('high'), 'HEADER X-PRIORITY 2');
$select_filter->add(rcube_label('priority').': '.rcube_label('normal'), 'NOT HEADER X-PRIORITY 1 NOT HEADER X-PRIORITY 2 NOT HEADER X-PRIORITY 4 NOT HEADER X-PRIORITY 5');
$select_filter->add(rcube_label('priority').': '.rcube_label('low'), 'HEADER X-PRIORITY 4');
$select_filter->add(rcube_label('priority').': '.rcube_label('lowest'), 'HEADER X-PRIORITY 5');
$out = $select_filter->show($_SESSION['search_filter']);
$OUTPUT->add_gui_object('search_filter', $attrib['id']);
return $out;
}
function rcmail_message_error($uid=null)
{
global $RCMAIL;
// Set env variables for messageerror.html template
if ($RCMAIL->action == 'show') {
$mbox_name = $RCMAIL->storage->get_folder();
$RCMAIL->output->set_env('mailbox', $mbox_name);
$RCMAIL->output->set_env('uid', null);
}
// display error message
$RCMAIL->output->show_message('messageopenerror', 'error');
// ... display message error page
$RCMAIL->output->send('messageerror');
}
// register UI objects
$OUTPUT->add_handlers(array(
'mailboxlist' => 'rcmail_mailbox_list',
'messages' => 'rcmail_message_list',
'messagecountdisplay' => 'rcmail_messagecount_display',
'quotadisplay' => 'rcmail_quota_display',
'mailboxname' => 'rcmail_mailbox_name_display',
'messageheaders' => 'rcmail_message_headers',
'messagefullheaders' => 'rcmail_message_full_headers',
'messagebody' => 'rcmail_message_body',
'messagecontentframe' => 'rcmail_messagecontent_frame',
'messagepartframe' => 'rcmail_message_part_frame',
'messagepartcontrols' => 'rcmail_message_part_controls',
'searchfilter' => 'rcmail_search_filter',
'searchform' => array($OUTPUT, 'search_form'),
));
// register action aliases
$RCMAIL->register_action_map(array(
'preview' => 'show.inc',
'print' => 'show.inc',
'moveto' => 'move_del.inc',
'delete' => 'move_del.inc',
'send' => 'sendmail.inc',
'expunge' => 'folders.inc',
'purge' => 'folders.inc',
'remove-attachment' => 'attachments.inc',
'display-attachment' => 'attachments.inc',
'upload' => 'attachments.inc',
'group-expand' => 'autocomplete.inc',
));

File Metadata

Mime Type
text/x-diff
Expires
Sat, Mar 1, 8:42 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
166401
Default Alt Text
(142 KB)

Event Timeline