From b9c2529745c9529ee4f6955c1518972ba6012c66 Mon Sep 17 00:00:00 2001 From: mlan Date: Wed, 6 Jan 2021 18:29:19 +0100 Subject: [PATCH] - [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. --- CHANGELOG.md | 4 + Dockerfile | 3 + README.md | 57 ++++++- demo/.env | 6 +- demo/Makefile | 108 ++++++++++--- demo/docker-compose.yml | 1 + src/kopano/entry.d/10-kopano-common | 50 ++++-- src/kopano/plugin/movetopublicldap.py | 211 ++++++++++++++++++++++++++ src/notused/plugin/ldaptocfg.py | 97 ++++++++++++ 9 files changed, 506 insertions(+), 31 deletions(-) create mode 100644 src/kopano/plugin/movetopublicldap.py create mode 100644 src/notused/plugin/ldaptocfg.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e6122..7f92cf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Dockerfile b/Dockerfile index 69fc97b..34ba7c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/README.md b/README.md index 2ffd494..9a86450 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/demo/.env b/demo/.env index ba74c99..0af278e 100644 --- a/demo/.env +++ b/demo/.env @@ -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 diff --git a/demo/Makefile b/demo/Makefile index c11a604..188dc95 100644 --- a/demo/Makefile +++ b/demo/Makefile @@ -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 \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 \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 \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 $@ \ diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 7a1b3fc..82c088b 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -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} diff --git a/src/kopano/entry.d/10-kopano-common b/src/kopano/entry.d/10-kopano-common index 05cd978..100d9fe 100755 --- a/src/kopano/entry.d/10-kopano-common +++ b/src/kopano/entry.d/10-kopano-common @@ -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 # diff --git a/src/kopano/plugin/movetopublicldap.py b/src/kopano/plugin/movetopublicldap.py new file mode 100644 index 0000000..f2859c5 --- /dev/null +++ b/src/kopano/plugin/movetopublicldap.py @@ -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: + +will have its email delivered to the public store in . +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, diff --git a/src/notused/plugin/ldaptocfg.py b/src/notused/plugin/ldaptocfg.py new file mode 100644 index 0000000..db4e49c --- /dev/null +++ b/src/notused/plugin/ldaptocfg.py @@ -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()