Overview

Packages

  • None
  • Vilma

Classes

  • Vilma
  • Vilma_Api
  • Vilma_Driver
  • Vilma_Driver_Qmailldap
  • Vilma_Driver_Sql
  • Vilma_Form_DeleteDomain
  • Vilma_Form_EditAlias
  • Vilma_Form_EditDomain
  • Vilma_Form_EditForward
  • Vilma_Form_EditUser
  • Vilma_MailboxDriver
  • Vilma_MailboxDriver_Hooks
  • Vilma_MailboxDriver_Imap
  • Vilma_MailboxDriver_Maildrop
  • Vilma_MailboxDriver_Null
  • Vilma_Test
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Copyright 2006-2007 Alkaloid Networks <http://www.alkaloid.net>
   4:  *
   5:  * See the enclosed file LICENSE for license information (BSD). If you did
   6:  * did not receive this file, see http://cvs.horde.org/co.php/vilma/LICENSE.
   7:  *
   8:  * @author Ben Klang <bklang@alkaloid.net>
   9:  * @author David Cummings <davidcummings@acm.org>
  10:  * @package Vilma
  11:  * @todo - Convert to Horde_Ldap
  12:  */
  13: class Vilma_Driver_Qmailldap extends Vilma_Driver_Sql
  14: {
  15:     /**
  16:      * Reference to initialized LDAP driver.
  17:      *
  18:      * @var resource
  19:      */
  20:     protected $_ldap;
  21: 
  22:     /**
  23:      * Cache for retrieved getUsers() results.
  24:      *
  25:      * @var array
  26:      */
  27:     protected $_users = array();
  28: 
  29:     /**
  30:      * Map of internal field names to LDAP attribute names.
  31:      *
  32:      * @var array
  33:      */
  34:     protected $_fieldmap = array(
  35:         'address' => 'mail',
  36:         'user_name' => 'uid',
  37:         'user_crypt' => 'userpassword',
  38:         'user_full_name' => 'cn',
  39:         'user_uid' => 'qmailuid',
  40:         'user_gid' => 'qmailgid',
  41:         'user_home_dir' => 'homedirectory',
  42:         'user_mail_dir' => 'mailmessagestore',
  43:         'user_mail_quota_bytes' => 'mailquotasize',
  44:         'user_mail_quota_count' => 'mailquotacount',
  45:         'user_enabled' => 'accountstatus',
  46:     );
  47: 
  48:     /**
  49:      * Constructor.
  50:      *
  51:      * @param array $params  Any parameters needed for this driver.
  52:      */
  53:     public function __construct($params)
  54:     {
  55:         $params = array_merge(
  56:             Horde::getDriverConfig('storage', 'sql'),
  57:             $params);
  58:         parent::__construct($params);
  59:         $this->_connect();
  60:     }
  61: 
  62:     /**
  63:      * Deletes a domain.
  64:      *
  65:      * @todo Add logic to remove all users, aliases, and grpfwds for this
  66:      *       domain.
  67:      * @todo Use parent::_deleteDomain()
  68:      *
  69:      * @param integer $domain_id  The id of the domain to delete.
  70:      *
  71:      * @throws Vilma_Exception
  72:      */
  73:     protected function _deleteDomain($domain_id)
  74:     {
  75:         $domain_record = $this->getDomain($domain_id);
  76:         $domain_name = $domain_record['domain_name'];
  77: 
  78:         /* Finally delete the domain. */
  79:         $sql = 'DELETE FROM vilma_domains WHERE domain_id=?';
  80:         $values = array((int)$domain_id);
  81: 
  82:         Horde::logMessage($sql, 'DEBUG');
  83:         return $this->_db->query($sql, $values);
  84:     }
  85: 
  86:     /**
  87:      * Returns the current number of users for a domain.
  88:      *
  89:      * @param string $domain_name  The name of the domain for which to
  90:      *                             get the current number of users.
  91:      *
  92:      * @return integer  The current number of users.
  93:      */
  94:     public function getDomainNumUsers($domain_name)
  95:     {
  96:         return count($this->_getUsers($domain_name));
  97:     }
  98: 
  99:     /**
 100:      * Returns all available users, if a domain name is passed then limit the
 101:      * list of users only to those users.
 102:      *
 103:      * @param string $domain  The name of the domain for which to fetch the
 104:      *                        users.
 105:      *
 106:      * @return array  The available users and their stored information.
 107:      * @throws Vilma_Exception
 108:      */
 109:     public function getUsers($domain = null)
 110:     {
 111:         // Cache for multiple calls.
 112:         if (is_null($domain) && isset($this->_users['_all'])) {
 113:             return $this->_users['_all'];
 114:         }
 115: 
 116:         if (!is_null($domain) && isset($this->_users[$domain])) {
 117:             return $this->_users[$domain];
 118:         }
 119: 
 120:         $filter = '(&';
 121:         if (!is_null($domain)) {
 122:             $filter .= '(mail=*@' . $domain . ')';
 123:         } else {
 124:             $domain = '_all';
 125:         }
 126: 
 127:         // Make sure we don't get any forwards.
 128:         $filter .= '(!(mailForwardingAddress=*))';
 129: 
 130:         // FIXME: Check/add configured filter instead of objectclasses
 131:         foreach ($this->_params['ldap']['objectclass'] as $objectclass) {
 132:             $filter .= '(objectClass=' . $objectclass . ')';
 133:         }
 134:         $filter .= ')';
 135: 
 136:         Horde::logMessage($filter, 'DEBUG');
 137:         $res = ldap_search($this->_ldap, $this->_params['ldap']['basedn'], $filter);
 138:         if ($res === false) {
 139:             throw new Vilma_Exception(sprintf(_("Error in LDAP search: %s"), ldap_error($this->LDAP)));
 140:         }
 141: 
 142:         $res = ldap_get_entries($this->_ldap, $res);
 143:         if ($res === false) {
 144:             throw new Vilma_Exception(sprintf(_("Error in LDAP search: %s"), ldap_error($this->LDAP)));
 145:         }
 146: 
 147:         $this->_users[$domain] = array();
 148:         // Can't use foreach because of the array format returned by LDAP driver
 149:         for ($i = 0; isset($res[$id]); $user = $res[$i++]) {
 150:             $info = array(
 151:                 'id' => $user['dn'],
 152:                 'address' => $user[$this->_fieldmap['address']][0],
 153:                 'type' => 'user',
 154:                 'user_name' => $user[$this->_fieldmap['user_name']][0]);
 155: 
 156:             // We likely don't have read permission on the crypted password so
 157:             // avoid any warnings/errors about missing array elements.
 158:             if (isset($user[$this->_fieldmap['user_crypt']])) {
 159:                 $info['user_crypt'] = $user[$this->_fieldmap['user_crypt']][0];
 160:             } else {
 161:                 $info['user_crypt'] = '';
 162:             }
 163:             $info['user_full_name'] = $user[$this->_fieldmap['user_full_name']][0];
 164:             // Mute assignment errors on the following optional fields.
 165:             // These may not be present if the mail is only forwarded.
 166:             $info['user_uid'] = @$user[$this->_fieldmap['user_uid']][0];
 167:             $info['user_gid'] = @$user[$this->_fieldmap['user_gid']][0];
 168:             $info['user_home_dir'] = @$user[$this->_fieldmap['user_home_dir']][0];
 169:             $info['user_mail_dir'] = @$user[$this->_fieldmap['user_mail_dir']][0];
 170:             $info['user_mail_quota_bytes'] = @$user[$this->_fieldmap['user_mail_quota_bytes']][0];
 171:             $info['user_mail_quota_count'] = @$user[$this->_fieldmap['user_mail_quota_count']][0];
 172: 
 173:             // If accountStatus is blank it's the same as active.
 174:             if (!isset($user[$this->_fieldmap['user_enabled']][0]) ||
 175:                 $user[$this->_fieldmap['user_enabled']][0] == 'active') {
 176:                 $info['user_enabled'] = 'active';
 177:             } else {
 178:                 // accountStatus can also be:
 179:                 // noaccess (receives but cannot pick up mail)
 180:                 // disabled (bounce incoming and deny pickup)
 181:                 // deleted (bounce incoming but allow pickup)
 182:                 $info['user_enabled'] = $user[$this->_fieldmap['user_enabled']][0];
 183:             }
 184: 
 185:             $this->_users[$domain][$i] = $info;
 186:         }
 187: 
 188:         return $this->_users[$domain];
 189:     }
 190: 
 191:     /**
 192:      * Returns the user information for a given user id.
 193:      *
 194:      * @param integer $user_id  The id of the user for which to fetch
 195:      *                          information.
 196:      *
 197:      * @return array  The user information.
 198:      */
 199:     public function getUser($user_id)
 200:     {
 201:         $user = $this->getUserStatus($user_id);
 202:         if (is_array($user)) {
 203:             return $user;
 204:         }
 205:         throw new Vilma_Exception(_("Unable to qualify address."));
 206:     }
 207: 
 208:     /**
 209:      * Saves a user to the backend.
 210:      *
 211:      * @param array $info  The user information to save.
 212:      *
 213:      * @return array  The user information.
 214:      * @throws Vilma_Exception
 215:      */
 216:     protected function _saveUser($info)
 217:     {
 218:         switch ($info['mode']) {
 219:         case 'edit':
 220:             return $this->_updateUser($info);
 221:         case 'new':
 222:             return $this->_createUser($info);
 223:         }
 224: 
 225:         throw new Vilma_Exception(_("Unable to save user information."));
 226:     }
 227: 
 228:     /**
 229:      * Updates a user in the backend.
 230:      *
 231:      * @param array $info  The user information to save.
 232:      *
 233:      * @return array  The user information.
 234:      * @throws Vilma_Exception
 235:      */
 236:     protected function _updateUser($info)
 237:     {
 238:         $address = $info['address'];
 239:         if (empty($address)) {
 240:             $user_name = $info['user_name'];
 241:             $domain = $info['domain'];
 242:             if (empty($user_name)) {
 243:                 throw new Vilma_Exception(_("Unable to acquire handle on address."));
 244:             }
 245:             $address = $info['user_name'] . $info['domain'];
 246:         }
 247:         $addrinfo = $this->getAddressInfo($address);
 248:         $type = $addrinfo['type'];
 249:         if ($type != 'user') {
 250:             throw new Vilma_Exception(sprintf(_("Unable to save account of type \"%s\""), $type));
 251:         }
 252: 
 253:         $user_info = $this->_searchForUser($address);
 254:         if ($res['count'] === 0) {
 255:             throw new Vilma_Exception(_("Error reading address information from backend."));
 256:         }
 257: 
 258:         $objectClassData = null;
 259:         if (isset($user_info[0]['objectclass'])) {
 260:             $objectClassData = $user_info[0]['objectclass'];
 261:         }
 262: 
 263:         // Don't want to save this to LDAP.
 264:         unset($info['mode']);
 265: 
 266:         // Special case for the password:  If it was provided, it needs
 267:         // to be crypted.  Otherwise, ignore it.
 268:         if (isset($info['password'])) {
 269:             if (!empty($user['password'])) {
 270:                 // FIXME: Allow choice of hash
 271:                 $info['user_password'] = Horde_Auth::getCryptedPassowrd($info['password'], '', 'ssha', true);
 272:             }
 273:             unset($info['password']);
 274:         }
 275: 
 276:         $tmp['dn'] = $addrinfo['id'];
 277:         foreach ($info as $key => $val) {
 278:             $attr = $this->_fieldmap[$key];
 279:             $tmp[$attr] = $val;
 280:         }
 281: 
 282:         // Bind with appropriate dn to give update access.
 283:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
 284:                          $this->_params['ldap']['bindpw']);
 285:         if (!$res) {
 286:             throw new Vilma_Exception(_("Unable to bind to the LDAP server.  Check authentication credentials."));
 287:         }
 288: 
 289:         // Prepare data.
 290:         $entry['cn'] = $info['user_full_name'];
 291:         // sn is not used operationally but we make an effort to be
 292:         // something sensical.  No guarantees, though.
 293:         $entry['sn'] = array_pop(explode(' ', $info['user_full_name']));
 294:         $entry['mail'] = $info['user_name'] . $info['domain'];
 295:         $entry['uid'] = $entry['mail'];
 296:         $entry['homeDirectory'] = '/srv/vhost/mail/' . $info['domain'] .'/' . $info['user_name'];
 297:         if ($type != 'group' && $type != 'forward') {
 298:             $entry['qmailUID'] = $entry['qmailGID'] = 8;
 299:         }
 300:         $entry['accountstatus'] = $info['user_enabled'];
 301:         if (isset($info['password']) && !empty($info['password'])) {
 302:             // FIXME: Allow choice of hash
 303:             $entry['userPassword'] = Horde_Auth::getCryptedPassword($info['password'], '', 'ssha', true);
 304:         }
 305:         if (isset($objectClassData)) {
 306:             array_shift($objectClassData);
 307:             $entry['objectclass'] = $objectClassData;
 308:         } else {
 309:             $entry['objectclass'] = array(
 310:                 'top',
 311:                 'person',
 312:                 'organizationalPerson',
 313:                 'inetOrgPerson',
 314:                 'hordePerson',
 315:                 'qmailUser');
 316:         }
 317: 
 318:         // Stir in any site-local custom LDAP attributes.
 319:         try {
 320:             $entry = Horde::callHook('getLDAPAttrs', array($entry), 'vilma');
 321:         } catch (Horde_Exception_HookNotSet $e) {
 322:         }
 323:         $rdn = 'mail=' . $entry['mail'];
 324:         $dn = $rdn . ',' . $this->_params['ldap']['basedn'];
 325:         $res = @ldap_modify($this->_ldap, $dn, $entry);
 326:         if ($res === false) {
 327:             throw new Vilma_Exception(sprintf(_("Error modifying account: %s"), @ldap_error($this->_ldap)));
 328:         }
 329: 
 330:         return $dn;
 331:     }
 332: 
 333:     /**
 334:      * Creates a user in the backend.
 335:      *
 336:      * @param array $info  The user information to save.
 337:      *
 338:      * @return array  The user information.
 339:      * @throws Vilma_Exception
 340:      */
 341:     protected function _createUser($info)
 342:     {
 343:         // Bind with appropriate dn to give update access.
 344:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
 345:                          $this->_params['ldap']['bindpw']);
 346:         if (!$res) {
 347:             throw new Vilma_Exception(_("Unable to bind to the LDAP server.  Check authentication credentials."));
 348:         }
 349: 
 350:         // Prepare data.
 351:         $entry['cn'] = $info['user_full_name'];
 352:         // sn is not used operationally but we make an effort to be
 353:         // something sensical.  No guarantees, though.
 354:         $entry['sn'] = array_pop(explode(' ', $info['user_full_name']));
 355:         $entry['mail'] = $info['user_name'] . '@' . $info['domain'];
 356:         // uid must match mail or SMTP auth fails.
 357:         $entry['uid'] = $entry['mail'];
 358:         $entry['homeDirectory'] = '/srv/vhost/mail/' . $info['domain'] .'/' . $info['user_name'];
 359:         $entry['qmailUID'] = $entry['qmailGID'] = 8;
 360:         $entry['objectclass'] = array(
 361:             'top',
 362:             'person',
 363:             'organizationalPerson',
 364:             'inetOrgPerson',
 365:             'hordePerson',
 366:             'qmailUser');
 367:         $entry['accountstatus'] = $info['user_enabled'];
 368:         // FIXME: Allow choice of hash
 369:         $entry['userPassword'] = Horde_Auth::getCryptedPassword($info['password'], '', 'ssha', true);
 370: 
 371:         // Stir in any site-local custom LDAP attributes.
 372:         try {
 373:             $entry = Horde::callHook('getLDAPAttrs', array($entry), 'vilma');
 374:         } catch (Horde_Exception_HookNotSet $e) {
 375:         }
 376:         $rdn = 'mail=' . $entry['mail'];
 377:         $dn = $rdn . ',' . $this->_params['ldap']['basedn'];
 378:         $res = @ldap_add($this->_ldap, $dn, $entry);
 379:         if ($res === false) {
 380:             throw new Vilma_Exception(sprintf(_("Error adding account to LDAP: %s"), @ldap_error($this->_ldap)));
 381:         }
 382: 
 383:         return $dn;
 384:     }
 385: 
 386:     /**
 387:      * Deletes a user.
 388:      *
 389:      * @param integer $user_id  The id of the user to delete.
 390:      *
 391:      * @throws Vilma_Exception
 392:      */
 393:     public function deleteUser($user_id)
 394:     {
 395:         // Get the user's DN.
 396:         $filter  = '(&';
 397:         foreach ($this->_params['ldap']['objectclass'] as $objectclass) {
 398:             // Add each objectClass from parameters.
 399:             $filter .= '(objectclass=' . $objectclass . ')';
 400:         }
 401:         $filter .= '(mail=' . $user_id . ')';
 402:         $filter .= ')';
 403: 
 404:         Horde::logMessage($filter, 'DEBUG');
 405:         $res = @ldap_search($this->_ldap, $this->_params['ldap']['basedn'], $filter);
 406:         if ($res === false) {
 407:             throw new Vilma_Exception(sprintf(_("Error searching LDAP: %s"), @ldap_error($this->_ldap)));
 408:         }
 409:         $res = @ldap_get_entries($this->_ldap, $res);
 410:         if ($res === false) {
 411:             throw new Vilma_Exception(sprintf(_("Error retrieving LDAP results: %s"), @ldap_error($this->_ldap)));
 412:         }
 413: 
 414:         if ($res['count'] === 0) {
 415:             throw new Vilma_Exception(_("Unable to acquire handle on DN.  Aborting delete operation."));
 416:         }
 417:         if ($res['count'] !== 1) {
 418:             throw new Vilma_Exception(_("More than one DN returned.  Aborting delete operation."));
 419:         }
 420: 
 421:         // We now have one unique DN to delete.
 422:         $res = @ldap_delete($this->_ldap, $res[0]['dn']);
 423:         if ($res === false) {
 424:             throw new Vilma_Exception(sprintf(_("Error deleting account from LDAP: %s"), @ldap_error($this->_ldap)));
 425:         }
 426:     }
 427: 
 428:     public function getUserFormAttributes()
 429:     {
 430:         return array(array(
 431:             'label' => _("Account Status"),
 432:             'name' => 'user_enabled',
 433:             'type' => 'enum',
 434:             'required' => true,
 435:             'readonly' => false,
 436:             'description' => null,
 437:             'params' => array(
 438:                 array(
 439:                     'active' => _("Account is active"),
 440:                     'noaccess' => _("Disable Delivery Only"),
 441:                     'disabled' => _("Bounce Incoming Only"),
 442:                     'deleted' => _("Account is disabled"),
 443:                 ),
 444:              ),
 445:              'default' => 'active',
 446:         ));
 447:     }
 448: 
 449:     /**
 450:      * Returns a list of all users, aliases, or groups and forwards for a
 451:      * domain.
 452:      *
 453:      * @param string $domain      Domain on which to search.
 454:      * @param string $type        Only return a specific type. One of 'all',
 455:      *                            'user', 'alias','forward', or 'group'.
 456:      * @param string $key         Sort list by this key.
 457:      * @param integer $direction  Sort direction.
 458:      *
 459:      * @return array Account information for this domain
 460:      */
 461:     protected function _getAddresses($domain, $type = 'all')
 462:     {
 463:         $addresses = array();
 464:         if ($type == 'all' || $type == 'user') {
 465:             $addresses += $this->_getUsers($domain);
 466:         }
 467:         if ($type == 'all' || $type == 'alias') {
 468:             $addresses += $this->_getAliases($domain);
 469:         }
 470:         if ($type == 'all' || $type == 'forward') {
 471:             $addresses += $this->_getGroupsAndForwards('forward', $domain);
 472:         }
 473:         if ($type == 'all' || $type == 'group') {
 474:             $addresses += $this->_getGroupsAndForwards('group', $domain);
 475:         }
 476:         return $addresses;
 477:     }
 478: 
 479:     /**
 480:      * Returns available email address aliases.
 481:      *
 482:      * @param string $target  If passed a domain then return all alias emails
 483:      *                        for the domain, otherwise if passed a user name
 484:      *                        return all virtual emails for that user.
 485:      *
 486:      * @return array  The used email aliases.
 487:      */
 488:     protected function _getAliases($target = null)
 489:     {
 490:         // FIXME: Add static cache
 491: 
 492:         // FIXME: Add preconfigured filter from $this->_params['ldap']
 493:         // Begin filter (cumulative AND).
 494:         $filter  = '(&';
 495:         foreach ($this->_params['ldap']['objectclass'] as $objectclass) {
 496:             // Add each objectClass from parameters
 497:             $filter .= '(objectClass=' . $objectclass . ')';
 498:         }
 499: 
 500:         // Check if filtering only for domain.
 501:         if (strpos($target, '@') === false && !empty($target)) {
 502:             $filter .= '(mailAlternateAddress=*@' . $target . ')';
 503:         } else {
 504:             // Otherwise filter for all aliases.
 505:             $filter .= '(mailAlternateAddress=*)';
 506:             // Restrict the results to $target.
 507:             if (!empty($target)) {
 508:                  // Add user's email.
 509:                 $filter .= '(mail=' . $target . ')';
 510:             }
 511:         }
 512:         // End filter.
 513:         $filter .= ')';
 514: 
 515:         Horde::logMessage($filter, 'DEBUG');
 516:         $res = @ldap_search($this->_ldap, $this->_params['ldap']['basedn'], $filter);
 517:         if ($res === false) {
 518:             throw new Vilma_Exception(sprintf(_("Error searching LDAP: %s"), @ldap_error($this->_ldap)));
 519:         }
 520: 
 521:         $res = @ldap_get_entries($this->_ldap, $res);
 522:         if ($res === false) {
 523:             throw new Vilma_Exception(sprintf(_("Error returning LDAP results: %s"), @ldap_error($this->_ldap)));
 524:         }
 525: 
 526:         $aliases = array();
 527:         // Can't use foreach because of the array format returned by LDAP driver
 528:         for ($e = 0; isset($res[$e]); $entry = $res[$e++]) {
 529:             // If accountStatus is blank it's the same as active
 530:             if (!isset($entry[$this->_fieldmap['user_enabled']][0]) ||
 531:                 $entry[$this->_fieldmap['user_enabled']][0] == 'active') {
 532:                 $curstatus = 'active';
 533:             } else {
 534:                 // accountStatus can also be:
 535:                 // noaccess (receives but cannot pick up mail)
 536:                 // disabled (bounce incoming and deny pickup)
 537:                 // deleted (bounce incoming but allow pickup)
 538:                 $curstatus = $entry[$this->_fieldmap['user_enabled']][0];
 539:             }
 540:             for ($a = 0; isset($entry['mailalternateaddress'][$a]); $mail = @$entry['mailalternateaddress'][$a++]) {
 541:                 $aliases[] = array(
 542:                     'id' => $mail,
 543:                     'type' => 'alias',
 544:                     'user_name' => $mail,
 545:                     'user_full_name' => sprintf(_("Alias for %s"), $entry['mail'][0]),
 546:                     'destination' => $entry['mail'][0],
 547:                     'user_enabled' => $curstatus);
 548:             }
 549:         }
 550: 
 551:         return $aliases;
 552:     }
 553: 
 554:     /**
 555:      * Returns all available groups and forwards unless otherwise specified.
 556:      *
 557:      * If a domain name is passed then limit the results to groups or forwards
 558:      * in that domain.
 559:      *
 560:      * @param string $acquire The default behavior is to acquire both
 561:      *                        groups and forwards; a value of 'group'
 562:      *                        will return only groups and a value of
 563:      *                        'forward' will return only forwards.
 564:      * @param string $domain  The name of the domain from which to fetch.
 565:      *
 566:      * @return array  The available groups and forwards with details.
 567:      */
 568:     protected function _getGroupsAndForwards($acquire = null, $domain = null)
 569:     {
 570:         // FIXME: Add preconfigured filter from $this->_params['ldap']
 571:         // Begin filter (cumulative AND).
 572:         $filter  = '(&';
 573:         foreach ($this->_params['ldap']['objectclass'] as $objectclass) {
 574:             // Add each objectClass from parameters.
 575:             $filter .= '(objectClass=' . $objectclass . ')';
 576:         }
 577: 
 578:         // Only return results which have a forward configured.
 579:         $filter .= '(mailForwardingAddress=*)';
 580: 
 581:         if (!empty($domain)) {
 582:             // mail or mailAlternateAddress.
 583:             $filter .= '(|';
 584:             $filter .= '(mail=*@' . $domain . ')';
 585:             $filter .= '(mailAlternateAddress=*@' . $domain . ')';
 586:             $filter .= ')';
 587:         } else {
 588:             $domain = '_all';
 589:         }
 590: 
 591:         // End filter
 592:         $filter .= ')';
 593: 
 594:         Horde::logMessage($filter, 'DEBUG');
 595:         $res = @ldap_search($this->_ldap, $this->_params['ldap']['basedn'], $filter);
 596:         if ($res === false) {
 597:             throw new Vilma_Exception(sprintf(_("Error searching LDAP: %s"), @ldap_error($this->_ldap)));
 598:         }
 599: 
 600:         $res = @ldap_get_entries($this->_ldap, $res);
 601:         if ($res === false) {
 602:             throw new Vilma_Exception(sprintf(_("Error returning LDAP results: %s"), @ldap_error($this->_ldap)));
 603:         }
 604: 
 605:         $grpfwds[$domain] = array();
 606:         // Can't use foreach because of the array format returned by LDAP driver
 607:         for ($e = 0; isset($res[$e]); $entry = $res[$e++]) {
 608:             $targets = array();
 609:             for ($a = 0; isset($res[$e]['mailforwardingaddress'][$a]); $attr = $res[$e]['mailforwardingaddress'][$a++]) {
 610:                 $targets[] = $attr;
 611:             }
 612:             $type = $entry['mailforwardingaddress']['count'];
 613:             if ($type > 1) {
 614:                 $type = 'group';
 615:             } else {
 616:                 $type = 'forward';
 617:             }
 618:             if ($acquire == 'all' || $type == $acquire) {
 619:                 $grpfwds[$domain][$e] = array(
 620:                     'id'             => $entry['dn'],
 621:                     'type'           => $type,
 622:                     'address'        => $entry[$this->_fieldmap['address']][0],
 623:                     'targets'        => $targets,
 624:                     'user_name'      => $entry[$this->_fieldmap['user_name']][0],
 625:                     'user_full_name' => @$entry[$this->_fieldmap['user_name']][0],
 626:                 );
 627:                 // If accountStatus is blank it's the same as active
 628:                 if (!isset($entry[$this->_fieldmap['user_enabled']][0]) ||
 629:                     $entry[$this->_fieldmap['user_enabled']][0] == 'active') {
 630:                     $grpfwds[$domain][$e]['user_enabled'] = 'active';
 631:                 } else {
 632:                     // accountStatus can also be:
 633:                     // noaccess (receives but cannot pick up mail)
 634:                     // disabled (bounce incoming and deny pickup)
 635:                     // deleted (bounce incoming but allow pickup)
 636:                     $grpfwds[$domain][$e]['user_enabled'] =
 637:                         $entry[$this->_fieldmap['user_enabled']][0];
 638:                 }
 639:             }
 640:         }
 641: 
 642:         return $grpfwds[$domain];
 643:     }
 644: 
 645:     /**
 646:      * Returns an array of information related to the address passed in.
 647:      *
 648:      * @param string $address  Address for which information will be pulled.
 649:      * @param string $type     Address type to request.
 650:      *                         One of 'all', 'user', 'alias', 'forward' or
 651:      *                         'group'.
 652:      *
 653:      * @return array  Array of user information on success or empty array
 654:      *                if the user does not exist.
 655:      * @throws Vilma_Exception if address of that type doesn't exist.
 656:      */
 657:     public function getAddressInfo($address, $type = 'all')
 658:     {
 659:         if ($type != 'alias') {
 660:             return parent::getAddressInfo($address, $type);
 661:         }
 662: 
 663:         // FIXME: Which is faster?  A linear array search or an LDAP search?
 664:         // I think LDAP in this case because we can't assume the domain.
 665:         // Begin filter (cumulative AND).
 666:         $filter = '(&';
 667:         foreach ($this->_params['ldap']['objectclass'] as $objectclass) {
 668:             // Add each objectClass from parameters.
 669:             $filter .= '(objectClass=' . $objectclass . ')';
 670:         }
 671:         $filter .= '(mailAlternateAddress=' . $address . ')';
 672:         // End filter.
 673:         $filter .= ')';
 674: 
 675:         Horde::logMessage($filter, 'DEBUG');
 676:         $res = @ldap_search($this->_ldap, $this->_params['ldap']['basedn'], $filter);
 677:         if ($res === false) {
 678:             throw new Vilma_Exception(sprintf(_("Error searching LDAP: %s"), @ldap_error($this->_ldap)));
 679:         }
 680:         $res = @ldap_get_entries($this->_ldap, $res);
 681:         if ($res === false) {
 682:             throw new Vilma_Exception(sprintf(_("Error returning LDAP results: %s"), @ldap_error($this->_ldap)));
 683:         }
 684: 
 685:         if ($res['count'] !== 1) {
 686:             throw new Vilma_Exception(_("More than one DN returned for this alias.  Please contact an administrator to resolve this error."));
 687:         }
 688: 
 689:         return array(
 690:             'id' => $res[0]['dn'],
 691:             'address' => $address,
 692:             'destination' => $res[0]['mail'][0],
 693:         );
 694:     }
 695: 
 696:     /**
 697:      * Saves or creates alias records for a user.
 698:      *
 699:      * @param array $info  The info used to store the information.
 700:      *                     Required fields are:
 701:      *                     - 'address': The destination address (used for LDAP
 702:      *                       ID lookup).
 703:      *                     - 'alias_address': The alias to create or the new
 704:      *                       data for the modified entry.
 705:      *                     - 'alias': The alias we are modifying, if we are
 706:      *                       modifying an existing one.
 707:      *
 708:      * @throws Vilma_Exception
 709:      */
 710:     public function saveAlias($info)
 711:     {
 712:         $address = $info['address'];
 713:         if (!empty($info['alias'])) {
 714:             $alias = $info['alias'];
 715:             $create = false;
 716:         } else {
 717:             $create = true;
 718:         }
 719:         $alias_address = $info['alias_address'];
 720: 
 721:         $user_res = $this->_searchForUser($address);
 722:         if ($res['count'] === 0) {
 723:             throw new Vilma_Exception(_("Error reading address information from backend."));
 724:         }
 725:         $user = $user_res[0];
 726: 
 727:         // Retrieve the current MAA values.
 728:         if (isset($user_res[0]['mailalternateaddress'])) {
 729:             $maa = $user['mailalternateaddress'];
 730:             unset($maa['count']);
 731:         } else {
 732:             $maa = array();
 733:         }
 734: 
 735:         $oldmaa = $maa;
 736:         if ($create) {
 737:             // Verify that it does not already exist.
 738:             if (in_array($alias_address, $maa)) {
 739:                 throw new Vilma_Exception(_("That alias already exists!"));
 740:             }
 741: 
 742:             // Not there, we create it.
 743:             $maa[] = $alias_address;
 744:         } elseif ($alias != $alias_address) {
 745:             $key = array_search($alias, $maa);
 746:             if ($key === false) {
 747:                 throw new Vilma_Exception(sprintf(_("Existing entry \"%s\" could not be found."), $alias));
 748:             }
 749:             $maa[$key] = $alias_address;
 750:         } else {
 751:             return;
 752:         }
 753:         sort($maa);
 754: 
 755:         $dn = $user['dn'];
 756:         Horde::logMessage("UPDATING: $dn \nOld MAA: " . print_r($oldmaa, true) . "\nNew MAA: " . print_r($maa, true), 'DEBUG');
 757: 
 758:         // Bind with appropriate dn to give update access.
 759:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
 760:                          $this->_params['ldap']['bindpw']);
 761:         if (!$res) {
 762:             throw new Vilma_Exception(_("Unable to bind to the LDAP server. Check authentication credentials."));
 763:         }
 764: 
 765:         $entry['mailAlternateAddress'] = $maa;
 766:         $res = @ldap_modify($this->_ldap, $dn, $entry);
 767:         if ($res === false) {
 768:             throw new Vilma_Exception(sprintf(_("Error modifying account: %s"), @ldap_error($this->_ldap)));
 769:         }
 770:     }
 771: 
 772:     /**
 773:      * Deletes alias records for a given user.
 774:      *
 775:      * @param array $info  The info used to store the information.
 776:      *                     Required fields are:
 777:      *                     - 'address': The destination address (used for LDAP
 778:      *                       ID lookup).
 779:      *                     - 'alias': The alias we are deleting.
 780:      *
 781:      * @throws Vilma_Exception
 782:      */
 783:     public function deleteAlias($info)
 784:     {
 785:         $address = $info['address'];
 786:         $alias = $info['alias'];
 787: 
 788:         $user_res = $this->_searchForUser($address);
 789:         if ($res['count'] === 0) {
 790:             throw new Vilma_Exception(_("Error reading address information from backend."));
 791:         }
 792:         $user = $user_res[0];
 793: 
 794:         // Retrieve the current MAA values.
 795:         if (!isset($user['mailalternateaddress'])) {
 796:             return;
 797:         }
 798: 
 799:         $maa = $user['mailalternateaddress'];
 800:         unset($maa['count']);
 801:         $oldmaa = $maa;
 802:         $key = array_search($alias, $maa);
 803:         if ($key === false) {
 804:             return;
 805:         }
 806: 
 807:         unset($maa[$key]);
 808:         sort($maa);
 809: 
 810:         $dn = $user['dn'];
 811:         Horde::logMessage("UPDATING: $dn \nOld MAA: " . print_r($oldmaa, true) . "\nNew MAA: " . print_r($maa, true), 'DEBUG');
 812: 
 813:         // Bind with appropriate dn to give update access.
 814:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
 815:                          $this->_params['ldap']['bindpw']);
 816:         if (!$res) {
 817:             throw new Vilma_Exception(_("Unable to bind to the LDAP server. Check authentication credentials."));
 818:         }
 819: 
 820:         $entry['mailAlternateAddress'] = $maa;
 821:         $res = @ldap_modify($this->_ldap, $dn, $entry);
 822:         if ($res === false) {
 823:             throw new Vilma_Exception(sprintf(_("Error modifying account: %s"), @ldap_error($this->_ldap)));
 824:         }
 825:     }
 826: 
 827:     /**
 828:      * Saves or creates forward records for a given user.
 829:      *
 830:      * @param array $info  The info used to store the information.
 831:      *                     Required fields are:
 832:      *                     - 'address': The destination address (used for LDAP
 833:      *                       ID lookup).
 834:      *                     - 'forward_address': The forward to create or the
 835:      *                       new data for the modified entry.
 836:      *                     - 'forward': The forward we are modifying, if we are
 837:      *                       modifying an existing one.
 838:      *
 839:      * @throws Vilma_Exception
 840:      */
 841:     public function saveForward($info)
 842:     {
 843:         $address = $info['address'];
 844:         if (!empty($info['forward'])) {
 845:             $forward = $info['forward'];
 846:             $create = false;
 847:         } else {
 848:             $create = true;
 849:         }
 850:         $forward_address = $info['forward_address'];
 851: 
 852:         $user_res = $this->_searchForUser($address);
 853:         if ($res['count'] === 0) {
 854:             throw new Vilma_Exception(_("Error reading address information from backend."));
 855:         }
 856:         $user = $user_res[0];
 857: 
 858:         // Retrieve the current MAA values.
 859:         if (isset($user['mailforwardingaddress'])) {
 860:             $mfa = $user['mailforwardingaddress'];
 861:             unset($mfa['count']);
 862:         } else {
 863:             $mfa = array();
 864:         }
 865: 
 866:         $oldmfa = $mfa;
 867:         if ($create) {
 868:             // Verify that it does not already exist
 869:             if (in_array($forward_address, $mfa)) {
 870:                 throw new Vilma_Exception(sprintf(_("That forward, \"%s\", already exists!"), $forward_address));
 871:             }
 872: 
 873:             // Not there, we create it.
 874:             $mfa[] = $forward_address;
 875:         } elseif ($forward != $forward_address) {
 876:             $key = array_search($forward, $mfa);
 877:             if ($key === false) {
 878:                 throw new Vilma_Exception(sprintf(_("Existing entry \"%s\" could not be found."), $forward));
 879:             }
 880:             $mfa[$key] = $forward_address;
 881:         } else {
 882:             return;
 883:         }
 884:         sort($mfa);
 885: 
 886:         $dn = $user['dn'];
 887:         Horde::logMessage("UPDATING: $dn \nOld MFA: " . print_r($oldmfa, true) . "\nNew MFA: " . print_r($mfa, true), 'DEBUG');
 888: 
 889:         // Bind with appropriate dn to give update access.
 890:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
 891:                          $this->_params['ldap']['bindpw']);
 892:         if (!$res) {
 893:             throw new Vilma_Exception(_("Unable to bind to the LDAP server. Check authentication credentials."));
 894:         }
 895: 
 896:         $entry['mailForwardingAddress'] = $mfa;
 897:         $res = @ldap_modify($this->_ldap, $dn, $entry);
 898:         if ($res === false) {
 899:             throw new Vilma_Exception(sprintf(_("Error modifying account: %s"), @ldap_error($this->_ldap)));
 900:         }
 901:     }
 902: 
 903:     /**
 904:      * Deletes forward records for a given user.
 905:      *
 906:      * @param array $info  The info used to store the information.
 907:      *                     Required fields are:
 908:      *                     - 'address': The destination address (used for LDAP
 909:      *                       ID lookup).
 910:      *                     - 'forward': The forward we are deleting.
 911:      *
 912:      * @throws Vilma_Exception
 913:      */
 914:     public function deleteForward($info)
 915:     {
 916:         $address = $info['address'];
 917:         $forward = $info['forward'];
 918: 
 919:         $user_res = $this->_searchForUser($address);
 920:         if ($res['count'] === 0) {
 921:             throw new Vilma_Exception(_("Error reading address information from backend."));
 922:         }
 923:         $user = $user_res[0];
 924: 
 925:         // Retrieve the current MFA values.
 926:         if (!isset($user['mailforwardingaddress'])) {
 927:             return;
 928:         }
 929: 
 930:         $mfa = $user['mailforwardingaddress'];
 931:         unset($mfa['count']);
 932:         $oldmfa = $mfa;
 933:         $key = array_search($forward, $mfa);
 934:         if ($key === false) {
 935:             return;
 936:         }
 937:         unset($mfa[$key]);
 938:         sort($mfa);
 939: 
 940:         $dn = $user['dn'];
 941:         Horde::logMessage("UPDATING: $dn \nOld MFA: " . print_r($oldmfa, true) . "\nNew MFA: " . print_r($mfa, true), 'DEBUG');
 942:         // Bind with appropriate dn to give update access.
 943:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
 944:                          $this->_params['ldap']['bindpw']);
 945:         if (!$res) {
 946:             throw new Vilma_Exception(_("Unable to bind to the LDAP server. Check authentication credentials."));
 947:         }
 948: 
 949:         $entry['mailForwardingAddress'] = $mfa;
 950:         $res = @ldap_modify($this->_ldap, $dn, $entry);
 951:         if ($res === false) {
 952:             throw new Vilma_Exception(sprintf(_("Error modifying account: %s"), @ldap_error($this->_ldap)));
 953:         }
 954:     }
 955: 
 956:     /**
 957:      * Searchs for a given email account.
 958:      *
 959:      * @param string $email_id The id of the account to be searched for.
 960:      *
 961:      * @return array  Data for given email account on success or no
 962:      *                information found.
 963:      */
 964:     protected function _searchForUser($email_id)
 965:     {
 966:         // Get the user's DN
 967:         $filter  = '(&';
 968:         foreach ($this->_params['ldap']['objectclass'] as $objectclass) {
 969:             // Add each objectClass from parameters.
 970:             $filter .= '(objectclass=' . $objectclass . ')';
 971:         }
 972:         $filter .= '(mail=' . $email_id . '))';
 973: 
 974:         Horde::logMessage($filter, 'DEBUG');
 975:         $res = @ldap_search($this->_ldap, $this->_params['ldap']['basedn'], $filter);
 976:         if ($res === false) {
 977:             throw new Vilma_Exception(sprintf(_("Error searching LDAP: %s"), @ldap_error($this->_ldap)));
 978:         }
 979:         $res = @ldap_get_entries($this->_ldap, $res);
 980:         if ($res === false) {
 981:             throw new Vilma_Exception(sprintf(_("Error retrieving LDAP results: %s"), @ldap_error($this->_ldap)));
 982:         }
 983: 
 984:         if ($res['count'] === 0) {
 985:             throw new Vilma_Exception(_("Unable to acquire handle on DN.  Aborting delete operation."));
 986:         }
 987:         if($res['count'] !== 1) {
 988:             throw new Vilma_Exception(_("More than one DN returned.  Aborting delete operation."));
 989:         }
 990: 
 991:         return $res;
 992:     }
 993: 
 994:     function _connect()
 995:     {
 996:         if (!is_null($this->_ldap)) {
 997:             return;
 998:         }
 999: 
1000:         Horde::assertDriverConfig($this->_params['ldap'], 'storage',
1001:                                   array('ldaphost', 'basedn', 'binddn', 'dn'));
1002: 
1003:         if (!isset($this->_params['ldap']['bindpw'])) {
1004:             $this->_params['ldap']['bindpw'] = '';
1005:         }
1006: 
1007:         $port = isset($this->_params['ldap']['port'])
1008:             ? $this->_params['ldap']['port']
1009:             : 389;
1010: 
1011:         $this->_ldap = ldap_connect($this->_params['ldap']['ldaphost'], $port);
1012:         if (!$this->_ldap) {
1013:             throw new Vilma_Exception("Unable to connect to LDAP server $hostname on $port");
1014:         }
1015:         $res = ldap_set_option($this->_ldap, LDAP_OPT_PROTOCOL_VERSION,
1016:                                $this->_params['ldap']['version']);
1017:         if (!$res) {
1018:             throw new Vilma_Exception(_("Unable to set LDAP protocol version"));
1019:         }
1020:         $res = ldap_bind($this->_ldap, $this->_params['ldap']['binddn'],
1021:                          $this->_params['ldap']['bindpw']);
1022:         if (!$res) {
1023:             throw new Vilma_Exception(_("Unable to bind to the LDAP server.  Check authentication credentials."));
1024:         }
1025:     }
1026: }
1027: 
API documentation generated by ApiGen