Last active
December 15, 2015 10:19
-
-
Save tpokorra/5244642 to your computer and use it in GitHub Desktop.
preparing Kolab for multi domain operation; for more details see http://www.tbits.net/tbits-opensource/kolab3multipledomains.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$hosted_domain = isset($argv[1])?$argv[1]:""; | |
$ldappassword = isset($argv[2])?$argv[2]:""; | |
$hosted_domain_root_dn="dc=".implode(",dc=",explode(".", $hosted_domain)); | |
# Do we have all infos to continue? | |
if($hosted_domain=="" || $hosted_domain_root_dn=="" || $ldappassword == "") { | |
die("Usage: ".$argv[0]." <hosted domain> <ldappasswd>\n". | |
"e.g. ".$argv[0]." kolab.example.org secret\n"); | |
} | |
# attach code from template to /etc/kolab/kolab.conf | |
$conf = file_get_contents("domain.kolab.conf.tpl"); | |
$conf = str_replace('{$hosted_domain}', $hosted_domain, $conf); | |
$conf = str_replace('{$hosted_domain_root_dn}', $hosted_domain_root_dn, $conf); | |
file_put_contents("/etc/kolab/kolab.conf", $conf, FILE_APPEND | LOCK_EX); | |
system("service httpd reload"); | |
# add a admin user for this domain | |
$ds=ldap_connect("localhost") or die("Couldn't connect to LDAP server!"); | |
ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3); | |
ldap_bind($ds, "cn=Directory Manager", "$ldappassword"); | |
$info = array(); | |
$info["ou"] = "ou=people,$hosted_domain_root_dn"; | |
$info["objectclass"] = array(); | |
$info["objectclass"][] = "top"; | |
$info["objectclass"][] = "kolabinetorgperson"; | |
$info["objectclass"][] = "inetorgperson"; | |
$info["objectclass"][] = "mailrecipient"; | |
$info["objectclass"][] = "person"; | |
$info["uid"] = "admin"; | |
$info["cn"] = "Admin"; | |
$info["sn"] = "Admin"; | |
$info["givenname"] = "Admin"; | |
$info["displayName"] = "Admin"; | |
$info["mail"] = "admin@$hosted_domain"; | |
$info["nsroledn"] = "cn=kolab-admin,$hosted_domain_root_dn"; | |
ldap_add($ds, "uid=admin,ou=People,$hosted_domain_root_dn", $info); | |
# add the domain admin to cn=Directory Administrators, of the domain | |
$hosteddomain_da_dn = "cn=Directory Administrators,".$hosted_domain_root_dn; | |
$info = array(); | |
$info["uniquemember"] = array(); | |
$info["uniquemember"][] = "cn=Directory Manager"; | |
$info["uniquemember"][] = "uid=admin,ou=People,$hosted_domain_root_dn"; | |
ldap_modify($ds, $hosteddomain_da_dn, $info); | |
# allow the admin to see the domain in cn=kolab,cn=config | |
$associateddomain_dn="associateddomain=$hosted_domain,cn=kolab,cn=config"; | |
$info = array(); | |
$info["aci"] = array(); | |
$info["aci"][] = "(targetattr =\"*\")(version 3.0;acl \"uid=admin,ou=People,$hosted_domain_root_dn\";allow (read,search) (userdn=\"ldap:///uid=admin,ou=People,$hosted_domain_root_dn\");)"; | |
ldap_modify($ds, $associateddomain_dn, $info); | |
ldap_close($ds); | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[{$hosted_domain}] | |
base_dn = {$hosted_domain_root_dn} | |
primary_mail = %(givenname)s.%(surname)s@%(domain)s | |
autocreate_folders = { | |
'Archive': { | |
'quota': 0 | |
}, | |
'Calendar': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "event.default", | |
'/shared/vendor/kolab/folder-type': "event", | |
}, | |
}, | |
'Configuration': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "configuration.default", | |
'/shared/vendor/kolab/folder-type': "configuration.default", | |
}, | |
}, | |
'Drafts': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "mail.drafts", | |
}, | |
}, | |
'Contacts': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "contact.default", | |
'/shared/vendor/kolab/folder-type': "contact", | |
}, | |
}, | |
'Journal': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "journal.default", | |
'/shared/vendor/kolab/folder-type': "journal", | |
}, | |
}, | |
'Notes': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': 'note.default', | |
'/shared/vendor/kolab/folder-type': 'note', | |
}, | |
}, | |
'Sent': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "mail.sentitems", | |
}, | |
}, | |
'Spam': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "mail.junkemail", | |
}, | |
}, | |
'Tasks': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "task.default", | |
'/shared/vendor/kolab/folder-type': "task", | |
}, | |
}, | |
'Trash': { | |
'annotations': { | |
'/private/vendor/kolab/folder-type': "mail.wastebasket", | |
}, | |
}, | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
if [ -z "$1" ] | |
then | |
echo "call $0 <ldap password for cn=Directory Manager>" | |
exit 1 | |
fi | |
DirectoryManagerPwd=$1 | |
#Removing Canonification from Cyrus IMAP | |
# TODO: could preserve canonification: http://www.intevation.de/pipermail/kolab-users/2012-August/013747.html | |
sed -r -i -e 's/^auth_mech/#auth_mech/g' /etc/imapd.conf | |
sed -r -i -e 's/^pts_module/#pts_module/g' /etc/imapd.conf | |
sed -r -i -e 's/^ldap_/#ldap_/g' /etc/imapd.conf | |
service cyrus-imapd restart | |
#Update Postfix LDAP Lookup Tables | |
# support subdomains too, search_base = dc=%3,dc=%2,dc=%1 | |
# see http://www.intevation.de/pipermail/kolab-users/2013-January/014270.html | |
rm -f /etc/postfix/ldap/*_3.cf | |
for f in `find /etc/postfix/ldap/ -type f -name "*.cf" ! -name "mydestination.cf"`; | |
do | |
f3=${f/.cf/_3.cf} | |
cp $f $f3 | |
sed -r -i -e 's/^search_base = .*$/search_base = dc=%2,dc=%1/g' $f | |
sed -r -i -e 's/^search_base = .*$/search_base = dc=%3,dc=%2,dc=%1/g' $f3 | |
done | |
sed -r -i -e 's#^transport_maps = .*$#transport_maps = ldap:/etc/postfix/ldap/transport_maps.cf, ldap:/etc/postfix/ldap/transport_maps_3.cf#g' /etc/postfix/main.cf | |
sed -r -i -e 's#^virtual_alias_maps = .*$#virtual_alias_maps = $alias_maps, ldap:/etc/postfix/ldap/virtual_alias_maps.cf, ldap:/etc/postfix/ldap/mailenabled_distgroups.cf, ldap:/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_3.cf, ldap:/etc/postfix/ldap/mailenabled_distgroups_3.cf, ldap:/etc/postfix/ldap/mailenabled_dynamic_distgroups_3.cf#g' /etc/postfix/main.cf | |
sed -r -i -e 's#^local_recipient_maps = .*$#local_recipient_maps = ldap:/etc/postfix/ldap/local_recipient_maps.cf, ldap:/etc/postfix/ldap/local_recipient_maps_3.cf#g' /etc/postfix/main.cf | |
service postfix restart | |
# withdraw permissions for all users from the default domain, which is used to manage the domain admins | |
management_domain=`cat /etc/kolab/kolab.conf | grep primary_domain` | |
management_domain=${management_domain:17} | |
cat > ./ldapparam.txt <<END | |
dn: associateddomain=$management_domain,cn=kolab,cn=config | |
changetype: modify | |
delete: aci | |
END | |
ldapmodify -x -h localhost -D "cn=Directory Manager" -w $DirectoryManagerPwd -f ./ldapparam.txt | |
#Configuring Roundcube | |
patch /usr/share/roundcubemail/plugins/kolab_auth/kolab_auth.php kolab_auth.php.patch | |
#sed -r -i -e "s#'ou=People,.*'#'ou=People,'.\$domain_base_dn#g" /etc/roundcubemail/main.inc.php | |
#sed -r -i -e "s#'ou=Groups,.*'#'dc=Groups,'.\$domain_base_dn#g" /etc/roundcubemail/main.inc.php | |
#sed -r -i -e "s#'ou=People,.*'#'ou=People,'.\$domain_base_dn#g" /etc/roundcubemail/kolab_auth.inc.php | |
#sed -r -i -e "s#'ou=Groups,.*'#'dc=Groups,'.\$domain_base_dn#g" /etc/roundcubemail/kolab_auth.inc.php | |
#sed -r -i -e 's#<\?php.*$#<?php if (file_exists(RCUBE_CONFIG_DIR . "/" . $_SERVER["HTTP_HOST"] . "/" . basename(__FILE__))) include_once(RCUBE_CONFIG_DIR . "/" . $_SERVER["HTTP_HOST"] . "/" . basename(__FILE__));#g' /etc/roundcubemail/kolab_auth.inc.php | |
#Configuring syncroton for Active Sync | |
patch /usr/share/kolab-syncroton/lib/plugins/kolab_auth/kolab_auth.php syncroton_kolab_auth.php.patch | |
#Patch WebAdmin for supporting multiple domains per domain admin | |
scriptDir=`pwd` | |
cd /usr/share/kolab-webadmin | |
patch -p0 -i $scriptDir/patchMultiDomainAdmins.patch | |
cd $scriptDir |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- kolab_auth.php.orig 2013-03-26 15:36:56.388999054 +0100 | |
+++ kolab_auth.php 2013-03-26 16:03:38.886999756 +0100 | |
@@ -32,6 +32,7 @@ | |
{ | |
static $ldap; | |
private $data = array(); | |
+ static $base_dn=""; | |
public function init() | |
{ | |
@@ -274,6 +275,9 @@ | |
$pass = $args['pass']; | |
$loginas = trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST)); | |
+ $domain=substr($user,strpos($user, '@') + 1); | |
+ self::$base_dn="dc=".implode(",dc=",explode(".", $domain)); | |
+ | |
if (empty($user) || empty($pass)) { | |
$args['abort'] = true; | |
return $args; | |
@@ -458,6 +462,10 @@ | |
*/ | |
public static function ldap() | |
{ | |
+ if (self::$base_dn == "") { | |
+ return null; | |
+ } | |
+ | |
if (self::$ldap) { | |
return self::$ldap; | |
} | |
@@ -485,6 +493,9 @@ | |
return null; | |
} | |
+ $addressbook['base_dn'] = "ou=People,".self::$base_dn; | |
+ $addressbook['groups']['base_dn'] = "ou=Groups,".self::$base_dn; | |
+ | |
self::$ldap = new kolab_auth_ldap_backend( | |
$addressbook, | |
$rcmail->config->get('ldap_debug'), |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff -uNr orig/kolab-webadmin/lib/api/kolab_api_service_domain_types.php /usr/share/kolab-webadmin/lib/api/kolab_api_service_domain_types.php | |
--- orig/kolab-webadmin/lib/api/kolab_api_service_domain_types.php 2013-04-16 15:50:26.894544164 +0200 | |
+++ /usr/share/kolab-webadmin/lib/api/kolab_api_service_domain_types.php 2013-05-06 12:48:11.922998781 +0200 | |
@@ -64,6 +64,10 @@ | |
'associateddomain' => array( | |
'type' => 'list', | |
), | |
+ 'domainadmin' => array( | |
+ 'type' => 'list', | |
+ 'optional' => 'true', | |
+ ), | |
'inetdomainbasedn' => array( | |
'optional' => 'true', | |
), | |
diff -uNr orig/kolab-webadmin/lib/Auth/LDAP.php /usr/share/kolab-webadmin/lib/Auth/LDAP.php | |
--- orig/kolab-webadmin/lib/Auth/LDAP.php 2013-04-16 15:50:26.893544183 +0200 | |
+++ /usr/share/kolab-webadmin/lib/Auth/LDAP.php 2013-05-07 08:26:33.442981366 +0200 | |
@@ -43,7 +43,7 @@ | |
// Causes nesting levels to be too deep...? | |
//$this->config_set('config_get_hook', array($this, "_config_get")); | |
- $this->config_set("debug", true); | |
+ $this->config_set("debug", false); | |
$this->config_set("log_hook", array($this, "_log")); | |
//$this->config_set("vlv", false); | |
@@ -132,6 +132,18 @@ | |
$_SESSION['user']->user_bind_dn = $result; | |
$_SESSION['user']->user_bind_pw = $password; | |
+ # if the user does not have access to the default domain, set another domain | |
+ $domains = $this->list_domains(); | |
+ $domain = ""; | |
+ foreach ($domains['list'] as $key => $value) { | |
+ $domain = $value['associateddomain']; | |
+ | |
+ if ($domain == $this->domain) { | |
+ break; | |
+ } | |
+ } | |
+ $_SESSION['user']->set_domain($domain); | |
+ | |
return $result; | |
} | |
@@ -143,7 +155,7 @@ | |
if ($domain_info === false) { | |
$this->_domain_add_new($parent_domain, $prepopulate); | |
} | |
- | |
+#TODO store domain admin? | |
return $this->_domain_add_alias($domain, $parent_domain); | |
} | |
else { | |
@@ -151,6 +163,93 @@ | |
} | |
} | |
+ private function ChangeDomainReadCapability($user, $domain, $action='add') | |
+ { | |
+ if (($tmpconn = ldap_connect($this->_ldap_server)) === false) { | |
+ return false; | |
+ } | |
+ | |
+ if (ldap_bind($tmpconn, $_SESSION['user']->user_bind_dn, $_SESSION['user']->user_bind_pw) === false) { | |
+ ldap_close($tmpconn); | |
+ return false; | |
+ } | |
+ | |
+ $associateddomain_dn="associateddomain=$domain,cn=kolab,cn=config"; | |
+ $info = array(); | |
+ $info["aci"] = array(); | |
+ if (!(($sr = ldap_read($tmpconn, $associateddomain_dn, "(aci=*)", array('aci'))) === false)) { | |
+ $entry = ldap_get_entries($tmpconn, $sr); | |
+ if ($entry['count'] > 0) { | |
+ for ($count = 0; $count < $entry[0]['aci']['count']; $count++) { | |
+ if (strpos($entry[0]['aci'][$count], $user) === false) { | |
+ $info['aci'][] = $entry[0]['aci'][$count]; | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ if ($action == 'add') { | |
+ $info["aci"][] = "(targetattr =\"*\")(version 3.0;acl \"$user\";allow (read,search) (userdn=\"ldap:///$user\");)"; | |
+ } | |
+ | |
+ if (ldap_modify($tmpconn, $associateddomain_dn, $info) === false) { | |
+ ldap_close($tmpconn); | |
+ return false; | |
+ } | |
+ | |
+ ldap_close($tmpconn); | |
+ return true; | |
+ } | |
+ | |
+ private function domain_admin_save($domain, $domain_dn, $attributes) { | |
+ $currentdomain_dn = $this->_standard_root_dn($domain[$domain_dn]["associateddomain"]); | |
+ $currentdomain_da_dn = "cn=Directory Administrators,".$currentdomain_dn; | |
+ | |
+ $domain_admins_result = $this->_search($currentdomain_dn, "cn=Directory Administrators*", array("uniqueMember")); | |
+ if ($domain_admins_result != null && count($domain_admins_result) > 0) { | |
+ $domain_admins = $domain_admins_result->entries(true); | |
+ } | |
+ | |
+ if (empty($domain_admins[$currentdomain_da_dn]["uniquemember"])) { | |
+ $domain_admins[$currentdomain_da_dn]["uniquemember"] = Array(); | |
+ } | |
+ | |
+ if (!is_array($domain_admins[$currentdomain_da_dn]["uniquemember"])) { | |
+ $domain_admins[$currentdomain_da_dn]["uniquemember"] = | |
+ (array)($domain_admins[$currentdomain_da_dn]["uniquemember"]); | |
+ } | |
+ | |
+ $info = array(); | |
+ $info["uniquemember"] = array(); | |
+ for ($count = 0; $count < count($attributes["domainadmin"]); $count++) { | |
+ $info["uniquemember"][] = $attributes["domainadmin"][$count]; | |
+ | |
+ if (!in_array($attributes["domainadmin"][$count], $domain_admins[$currentdomain_da_dn]["uniquemember"])) { | |
+ # add read permission to associateddomain in cn=kolab,cn=config | |
+ $this->ChangeDomainReadCapability($attributes["domainadmin"][$count], $domain[$domain_dn]["associateddomain"], 'add'); | |
+ } | |
+ } | |
+ | |
+ # check for removed admins: remove also read permission from associateddomain in cn=kolab,cn=config | |
+ foreach ($domain_admins[$currentdomain_da_dn]["uniquemember"] as $oldadmin) { | |
+ if (!in_array($oldadmin, $attributes["domainadmin"])) { | |
+ if ($oldadmin == "cn=Directory Manager") { | |
+ # make sure that Directory Manager is still in the list | |
+ $info["uniquemember"][] = "cn=Directory Manager"; | |
+ } else { | |
+ # drop read permission to associateddomain in cn=kolab,cn=config | |
+ $this->ChangeDomainReadCapability($oldadmin, $domain[$domain_dn]["associateddomain"], 'remove'); | |
+ } | |
+ } | |
+ } | |
+ | |
+ $result = $this->modify_entry($currentdomain_da_dn, $domain_admins[$currentdomain_da_dn], $info); | |
+ | |
+ if (!$result) { | |
+ return false; | |
+ } | |
+ } | |
+ | |
public function domain_edit($domain, $attributes, $typeid = null) | |
{ | |
$domain = $this->domain_info($domain, array_keys($attributes)); | |
@@ -161,6 +260,12 @@ | |
$domain_dn = key($domain); | |
+ # using isset, because if the array is empty, then we want to drop the domain admins. | |
+ if (isset($attributes["domainadmin"])) { | |
+ $this->domain_admin_save($domain, $domain_dn, $attributes); | |
+ unset($attributes["domainadmin"]); | |
+ } | |
+ | |
// We should start throwing stuff over the fence here. | |
return $this->modify_entry($domain_dn, $domain[$domain_dn], $attributes); | |
} | |
@@ -195,6 +300,7 @@ | |
$this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() uses _search()"); | |
$result = $this->_search($domain_base_dn, $domain_filter, $attributes); | |
$result = $result->entries(true); | |
+ $domain_dn = key($result); | |
} else { | |
$this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() uses _read()"); | |
$result = $this->_read($domain_dn, $attributes); | |
@@ -204,6 +310,25 @@ | |
return false; | |
} | |
+ $currentdomain_dn = $this->_standard_root_dn($result[$domain_dn]["associateddomain"]); | |
+ $currentdomain_da_dn = "cn=Directory Administrators,".$currentdomain_dn; | |
+ | |
+ $domain_admins_result = $this->_search($currentdomain_dn, "cn=Directory Administrators*", array("uniqueMember")); | |
+ if ($domain_admins_result != null && count($domain_admins_result) > 0) { | |
+ $domain_admins = $domain_admins_result->entries(true); | |
+ } | |
+ | |
+ // read domain admins from LDAP, uniqueMembers of Directory Administrators of domain | |
+ $result[$domain_dn]["domainadmin"] = array(); | |
+ if (is_array($domain_admins[$currentdomain_da_dn]["uniquemember"])) { | |
+ foreach ($domain_admins[$currentdomain_da_dn]["uniquemember"] as $domainadmin) { | |
+ $result[$domain_dn]["domainadmin"][] = $domainadmin; | |
+ } | |
+ } | |
+ else { | |
+ $result[$domain_dn]["domainadmin"][] = $domain_admins[$currentdomain_da_dn]["uniquemember"]; | |
+ } | |
+ | |
$this->_log(LOG_DEBUG, "Auth::LDAP::domain_info() result: " . var_export($result, true)); | |
return $result; | |
diff -uNr orig/kolab-webadmin/lib/client/kolab_client_task_domain.php /usr/share/kolab-webadmin/lib/client/kolab_client_task_domain.php | |
--- orig/kolab-webadmin/lib/client/kolab_client_task_domain.php 2013-04-16 15:50:26.895544147 +0200 | |
+++ /usr/share/kolab-webadmin/lib/client/kolab_client_task_domain.php 2013-05-06 12:48:11.925998781 +0200 | |
@@ -224,6 +224,7 @@ | |
$sections = array( | |
'system' => 'domain.system', | |
'other' => 'domain.other', | |
+ 'admins' => 'domain.admins', | |
); | |
// field-to-section map and fields order | |
@@ -231,6 +232,7 @@ | |
'type_id' => 'system', | |
'type_id_name' => 'system', | |
'associateddomain' => 'system', | |
+ 'domainadmin' => 'admins', | |
); | |
//console("domain_form() \$data", $data); | |
diff -uNr orig/kolab-webadmin/lib/kolab_api_service.php /usr/share/kolab-webadmin/lib/kolab_api_service.php | |
--- orig/kolab-webadmin/lib/kolab_api_service.php 2013-04-16 15:50:26.893544183 +0200 | |
+++ /usr/share/kolab-webadmin/lib/kolab_api_service.php 2013-05-06 12:48:11.931998781 +0200 | |
@@ -86,6 +86,9 @@ | |
'associateddomain' => array( | |
'type' => 'list' | |
), | |
+ 'domainadmin' => array( | |
+ 'type' => 'list' | |
+ ), | |
), | |
'fields' => array( | |
'objectclass' => array( |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- kolab_auth.php.orig 2013-04-15 12:07:43.000000000 +0200 | |
+++ kolab_auth.php 2013-04-15 11:10:27.605569812 +0200 | |
@@ -32,6 +32,7 @@ | |
{ | |
private $ldap; | |
private $data = array(); | |
+ private $base_dn=""; | |
public function init() | |
{ | |
@@ -258,6 +259,10 @@ | |
{ | |
$this->load_config(); | |
+ $user=$args['user']; | |
+ $domain=substr($user,strpos($user, '@') + 1); | |
+ $this->base_dn="dc=".implode(",dc=",explode(".", $domain)); | |
+ | |
if (!$this->init_ldap()) { | |
$args['abort'] = true; | |
return $args; | |
@@ -437,6 +442,10 @@ | |
*/ | |
private function init_ldap() | |
{ | |
+ if ($this->base_dn == "") { | |
+ return null; | |
+ } | |
+ | |
if ($this->ldap) { | |
return $this->ldap->ready; | |
} | |
@@ -453,6 +462,10 @@ | |
if (empty($addressbook)) { | |
return false; | |
} | |
+ | |
+ // for supporting multiple domains | |
+ $addressbook['base_dn'] = "ou=People,".$this->base_dn; | |
+ $addressbook['groups']['base_dn'] = "ou=Groups,".$this->base_dn; | |
$this->ldap = new kolab_auth_ldap_backend( | |
$addressbook, |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
OK.. have a fork of this gist with my tweaks. https://gist.github.com/urkle/6489680 my tweaks get password changing in Roundcube working and get the global address book working.