- [kopano](src/kopano/plugin/movetopublicldap.py) Add LDAP support to the [Move to public](https://documentation.kopano.io/kopanocore_administrator_manual/special_kc_configurations.html#move-to-public ) kopano-dagent python plugin.

master
mlan 2021-01-06 18:29:19 +01:00
parent f82c64cd47
commit b9c2529745
9 changed files with 506 additions and 31 deletions

View File

@ -1,3 +1,7 @@
# 1.2.7
- [kopano](src/kopano/plugin/movetopublicldap.py) Add LDAP support to the [Move to public](https://documentation.kopano.io/kopanocore_administrator_manual/special_kc_configurations.html#move-to-public ) kopano-dagent python plugin.
# 1.2.6
- [docker](src/docker/bin/docker-config.sh) Allow the crontab support to work also when the file `/etc/kopano/docker-crontab` is missing.

View File

@ -17,6 +17,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
DOCKER_CRONTAB_DIR=/etc/cron.d \
DOCKER_CONF_DIR1=/etc/kopano \
DOCKER_SMPL_DIR1=/usr/share/doc/kopano/example-config \
DOCKER_PLUG_DIR=/usr/share/kopano-dagent/python/plugins \
DOCKER_CONF_DIR2=/usr/share/z-push \
DOCKER_APPL_LIB=/var/lib/kopano \
DOCKER_APPL_SSL_DIR=/etc/kopano/ssl \
@ -35,6 +36,7 @@ COPY src/*/bin $DOCKER_BIN_DIR/
COPY src/*/entry.d $DOCKER_ENTRY_DIR/
COPY src/*/exit.d $DOCKER_EXIT_DIR/
COPY src/*/config $DOCKER_CONF_DIR1/
COPY src/*/plugin $DOCKER_PLUG_DIR/
#
# Install helpers. Set bash as default shell. Setup syslogs service.
@ -115,6 +117,7 @@ RUN mkdir -p $DOCKER_BUILD_DEB_DIR \
&& for i in $(seq ${DOCKER_BUILD_PASSES}); do echo "\033[1;36mKOPANO CORE INSTALL PASS: $i\033[0m" \
&& dpkg --install --force-depends --skip-same-version --recursive $DOCKER_BUILD_DEB_DIR \
&& apt-get install --yes --no-install-recommends --fix-broken; done \
&& apt-get install --yes --no-install-recommends python3-ldap \
&& mkdir -p /var/lib/kopano/attachments && chown $DOCKER_APPL_RUNAS: /var/lib/kopano/attachments \
&& mkdir -p $DOCKER_APPL_SSL_DIR \
&& mkdir -p $DOCKER_ACME_SSL_DIR \

View File

@ -3,8 +3,9 @@
![travis-ci test](https://img.shields.io/travis/mlan/docker-kopano.svg?label=build&style=popout-square&logo=travis)
![docker build](https://img.shields.io/docker/cloud/build/mlan/kopano.svg?label=build&style=popout-square&logo=docker)
![image Size](https://img.shields.io/docker/image-size/mlan/kopano.svg?label=size&style=popout-square&logo=docker)
![docker stars](https://img.shields.io/docker/stars/mlan/kopano.svg?label=stars&style=popout-square&logo=docker)
![docker pulls](https://img.shields.io/docker/pulls/mlan/kopano.svg?label=pulls&style=popout-square&logo=docker)
![docker stars](https://img.shields.io/docker/stars/mlan/kopano.svg?label=stars&style=popout-square&logo=docker)
![github stars](https://img.shields.io/github/stars/mlan/docker-kopano.svg?label=stars&style=popout-square&logo=github)
This (non official) repository provides dockerized web mail service as well as Exchange ActiveSync (EAS), IMAP, POP3 and ICAL service (and their secure variants IMAPS, POP3S and ICALS). It is based on [Kopano](https://kopano.com) core components, as well as the Kopano WebApp and [Z-Push](http://z-push.org/). The image uses [nightly built packages](https://download.kopano.io/community/) which are provided by the Kopano community.
@ -21,6 +22,7 @@ Hopefully this repository can be retired once the Kopano community make official
- Configuration using environment variables
- Log directed to docker daemon with configurable level
- Built in utility script [run](src/docker/bin/run) helping configuring Kopano components, WebApp and Z-Push
- [Move to public with LDAP lookup](#move-to-public-with-ldap-lookup)
- [Crontab](https://en.wikipedia.org/wiki/Cron) support.
- Health check
- Hook for theming
@ -438,13 +440,64 @@ make app-create_smime
The [Mobile Device Management](https://documentation.kopano.io/webapp_mdm_manual/) WebApp plugin comes pre-installed. With it you can resync, remove, refresh and even wipe your devices, connected via [Exchange ActiveSync (EAS)](https://en.wikipedia.org/wiki/Exchange_ActiveSync).
## Public folders
There are two type of stores (folders containing communication elements); private and public stores. There can only be one public store. It is the Kopano dagent that places incoming messages into mail boxes, that is the private and public stores.
Public folders are managed by the system admin and not by individual users. Users have them mapped automatically. The public folders can be synced via [Exchange ActiveSync (EAS)](https://wiki.z-hub.io/display/ZP/Sharing+folders+and+Read-only) .
With the current Kopano implementation, delivering to the public store is configured separately from normal user management. There is the [move to public](https://documentation.kopano.io/kopanocore_administrator_manual/special_kc_configurations.html#move-to-public) plugin which moves incoming messages to a folder in the public store. It has a static configuration and does not support LDAP lookup.
### Move to public with LDAP lookup
The `mlan/kopano` image include a extended version of the move to public plugin which use LDAP lookup, instead of a static file based lookup. When the plugin [move to public with LDAP](src/kopano/plugin/movetopublicldap.py) is enabled, `DAGENT_PLUGINS=movetopublicldap.py`, the kopano dagent will do two LDAP queries. The first is to search for an entry/user with matching email address. The second, introduced with this plugin, is to get the public folder from this entry. If found the message will be delivered to the public folder otherwise it will be delivered to the mailbox of the user.
Lets demonstrate how delivery to a public folder is configured. With this LDAP entry:
```yaml
dn: uid=public,ou=users,dc=example,dc=com
cn: public
objectClass: top
objectClass: inetOrgPerson
objectClass: kopano-user
sn: public
uid: public
mail: public@example.com
kopanoAccount: 1
kopanoHidden: 1
kopanoSharedStoreOnly: 1
kopanoResourceType: publicStore:Public Stores/public
```
messages to `public@example.com` will be delivered to the public store in `Public Stores/public`.
The central [attribute](https://documentation.kopano.io/kopanocore_administrator_manual/appendix_b.html#appendix-b-ldap-attribute-description) is `kopanoResourceType: publicStore:Public Stores/public`. It contains a token and a folder name. The token match is case sensitive and there must be a colon `:` separating
the token and the public folder name. The folder name can contain space and
sub folders, which are distinguished using a forward slash `/`.
The parameters in `/etc/kopano/ldap.cfg` will be used to arrange the LDAP queries.
The LDAP attribute holding the token and the token itself have the following
default values, which can be modified in `/etc/kopano/movetopublicldap.cfg` if desired.
```yaml
ldap_public_store_attribute = kopanoResourceType
ldap_public_store_attribute_token = publicStore
```
As with other parameters, environment variables can be used to define them: `LDAP_PUBLIC_STORE_ATTRIBUTE=kopanoResourceType` and `LDAP_PUBLIC_STORE_ATTRIBUTE_TOKEN=publicStore`.
## Shared folders
Users can share folders when sufficient permission have been granted. When logged into WebApp with an administrative account (`kopanoAdmin: 1`) you can modify the permissions on users shares and folders. Users can then, when logged into WebApp, open the inbox of other users by selecting `Open Shared Mails`.
The [impersonation](https://wiki.z-hub.io/display/ZP/Impersonation) mechanism allow such shared folders to be synced over [Exchange ActiveSync (EAS)](https://wiki.z-hub.io/display/ZP/Sharing+folders+and+Read-only) too.
## Crontab
The `mlan/kopano` has a [cron](https://en.wikipedia.org/wiki/Cron) service activated. You can use environment variables to set up [crontab](https://man7.org/linux/man-pages/man5/crontab.5.html) entries. Any environment variable name staring with `CRONTAB_ENTRY` will be use to add entries to cron.
One trivial example is `CRONTAB_ENTRY_TEST=* * * * * root logger -t cron -p user.notice "SHELL=$$SHELL, PATH=$$PATH"`.
During the initial configuration procedure any `CRONTAB_ENTRY` will add crontab entries to the file `/etc/kopano/docker-crontab`, all the while previously present entries are deleted. This file defines the `PATH` variable so that you don't need to give full path names to commands in the crontab entry. This is, you need to provide the full path names to commands if this `PATH` definition is missing in the `/etc/kopano/docker-crontab` file.
During the initial configuration procedure any `CRONTAB_ENTRY` will add crontab entries to the file `/etc/kopano/docker-crontab`, all the while previously present entries are deleted. This file defines the `PATH` variable so that you don't need to give full path names to commands in the crontab entry. This is, you need to provide the full path names to commands if this `PATH` definition is missing in the `/etc/kopano/docker-crontab` file.
## Mail transfer agent interaction

View File

@ -15,8 +15,12 @@ LDAP_USERFLT=
LDAP_GROUPOU=groups
LDAP_GROUPOBJ=kopano-group
LDAP_TEST_USER=demo
LDAP_TEST_PASSWD=demo
LDAP_TEST_USERPW=demo
LDAP_TEST_ADMIN=admin
LDAP_TEST_ADMINPW=admin
LDAP_TEST_GROUP=team
LDAP_TEST_PUB=public
LDAP_TEST_SHR=shared
MYSQL_ROOT_PASSWORD=secret
MYSQL_DATABASE=kopano
MYSQL_USER=kopano

View File

@ -76,7 +76,7 @@ wait_%:
web:
firefox localhost:8008 &
auth-init: wait_11 auth-mod_conf auth-add_schema auth-add_data
auth-init: wait_11 auth-mod_conf auth-add_schema auth-add_data auth-add_sto
define LDIF_MOD_CONF
dn: olcDatabase={-1}frontend,cn=config
@ -115,9 +115,27 @@ cn: $(LDAP_TEST_GROUP)
objectClass: top
objectClass: groupOfNames
objectClass: kopano-group
member: uid=$(LDAP_TEST_ADMIN),ou=$(LDAP_USEROU),$(LDAP_BASE)
member: uid=$(LDAP_TEST_USER),ou=$(LDAP_USEROU),$(LDAP_BASE)
mail: $(LDAP_TEST_GROUP)@$(MAIL_DOMAIN)
dn: uid=$(LDAP_TEST_ADMIN),ou=$(LDAP_USEROU),$(LDAP_BASE)
changetype: add
cn: $(LDAP_TEST_ADMIN)
objectClass: top
objectClass: inetOrgPerson
objectClass: kopano-user
sn: $(LDAP_TEST_ADMIN)
uid: $(LDAP_TEST_ADMIN)
mail: $(LDAP_TEST_ADMIN)@$(MAIL_DOMAIN)
userPassword: $(LDAP_TEST_ADMINPW)
telephoneNumber: 0123 987654321
title: System Admin
kopanoAccount: 1
kopanoAdmin: 1
kopanoEnabledFeatures: imap
kopanoEnabledFeatures: pop3
dn: uid=$(LDAP_TEST_USER),ou=$(LDAP_USEROU),$(LDAP_BASE)
changetype: add
cn: $(LDAP_TEST_USER)
@ -127,22 +145,54 @@ objectClass: kopano-user
sn: $(LDAP_TEST_USER)
uid: $(LDAP_TEST_USER)
mail: $(LDAP_TEST_USER)@$(MAIL_DOMAIN)
userPassword: $(LDAP_TEST_PASSWD)
userPassword: $(LDAP_TEST_USERPW)
telephoneNumber: 0123 123456789
title: MCP
title: First User
kopanoAccount: 1
kopanoAdmin: 1
kopanoEnabledFeatures: imap
kopanoEnabledFeatures: pop3
endef
export LDIF_ADD_DATA
define LDIF_ADD_STO
dn: uid=$(LDAP_TEST_SHR),ou=$(LDAP_USEROU),$(LDAP_BASE)
changetype: add
cn: $(LDAP_TEST_SHR)
objectClass: top
objectClass: inetOrgPerson
objectClass: kopano-user
sn: $(LDAP_TEST_SHR)
uid: $(LDAP_TEST_SHR)
mail: $(LDAP_TEST_SHR)@$(MAIL_DOMAIN)
kopanoAccount: 1
kopanoSharedStoreOnly: 1
dn: uid=$(LDAP_TEST_PUB),ou=$(LDAP_USEROU),$(LDAP_BASE)
changetype: add
cn: $(LDAP_TEST_PUB)
objectClass: top
objectClass: inetOrgPerson
objectClass: kopano-user
sn: $(LDAP_TEST_PUB)
uid: $(LDAP_TEST_PUB)
mail: $(LDAP_TEST_PUB)@$(MAIL_DOMAIN)
kopanoAccount: 1
kopanoHidden: 1
kopanoSharedStoreOnly: 1
kopanoResourceType: publicStore:Public Stores/public
endef
export LDIF_ADD_STO
auth-mod_conf:
echo "$$LDIF_MOD_CONF" | docker-compose exec -T auth ldap modify
auth-add_data:
echo "$$LDIF_ADD_DATA" | docker-compose exec -T auth ldap modify
auth-add_sto:
echo "$$LDIF_ADD_STO" | docker-compose exec -T auth ldap modify
auth-add_schema:
docker-compose exec app zcat /usr/share/doc/kopano/kopano.ldif.gz \
| docker-compose exec -T auth ldapadd -H ldapi://%2Fvar%2Frun%2Fopenldap%2Fldapi/ -Y EXTERNAL
@ -162,14 +212,14 @@ auth-show_cat1:
docker-compose exec auth slapcat -n1
auth-gui-up:
docker run -d --rm --name auth-gui --network demo_backend \
docker run -d --name auth-gui --network demo_backend \
-p 127.0.0.1:8001:80 -e PHPLDAPADMIN_LDAP_HOSTS=auth \
-e PHPLDAPADMIN_HTTPS=false osixia/phpldapadmin || true
sleep 2
firefox localhost:8001 &
auth-gui-down:
docker stop auth-gui || true
docker rm -f auth-gui || true
mta-init:
@ -184,7 +234,7 @@ mta-test_smtp:
mta-test_smtps:
printf "From: A tester <test@example.biz>\nTo: <$(LDAP_TEST_USER)@$(MAIL_DOMAIN)>\nDate: $$(date)\nSubject: A SMTPS test message\n\nGreat news! You can receive secure email.\n" \
| curl smtps://localhost -T - --mail-from test@example.biz -k \
-u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) \
-u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) \
--mail-rcpt $(LDAP_TEST_USER)@$(MAIL_DOMAIN) $(curl_dbg)
mta-test_smtp2:
@ -192,6 +242,16 @@ mta-test_smtp2:
| curl smtp://localhost -T - --mail-from test@example.biz \
--mail-rcpt $(LDAP_TEST_USER)@$(MAIL_DOMAIN) $(curl_dbg)
mta-test_shared: all-test_quiet
printf "From: A tester <test@example.biz>\nTo: <$(LDAP_TEST_SHR)@$(MAIL_DOMAIN)>\nDate: $$(date)\nSubject: A SMTP test message\n\nGreat news! A shared store can receive email.\n" \
| curl smtp://localhost -T - --mail-from test@example.biz \
--mail-rcpt $(LDAP_TEST_SHR)@$(MAIL_DOMAIN) $(curl_dbg)
mta-test_public: all-test_quiet
printf "From: A tester <test@example.biz>\nTo: <$(LDAP_TEST_PUB)@$(MAIL_DOMAIN)>\nDate: $$(date)\nSubject: A SMTP test message\n\nGreat news! A public store can receive email.\n" \
| curl smtp://localhost -T - --mail-from test@example.biz \
--mail-rcpt $(LDAP_TEST_PUB)@$(MAIL_DOMAIN) $(curl_dbg)
mta-razor:
docker-compose exec mta run amavis_register_razor
@ -234,15 +294,15 @@ mta-hostaddr:
$(eval myhost := $(call _ip,$(COMPOSE_PROJECT_NAME)_mta_1))
mta-test_auth:
docker-compose exec mta doveadm auth test $(LDAP_TEST_USER) $(LDAP_TEST_PASSWD)
docker-compose exec mta doveadm auth test $(LDAP_TEST_USER) $(LDAP_TEST_USERPW)
mta-test_imap: mta-hostaddr
curl imap://$(myhost) -X CAPABILITY
curl imap://$(myhost) -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD)
curl imap://$(myhost) -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW)
mta-test_rimap:
docker-compose exec mta curl imap://app -X CAPABILITY
docker-compose exec mta curl imap://app -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD)
docker-compose exec mta curl imap://app -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW)
mta-test_ldap: mta-debugtools
docker-compose exec mta ldapsearch -H ldap://auth:389 -xLLL -s base namingContexts
@ -252,7 +312,17 @@ db-init:
db-test:
docker-compose exec db mysqlshow -u $(MYSQL_USER) $(MYSQL_DATABASE) -p$(MYSQL_PASSWORD)
app-init: #wait_21 app-public_store
db-gui-up:
docker run -d --name db-gui --network demo_backend \
-p 127.0.0.1:8002:80 -e PMA_HOST=db \
phpmyadmin/phpmyadmin || true
sleep 2
firefox localhost:8002 &
db-gui-down:
docker rm -f db-gui || true
app-init: #wait_92 app-public_store
app-debugtools:
docker-compose exec app apt-get update
@ -279,22 +349,22 @@ app-test_lmtp: app-hostaddr
app-test_all: all-test_muted app-test_imap app-test_pop3 app-test_ical app-test_imaps app-test_pop3s app-test_icals
app-test_imap: app-hostaddr
curl imap://$(myhost) -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) $(curl_dbg)
curl imap://$(myhost) -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) $(curl_dbg)
app-test_imaps: app-hostaddr
curl imaps://$(myhost) -k -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) $(curl_dbg)
curl imaps://$(myhost) -k -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) $(curl_dbg)
app-test_pop3: app-hostaddr
curl pop3://$(myhost) -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) $(curl_dbg)
curl pop3://$(myhost) -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) $(curl_dbg)
app-test_pop3s: app-hostaddr
curl pop3s://$(myhost) -k -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) $(curl_dbg)
curl pop3s://$(myhost) -k -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) $(curl_dbg)
app-test_ical: app-hostaddr
curl http://$(myhost):8080 -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) $(curl_dbg)
curl http://$(myhost):8080 -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) $(curl_dbg)
app-test_icals: app-hostaddr
curl https://$(myhost):8443 -k -u $(LDAP_TEST_USER):$(LDAP_TEST_PASSWD) $(curl_dbg)
curl https://$(myhost):8443 -k -u $(LDAP_TEST_USER):$(LDAP_TEST_USERPW) $(curl_dbg)
app-test_tls: app-hostaddr
docker run --rm -it --network demo_backend drwetter/testssl.sh app:993 || true
@ -312,7 +382,7 @@ app-create_store:
docker-compose exec app kopano-admin --create-store $(LDAP_TEST_USER)
app-public_store:
docker-compose exec app kopano-storeadm -h default: -P
docker-compose exec app kopano-storeadm -P
$(addprefix app-parms_,archiver dagent gateway ical ldap search server spamd spooler):
docker-compose exec app run list_parms $(patsubst app-parms_%,%,$@)
@ -336,7 +406,7 @@ all-destroy_smime:
%.p12: %.crt
openssl pkcs12 -export -in $< -inkey $*.key -out $@ \
-passout pass:$(LDAP_TEST_PASSWD)
-passout pass:$(LDAP_TEST_USERPW)
%.csr: %.key
openssl req -new -key $< -out $@ \

View File

@ -27,6 +27,7 @@ services:
- LDAP_GROUP_TYPE_ATTRIBUTE_VALUE=${LDAP_GROUPOBJ-posixGroup}
- LDAP_GROUPMEMBERS_ATTRIBUTE_TYPE=dn
- LDAP_PROPMAP=
- DAGENT_PLUGINS=movetopublicldap.py
- MYSQL_DATABASE=${MYSQL_DATABASE-kopano}
- MYSQL_USER=${MYSQL_USER-kopano}
- MYSQL_PASSWORD=${MYSQL_PASSWORD-secret}

View File

@ -18,7 +18,7 @@ DOCKER_MAN5_DIR=${DOCKER_MAN5_DIR-/usr/share/man/man5/}
DOCKER_APPL_SSL_CERT=${DOCKER_APPL_SSL_CERT-$DOCKER_APPL_SSL_DIR/cert.pem}
DOCKER_APPL_SSL_KEY=${DOCKER_APPL_SSL_KEY-$DOCKER_APPL_SSL_DIR/priv_key.pem}
DOCKER_LDAP_PMAP_FILE=${DOCKER_LDAP_PMAP_FILE-/usr/share/kopano/ldap.propmap.cfg}
DOCKER_LDAP_SERVICES=${DOCKER_LDAP_SERVICES-archiver dagent gateway ical ldap search server spamd spooler}
DOCKER_LDAP_SERVICES=${DOCKER_LDAP_SERVICES-archiver dagent gateway ical ldap movetopublicldap search server spamd spooler}
sqlstate_cfg_file=$DOCKER_CONF_DIR2/backend/sqlstatemachine/config.php
zpush_cfg_file=$DOCKER_CONF_DIR2/config.php
webapp_cfg_file=$DOCKER_CONF_DIR1/webapp/config.php
@ -36,6 +36,7 @@ list_parms() {
[ $# -ne 1 ] && echo "# $service"
local man_file=$(kopano_gen_filename_man $service)
kopano_get_envvars_man $man_file
kopano_get_envvars_ext $service
[ $# -ne 1 ] && echo
done
}
@ -48,6 +49,7 @@ list_parms() {
# With kopano-webapp and z-push use installed config file to find valid keys.
#
kopano_apply_envvars_core() {
kopano_enable_envvars_plugin
for service in $DOCKER_LDAP_SERVICES; do
kopano_apply_envvars_cfg $service
done
@ -71,17 +73,18 @@ kopano_apply_envvars_cfg() {
local service=$1
local cfg_file=$(kopano_gen_filename_cfg $service)
local man_file=$(kopano_gen_filename_man $service)
local env_vars="$(kopano_get_envvars_ext $service)"
if [ -f $man_file ]; then
local env_vars="$(kopano_get_envvars_man $man_file)"
if [ -e $cfg_file ]; then
mv -f $cfg_file $cfg_file.bak
fi
for env_var in $env_vars; do
kopano_set_envvars_cfg $service $env_var
done
env_vars="$(kopano_get_envvars_man $man_file) $env_vars"
else
dc_log 4 "Could not find $man_file"
dc_log 6 "Could not find $man_file"
fi
if [ -e $cfg_file ]; then
mv -f $cfg_file $cfg_file.bak
fi
for env_var in $env_vars; do
kopano_set_envvars_cfg $service $env_var
done
}
kopano_apply_envvars_php() {
@ -118,6 +121,35 @@ kopano_set_envvars_cfg() {
fi
}
#
# External parameters
# services without a man file or additional parameters are listed here.
#
kopano_get_envvars_ext() {
local service=$1
local vars
case $service in
movetopublicldap)
vars="ldap_public_store_attribute ldap_public_store_attribute_token"
;;
esac
echo $vars | tr "[:lower:]" "[:upper:]" | tr " " "\n" | sort -u
}
#
# Python plugins
#
kopano_enable_envvars_plugin() {
for service in dagent spooler; do
local envvar=${service^^}_PLUGINS
for plugin in ${!envvar}; do
dc_log 5 "Enabling ${service} python plugin ${plugin}"
ln -sf /usr/share/kopano-${service}/python/plugins/${plugin} /var/lib/kopano/${service}/plugins
export ${service^^}_PLUGIN_ENABLED=yes
done
done
}
#
# Helpers
#

View File

@ -0,0 +1,211 @@
""" movetopublicldap.py
This is an LDAP lookup extension to the move to public plugin.
The move to public plugin moves incoming messages to a folder in the public
store. If folders are missing they will be created.
A LDAP entry including:
kopanoAccount: 1
kopanoResourceType: publicStore:<public folder>
will have its email delivered to the public store in <public folder>.
The token match is case sensitive and there must be a colon ':' separating
the token and the public folder name. The folder name can contain space and
sub folders, which are distinguished using forward slash '/'.
So if we have 'kopanoResourceType: publicStore:Public Stores/public' emails will
be delivered to 'Public Folders/Public Stores/public'.
The parameters in /etc/kopano/ldap.cfg will be used for the LDAP query.
The LDAP attribute holding the token and the token itself have the following
default values, which can be modified in /etc/kopano/movetopublicldap.cfg
if desired.
ldap_public_store_attribute = kopanoResourceType
ldap_public_store_attribute_token = publicStore
"""
from sys import hexversion
from MAPI.Util import GetPublicStore
from MAPI.Struct import NEWMAIL_NOTIFICATION
from MAPI import MAPI_UNICODE, MAPI_MODIFY, OPEN_IF_EXISTS, MDB_WRITE
from MAPI.Tags import (PR_RECEIVED_BY_EMAIL_ADDRESS_W, PR_EC_COMPANY_NAME_W,
PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_ENTRYID, PR_MAILBOX_OWNER_ENTRYID,
IID_IMessage, IID_IExchangeManageStore)
from plugintemplates import IMapiDAgentPlugin, MP_CONTINUE, MP_STOP_SUCCESS
from zconfig import ZConfigParser
import configparser
import ldap
class KConfigParser(ZConfigParser):
""" Extends zconfig.ZConfigParser to also allow !directive in cfg files """
def __init__(self, configfile, defaultconfig={}):
self.config = configparser.ConfigParser(defaults=defaultconfig,
delimiters=('='), comment_prefixes=('#', '!'))
self.readZConfig(configfile)
class MoveToPublic(IMapiDAgentPlugin):
prioPreDelivery = 50
config = {}
CONFIGFILES = ['/etc/kopano/ldap.cfg', '/etc/kopano/movetopublicldap.cfg']
DEFAULTCONFIG = {
'ldap_uri': None,
'ldap_search_base': None,
'ldap_bind_user': None,
'ldap_bind_passwd': None,
'ldap_user_unique_attribute': "uid",
'ldap_public_store_attribute': "kopanoResourceType",
'ldap_public_store_attribute_token': "publicStore"
}
def __init__(self, logger):
IMapiDAgentPlugin.__init__(self, logger)
self.readconfig(self.CONFIGFILES, self.DEFAULTCONFIG)
def readconfig(self, configfiles=CONFIGFILES, defaultconfig=DEFAULTCONFIG):
""" Reads ldap.cfg and movetopublicldap.cfg into self.config """
options = [opt.split('_', 1)[1] for opt in defaultconfig.keys()]
config = None
for configfile in configfiles:
if not config:
config = KConfigParser(configfile, defaultconfig)
else:
config = KConfigParser(configfile, config.options())
self.config = config.getdict('ldap',options)
self.logger.logDebug("*--- Config list {}".format(self.config))
return self.config
def searchfilter(self, recipient):
""" (&(uid=recipient)(kopanoResourceType=publicStore:*)) """
return ("(&({}={})({}={}:*))"
.format(self.config['user_unique_attribute'],
recipient,
self.config['public_store_attribute'],
self.config['public_store_attribute_token']))
def searchquery(self, recipient):
""" Query a LDAP/AD driectory server to lookup recipient using
search_base and return public_store_attribute
"""
if (self.config['uri'] is None):
self.logger.logError(("!--- ldap_uri is not defined."
" Please check {}" .format(self.CONFIGFILES[0])))
return None
else:
l = ldap.initialize(self.config['uri'])
try:
l.protocol_version = ldap.VERSION3
l.simple_bind_s(self.config['bind_user'] or u'', \
self.config['bind_passwd'] or u'')
except ldap.SERVER_DOWN as e:
self.logger.logError(("!--- LDAP server is not reachable {}"
.format(e)))
return None
except ldap.INVALID_CREDENTIALS as e:
self.logger.logError(("!--- Invalid LDAP credentials {}"
" Please check {}" .format(e, self.CONFIGFILES[0])))
l.unbind_s()
return None
except ldap.LDAPError as e:
self.logger.logError("!--- LDAPError {}".format(e))
l.unbind_s()
return None
try:
result = l.search_s(self.config['search_base'], \
ldap.SCOPE_SUBTREE, self.searchfilter(recipient), \
[self.config['public_store_attribute']])
except ldap.LDAPError as e:
self.logger.logError("!--- LDAPError {}".format(e))
l.unbind_s()
return result
def publicfolder(self, recipient):
""" Check for ldap_public_store_attribute_token and return folder """
destination_folder = []
result = self.searchquery(recipient)
if result:
tokenandfolder = (result[0][1]
.get(self.config['public_store_attribute'])[0].decode('utf-8'))
if tokenandfolder:
destination_folder = tokenandfolder.split(':')[1]
if destination_folder:
self.logger.logDebug(("*--- Found public folder {}"
"for recipient {}".format(
destination_folder.encode('utf-8'),
recipient.encode('utf-8'))))
return destination_folder
def PreDelivery(self, session, addrbook, store, folder, message):
props = message.GetProps([PR_RECEIVED_BY_EMAIL_ADDRESS_W], 0)
if props[0].ulPropTag != PR_RECEIVED_BY_EMAIL_ADDRESS_W:
self.logger.logError("!--- Not received by emailaddress")
return MP_CONTINUE,
recipient = props[0].Value.lower()
if not recipient:
self.logger.logError("!--- No recipient in props {}".format(props))
return MP_CONTINUE,
recipfolder = self.publicfolder(recipient)
if not recipfolder:
self.logger.logDebug(("*--- No public folder for recipient {}"
.format(recipient.encode('utf-8'))))
return MP_CONTINUE,
publicstore = GetPublicStore(session)
if not publicstore:
storeprops = store.GetProps([PR_MAILBOX_OWNER_ENTRYID], 0)
if storeprops[0].ulPropTag == PR_MAILBOX_OWNER_ENTRYID:
user = addrbook.OpenEntry(storeprops[0].Value, None, 0)
userprops = user.GetProps([PR_EC_COMPANY_NAME_W], 0)
if userprops[0].ulPropTag == PR_EC_COMPANY_NAME_W:
companyname = userprops[0].Value
else:
companyname = None
if not companyname:
self.logger.logError(("!--- Cannot open a public store."
' Use "kopano-storeadm -P"'
" to create one if it is missing."))
return MP_CONTINUE,
ema = store.QueryInterface(IID_IExchangeManageStore)
publicstoreid = ema.CreateStoreEntryID(None, companyname, MAPI_UNICODE)
publicstore = session.OpenMsgStore(0, publicstoreid, None, MDB_WRITE)
publicfolders = publicstore.OpenEntry(
publicstore.GetProps([PR_IPM_PUBLIC_FOLDERS_ENTRYID], 0)[0].Value,
None, MAPI_MODIFY)
folderlist = recipfolder.split('/')
folder = publicfolders
for foldername in folderlist:
if len(foldername) > 0:
if hexversion >= 0x03000000:
folder = folder.CreateFolder(0, foldername,
"Create by Move to Public plugin", None,
OPEN_IF_EXISTS | MAPI_UNICODE)
else:
folder = folder.CreateFolder(0, foldername,
"Create by Move to Public plugin", None, OPEN_IF_EXISTS)
msgnew = folder.CreateMessage(None, 0)
tags = message.GetPropList(MAPI_UNICODE)
message.CopyProps(tags, 0, None, IID_IMessage, msgnew, 0)
msgnew.SaveChanges(0)
folderid = folder.GetProps([PR_ENTRYID], 0)[0].Value
msgid = msgnew.GetProps([PR_ENTRYID], 0)[0].Value
publicstore.NotifyNewMail(NEWMAIL_NOTIFICATION(msgid, folderid, 0, None, 0))
self.logger.logInfo(("*--- Message moved to public folder {}"
.format(recipfolder)))
return MP_STOP_SUCCESS,

View File

@ -0,0 +1,97 @@
"""
This code will query a LDAP/AD driectory server and updated the cgf file
/etc/kopano/movetopublic.cfg
/usr/share/kopano-dagent/python/plugins/movetopublic.cfg
export PYTHONPATH=/usr/share/kopano-dagent/python
"""
from zconfig import ZConfigParser
import configparser
import ldap
class KConfigParser(ZConfigParser):
""" allow !directive in cfg files """
def __init__(self, configfile, defaultconfig={}):
self.config = configparser.ConfigParser(defaults=defaultconfig, \
delimiters=('='), comment_prefixes=('#', '!'))
self.readZConfig(configfile)
class ldapstores():
defaultconfig = {
'ldap_uri': None,
'ldap_search_base': None,
'ldap_bind_user': None,
'ldap_bind_passwd': None,
'ldap_user_unique_attribute': "uid",
'ldap_user_search_filter': "(kopanoAccount=1)",
# 'ldap_user_search_filter': "(&(kopanoAccount=1)(kopanoResourceType=publicStore:*))",
'ldap_public_store_attribute': "kopanoResourceType",
'ldap_public_store_attribute_token': "publicStore"
}
def __init__(self, configfile = '/etc/kopano/ldap.cfg'):
self.readconfig(configfile)
def readconfig(self, configfile):
config = KConfigParser(configfile, self.defaultconfig)
options = [opt.split('_', 1)[1] for opt in self.defaultconfig.keys()]
self.config = config.getdict('ldap',options)
return self.config
def searchquery(self):
if (self.config['uri'] is None):
print ("ldap_uri is None")
sys.exit(0)
else:
l = ldap.initialize(self.config['uri'])
try:
l.protocol_version = ldap.VERSION3
l.simple_bind_s(self.config['bind_user'] or u'', \
self.config['bind_passwd'] or u'')
except ldap.INVALID_CREDENTIALS:
sys.exit(0)
except ldap.LDAPError as e:
print (e)
sys.exit(0)
try:
ldap_result_id = l.search(self.config['search_base'], \
ldap.SCOPE_SUBTREE, self.config['user_search_filter'], \
[self.config['user_unique_attribute'], \
self.config['public_store_attribute']])
results = []
while 1:
result_type, result_data = l.result(ldap_result_id, 0)
if (result_data == []):
break
else:
if result_type == ldap.RES_SEARCH_ENTRY:
results.append(result_data[0])
except ldap.LDAPError as e:
print (e)
l.unbind_s()
return results
def findpublic(self):
stores = self.searchquery()
public = []
for store in stores:
recipient = store[1].get(self.config['user_unique_attribute'])
tokenandfolder = store[1].get(self.config['ldap_public_store_attribute'])
if tokenandfolder:
token = tokenandfolder.split(':')[0]
destination_folder = tokenandfolder.split(':')[1]
if (token == self.config['ldap_public_store_attribute_token']);
public[recipient] = destination_folder
return public
def printpublic(self, outputfile = '/etc/kopano/movetopublic.cfg'):
public = self.findpublic()
i = 1
for recipient in public.keys():
print ("rule%d_recipient = %s", i, recipient)
print ("rule%d_destination_folder = %s", i, public[recipient])
i += 1
s = ldapstores()
s.printpublic()