diff --git a/conf/kolab.conf b/conf/kolab.conf index 4b590b9..afe318f 100644 --- a/conf/kolab.conf +++ b/conf/kolab.conf @@ -1,452 +1,494 @@ [kolab] ; Set this to the primary domain name space served within this Kolab Groupware ; deployment. primary_domain = example.org ; This is the primary authentication mechanism used, and contains the list of ; domain name spaces for this deployment. Each domain name space may have its ; own auth_mechanism setting. ; ; Valid options currently include only 'ldap'. auth_mechanism = ldap ; The IMAP backend to use - currently supported values include only ; 'cyrus-imap'. imap_backend = cyrus-imap ; The default locale for this Kolab Groupware installation default_locale = en_US ; Synchronization interval - describes the number of seconds to wait in ; between non-persistent synchronization attempts. Relevant only for ; deployments that lack persistent search and syncrepl ldap controls. sync_interval = 300 ; Synchronization interval for domains - describes the number of seconds ; to wait in between polls for new and deleted domain name spaces. domain_sync_interval = 600 ; The policy to use when originally composing the uid attribute value. ; Normally '%(surname)s.lower()', the transliterated value of the 'sn', ; in all lower-case. ; ; Other examples include: ; ; policy_uid = '%(givenname)s'[0:1]%(surname)s.lower() ; policy_uid = %(givenname)s policy_uid = %(surname)s.lower() ; Primary and secondary recipient address policies. This is called the ; recipient policy as documented in: ; ; http://docs.kolab.org/administrator-guide/configuring-the-kolab-server.html#recipient-policy ; ; Note this is the global default, and each [$domain] section can have ; their own (as in this default configuration, see [example.org]). primary_mail = %(surname)s@%(domain)s secondary_mail = { 0: { "{0}.{1}@{2}": "format('%(givenname)s'[0:1].capitalize(), '%(surname)s', '%(domain)s')" }, 1: { "{0}@{1}": "format('%(uid)s', '%(domain)s')" }, 2: { "{0}@{1}": "format('%(givenname)s.%(surname)s', '%(domain)s')" } } ; To disable the application of the recipient policy by the daemon ('kolabd' service), ; uncomment the next line. ;daemon_rcpt_policy = False ; A global default for folders to create in addition to the INBOX ; folder. autocreate_folders = { 'Archive': { 'quota': 0, 'partition': 'archive' }, 'Calendar': { 'annotations': { '/private/vendor/kolab/folder-type': "event.default", '/shared/vendor/kolab/folder-type': "event", }, }, 'Calendar/Personal Calendar': { 'annotations': { '/shared/vendor/kolab/folder-type': "event", }, }, 'Configuration': { 'annotations': { '/private/vendor/kolab/folder-type': "configuration.default", '/shared/vendor/kolab/folder-type': "configuration", }, }, 'Contacts': { 'annotations': { '/private/vendor/kolab/folder-type': "contact.default", '/shared/vendor/kolab/folder-type': "contact", }, }, 'Contacts/Personal Contacts': { 'annotations': { '/shared/vendor/kolab/folder-type': "contact", }, }, 'Drafts': { 'annotations': { '/private/vendor/kolab/folder-type': "mail.drafts", }, }, 'Files': { 'annotations': { '/private/vendor/kolab/folder-type': "file.default", '/shared/vendor/kolab/folder-type': "file", }, }, '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", }, }, } [autodiscover] ; service hosts (%d will be replaced by user domain) ; for imap/pop3/smtp protocol prefix and port is required activesync = activesync.%d imap = ssl://imap.%d:993 pop3 = ssl://pop3.%d:995 smtp = ssl://smtp.%d:465 ; LDAP attribute used as login login_attribute = mail ; optional service name service_name = Kolab Groupware service_short = Kolab ; enables HTTP/LDAP debugging ;debug_mode = trace [imap] virtual_domains = userid [ldap] ; The URI to LDAP ldap_uri = ldap://localhost:389 ; A list of integers containing supported controls, to increase the efficiency ; of individual short-lived connections with LDAP. supported_controls = 0,2,3 ; The base dn for the deployment. Note that this is the highest level in the ; tree Kolab will ever go. Should your OU structure allow it, you could set this ; to ou=Kolab,ou=Not-So-Private,dc=example,dc=org. base_dn = dc=example,dc=org ; The (administrative) bind dn and corresponding password. ; ; Feel free to set this to a DN with only read permissions on the tree. These ; credentials are used by the Kolab Daemon only, as it might need to set ; additional attributes in order to apply plugins successfully. Such attributes ; could include the first two values in the 'mail_attributes' list (see further ; down) to complete the 'recipient_policy' (see further down), mail quota, ; the mail server attribute, and others. bind_dn = cn=Directory Manager bind_pw = Welcome123 ; Bind DN and password used for services. The DN should have read and search ; privileges only, but should be able to read all relevant parts of the tree. ; ; These credentials are used by, among others, Postfix, Wallace, programs that ; need to find the user DN before binding as the user (including the webadmin ; API, Roundcube, Syncroton). service_bind_dn = uid=kolab-service,ou=Special Users,%(base_dn)s service_bind_pw = wc18bqshFmifGtN ; The base DN, search scope and filter to use when searching for users of any ; type. User types are of primary purpose to the web admin (API), but the ; generic base DN, scope and filter allow us to configure other services as ; well, including Address Books in Roundcube and for Syncroton, the list of ; users in the web admin (API), etc. user_base_dn = ou=People,%(base_dn)s user_scope = sub user_filter = (objectclass=inetorgperson) ; The base DN, scope and filter to use when searching for users of the 'kolab' ; type. This filter is preferred when searching for Kolab users specifically, ; such as in the synchronisation between LDAP and IMAP. Also, it is ; (preferrably) only Kolab users that are allowed to login, use the SMTP server, ; etc. ; ; Note that all user_* settings are valid, and those not available with a kolab_ ; prefix fall back to using the generic user_* equivalent setting. kolab_user_base_dn = ou=People,%(base_dn)s kolab_user_filter = (objectclass=kolabinetorgperson) ; Add additional _user_base_dn, _user_scope and _user_filter. ; Useful for configuring sub-address books, and for the webadmin API when adding ; new users of the example type key 'posix' - the new user will be added in the ; OU configured below. ;posix_user_base_dn = ou=POSIX Accounts,ou=People,%(base_dn)s ;posix_user_scope = one ;posix_user_filter = (&(objectclass=posixaccount)(uidnumber>=1000)) ; The same as for users, but applicable to groups group_base_dn = ou=Groups,%(base_dn)s group_filter = (|(objectclass=groupofuniquenames)(objectclass=groupofurls)) group_scope = sub kolab_group_filter = (|(objectclass=kolabgroupofuniquenames)(objectclass=kolabgroupofurls)) ; Same again sharedfolder_base_dn = ou=Shared Folders,%(base_dn)s sharedfolder_filter = (objectclass=kolabsharedfolder) ; The attribute entry name that controls the ACLs set on a shared folder sharedfolder_acl_entry_attribute = acl ; Same again. Resources live in a different OU structure or; ; ; - They would appear in the address book(s) as distribution lists or individual contacts, ; - Groups or individual users would appear to be Resources. ; resource_base_dn = ou=Resources,%(base_dn)s resource_filter = (|%(group_filter)s(objectclass=kolabsharedfolder)) ; The base DN, scope and filter to use when searching for additional domain ; name spaces in this environment. domain_base_dn = cn=kolab,cn=config domain_filter = (&(associatedDomain=*)) domain_name_attribute = associateddomain ; Attribute that holds the root dn for the domain name space. If this attribute ; does not exist, a standard root dn is formed from the primary domain name ; space (the value in the RDN), as follows: ; ; 'dc=' + ',dc='.join(domainname.split('.')) ; ; or, in example: ; ; domain: example.org ; root dn: dc=example,dc=org domain_rootdn_attribute = inetdomainbasedn ; The attribute that holds the quota. quota_attribute = mailquota ; The format of the modifytimestamp attribute values modifytimestamp_format = %Y%m%d%H%M%SZ ; A unique attribute that can be used to identify the entry beyond renames and ; moves. Note that 'nsuniqueid' is specific to all Netscape-based directory ; services. ; ; For OpenLDAP, use 'entrydn' - the 'entryUUID' can regrettably not be searched ; with. ; ; For Active Directory, use 'objectsid'. unique_attribute = nsuniqueid ; Attribute names that hold valid, internal recipient addresses. Note the use ; of mail and alias frees up the use of mailAlternateAddress to contain a user's ; external email address. ; ; Syntax is a comma- or comma-space separated list. ; ; The first value is used for the purpose of a single "primary" email address, ; that could be subject to a recipient policy, the second is used for the ; purpose of one or more secondary mail addresses, that could also be subject to ; a recipient policy. mail_attributes = mail, alias ; The attribute that holds the FQDN to the mail server the folder exists on mailserver_attribute = mailhost ; Attributes that hold valid authentication login names. Use 'mail', 'alias' and ; optionally 'uid' (the uid is marked as an auth_attribute automatically), so ; that a user can login with; ; ; - uid (i.e. 'jdoe'), ; - mail, fully qualified and localpart only (i.e. "john.doe" and ; "john.doe@example.org"), ; - alias, fully qualified and localpart only (i.e. "j.doe" and ; "j.doe@example.org). auth_attributes = mail, alias, uid ; Virtual List View control, and Server-Side Sorting control configuration. ; ; Configure these to allow the Web Administration Panel (API) to not have to ; search a non-database cn=config for the VLV configuration. ; ;vlv = [ ; { ; 'ou=People,dc=example,dc=org': { ; 'scope': 'sub', ; 'filter': '(objectclass=inetorgperson)', ; 'sort' : [ ; [ ; 'displayname', ; 'sn', ; 'givenname', ; 'cn' ; ] ; ] ; } ; }, ; { ; 'ou=Groups,dc=example,dc=org': { ; 'scope': 'sub', ; 'filter': '(objectclass=groupofuniquenames)', ; 'sort' : [ ; [ ; 'cn' ; ] ; ] ; } ; }, ; ] [kolab_smtp_access_policy] cache_uri = mysql://user:pass@localhost/database cache_retention = 86400 ; To allow users to also send using email addresses in domain name spaces not ; in their own parent and/or alias domains, add 'mailalternateaddress' to this ; list. address_search_attrs = mail, alias ; Prepend the Sender: and/or X-Sender header(s) if the user authenticated is a ; designated delegatee of the envelope sender address? delegate_sender_header = True ; Prepend the Sender: and/or X-Sender header(s) if the user authenticated ; is using an envelope sender address that is a secondary recipient email ; address (attached to the object entry) of the user authenticated? alias_sender_header = True ; Prepend the Sender: header? Only relevant if delegate_sender_header or ; alias_sender_header is set to True. sender_header = True ; Prepend the X-Sender: header? Only relevant if delegate_sender_header or ; alias_sender_header is set to True. xsender_header = True ; "Encrypt" -- read, "obscure" -- the contents of the 'Sender:' and/or ; 'X-Sender:' header(s). Note that this invalidates client's use of the header ; value, and therefore replaces both headers with 'X-Authenticated-As'. ; ; Example: 'vanmeeuwen@kolabsys.com' becomes '6crb3dHK6ODS3qzQ4tXO0t_e5pfQ39k=' ; ; sender_header_enc_key = 'simple' ; Allow hosts in these networks to submit messages with empty envelope senders, ; such as web-clients responding to MDN requests. empty_sender_hosts = 3.2.1.0/24, 6.6.6.0/24 ; Section for Hosted client interface settings. This is not enabled by default. ;[kolab_hosting] ; ;; Set the default domain name space for the list of domain name spaces (if more ;; than one) that new users that register are allowed to select. ;primary_domain = somedomain.tld ; ;; The following bind credentials should be allowed to search ;; "ldap/domain_base_dn" (i.e. cn=kolab,cn=config), but should not be allowed to ;; read any domain name space LDAP entry that users are not eligible to select. ;; ;; Note that the bind credentials usually live in the upper ;; "kolab/primary_domain". ;bind_dn = uid=hosted-service,ou=Special Users,dc=kolab,dc=net ;bind_pw = bla ;recaptcha_private_key = bla ;recaptcha_public_key = bla [kolab_wap] skin = default sql_uri = mysql://user:pass@localhost/database ; Use the following setting to indicate the API is installed on a different ; system, or in a non-standard location. ;api_url = http://localhost/kolab-webadmin/api ; Configure SSL should you want to have the web admin panel (client interface) ; use the API over HTTPS. ; ; By default, httpd and coconspirators are setup to use self-signed certificates, ; so the following two settings are set to false by default. ssl_verify_peer = false ssl_verify_host = false ;ssl_cafile = /path/to/ca/file ;ssl_capath = /path/to/ca/dir ;ssl_local_cert = /path/to/local/cert ;ssl_passphrase = MyPassword [cyrus-imap] ; The URI to use to connect to IMAP. Note that pykolab itself can detect whether ; or not Cyrus IMAP is deployed in a Murder topology, and should be able to ; connect to individual backends as well. uri = imaps://localhost:993 ; The login username to use for global administration. admin_login = cyrus-admin ; The corresponding password. admin_password = Welcome123 [cyrus-sasl] ; The user canonification result attribute. result_attribute = mail [wallace] -modules = resources, invitationpolicy, footer -footer_text = /etc/kolab/footer.text -footer_html = /etc/kolab/footer.html +; List the modules to load and apply, in order. +; +; Available modules include; +; +; * resources +; * invitationpolicy +; * footer +; * signature +; +modules = resources, invitationpolicy + +; Footer module settings +;footer_text = /etc/kolab/footer.text +;footer_html = /etc/kolab/footer.html + +; Signature module settings +; +; Two modes: write out the exact signature in /etc/kolab/ as html and/or text, +; or use rules. +; +; If files are configured, rules do not apply. If files are configured, a +; fallback is /etc/kolab/signature_default.{html,txt}. +; +; signature_file_html = /etc/kolab/signature.d/%(mail)s.html +; signature_file_text = /etc/kolab/signature.d/%(mail)s.txt +; +; A list of dicts, with each dict holding an attribute name ("o", "cn", +; "entrydn"), and a regular expression to be matched against the attribute +; value. +; +; The module takes the first match, and uses the "html" and "text" files as +; templates. +; +;signature_rules = [ +; { +; "entrydn": "uid=.*,ou=IT,ou=People,dc=example,dc=org", +; "html": "/etc/kolab/signature_IT.html", +; "text": "/etc/kolab/signature_IT.txt" +; }, +; { +; "entrydn": "uid=.*,ou=Finance,ou=People,dc=example,dc=org", +; "html": "/etc/kolab/signature_Finance.html", +; "text": "/etc/kolab/signature_Finance.txt" +; } +; ] ; default settings for kolabInvitationPolicy LDAP attribute on user records kolab_invitation_policy = ACT_ACCEPT_IF_NO_CONFLICT:example.org, ACT_MANUAL ; automatically update the participant status from iTip REPLY messages ; in the personal calendars of all other listed participants. invitationpolicy_autoupdate_other_attendees_on_reply = false ; number of days past their end resource calendar events should be kept resource_calendar_expire_days = 100 ; This is a domain name space specific section, that enables us to override ; all settings, for example, the LDAP URI, base and bind DNs, scopes, filters, ; etc. Note that overriding the LDAP settings for the primary domain name space ; does not make any sense. [example.org] default_quota = 1048576 primary_mail = %(givenname)s.%(surname)s@%(domain)s diff --git a/conf/signature_IT.html b/conf/signature_IT.html new file mode 100644 index 0000000..07ad4dd --- /dev/null +++ b/conf/signature_IT.html @@ -0,0 +1,13 @@ +
+
+This is an example HTML signature. + +

+%(o)s + +%(manager:cn)s + +M: %(mobile)s +T: %(telephonenumber)s +W: https://it.services.inc +

diff --git a/conf/signature_IT.txt b/conf/signature_IT.txt new file mode 100644 index 0000000..53fd640 --- /dev/null +++ b/conf/signature_IT.txt @@ -0,0 +1,9 @@ +This is an example TEXT signature. + +%(o)s + +%(manager:cn)s + +M: %(mobile)s +T: %(telephonenumber)s +W: https://it.services.inc diff --git a/wallace/module_signature.py b/wallace/module_signature.py new file mode 100644 index 0000000..5098120 --- /dev/null +++ b/wallace/module_signature.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# Copyright 2010-2019 Kolab Systems AG (http://www.kolabsys.com) +# +# Jeroen van Meeuwen (Kolab Systems) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import json +import os +import re +import tempfile + +from email.encoders import encode_quopri +from email.parser import Parser +from email.utils import getaddresses + +import modules +import pykolab + +from pykolab.auth import Auth +from pykolab.translate import _ + +# pylint: disable=invalid-name +log = pykolab.getLogger('pykolab.wallace') +conf = pykolab.getConf() + +mybasepath = '/var/spool/pykolab/wallace/signature/' + + +def __init__(): + modules.register('signature', execute, description=description()) + + +def description(): + return """Append a signature to messages.""" + + +def set_part_content(part, content): + # Reset old encoding and use quoted-printable (#5414) + del part['Content-Transfer-Encoding'] + part.set_payload(content) + encode_quopri(part) + + return True + + +def attr_resolve(sender_info, attr): + try: + attr, attr_val = attr.split(':') + except ValueError: + return None + + auth = Auth() + auth.connect() + + values = [] + + if not isinstance(sender_info[attr], list): + sender_info[attr] = [sender_info[attr]] + + for sender_attr_val in sender_info[attr]: + values.append(auth.get_entry_attribute(None, sender_attr_val, attr_val)) + + return ", ".join(values) + + +# pylint: disable=too-many-branches,too-many-locals,too-many-statements +def execute(*args, **kw): # noqa: C901 + if not os.path.isdir(mybasepath): + os.makedirs(mybasepath) + + for stage in ['incoming', 'ACCEPT']: + if not os.path.isdir(os.path.join(mybasepath, stage)): + os.makedirs(os.path.join(mybasepath, stage)) + + # TODO: Test for correct call. + filepath = args[0] + + if 'stage' in kw: + log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) + log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) + if hasattr(modules, 'cb_action_%s' % (kw['stage'])): + log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) + exec('modules.cb_action_%s(%r, %r)' % (kw['stage'], 'signature', filepath)) + return + + log.debug(_("Executing module signature for %r, %r") % (args, kw), level=8) + + new_filepath = os.path.join( + '/var/spool/pykolab/wallace/signature/incoming', + os.path.basename(filepath) + ) + + os.rename(filepath, new_filepath) + filepath = new_filepath + + # parse message + message = Parser().parse(open(filepath, 'r')) + + sender_address = [ + address for displayname, address in getaddresses(message.get_all('X-Kolab-From')) + ][0] + + auth = Auth() + auth.connect() + + sender_dn = auth.find_recipient(sender_address) + if not sender_dn: + exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT', 'signature', filepath)) + return + + sender_info = auth.get_entry_attributes(None, sender_dn, ['*', 'entrydn', 'manager']) + + log.debug("Sender info: %r" % (sender_info), level=7) + + signature_rules = conf.get_raw('wallace', 'signature_rules') + + if signature_rules: + signature_rules = json.loads(signature_rules) + + log.debug("Signature rules: %r" % (signature_rules), level=7) + + signature_html = None + signature_text = None + + sig_html_conf = conf.get_raw('wallace', 'signature_file_html') + sig_text_conf = conf.get_raw('wallace', 'signature_file_text') + + if sig_html_conf and sig_text_conf: + _sig_html_conf = sig_html_conf % sender_info + _sig_text_conf = sig_text_conf % sender_info + + if not os.path.exists(_sig_html_conf): + _sig_html_conf = '/etc/kolab/signature.d/default.html' + + if not os.path.exists(_sig_text_conf): + _sig_text_conf = '/etc/kolab/signature.d/default.txt' + + if os.path.exists(_sig_html_conf): + signature_html = open(_sig_html_conf, 'r').read() + + if os.path.exists(_sig_text_conf): + signature_text = open(_sig_text_conf, 'r').read() + + if not signature_html and not signature_text: + for signature_rule in signature_rules: + try: + for attr, regex in signature_rule.iteritems(): + if attr == "html": + if not os.path.exists(signature_rule['html']): + raise ValueError + continue + + if attr == "text": + if not os.path.exists(signature_rule['text']): + raise ValueError + continue + + if attr in sender_info and re.match(regex, sender_info[attr], flags=re.IGNORECASE): + success = False + + while not success: + try: + signature_html = open(signature_rule['html'], 'r').read() % sender_info + signature_text = open(signature_rule['text'], 'r').read() % sender_info + + success = True + + except KeyError as errmsg: + sender_info[errmsg] = attr_resolve(sender_info, errmsg) + except ValueError: + continue + + if signature_html is None and signature_text is None: + exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT', 'signature', filepath)) + return + + signature_added = False + + try: + _signature_added = message.get("X-Wallace-Signature") + + # pylint: disable=broad-except + except Exception: + pass + + if _signature_added == "YES": + exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','signature', filepath)) + return + + for part in message.walk(): + disposition = None + + try: + content_type = part.get_content_type() + + # pylint: disable=broad-except + except Exception: + continue + + try: + disposition = part.get("Content-Disposition") + + # pylint: disable=broad-except + except Exception: + pass + + log.debug("Walking message part: %s; disposition = %r" % (content_type, disposition), level=8) + + if disposition is not None: + continue + + if content_type == "text/plain": + content = part.get_payload(decode=True) + content += "\n\n-- \n%s" % (signature_text) + signature_added = set_part_content(part, content) + + elif content_type == "text/html": + content = part.get_payload(decode=True) + append = "\n\n" + signature_html + if "" in content: + content = content.replace("", append + "") + else: + content = "" + content + append + "" + signature_added = set_part_content(part, content) + + if signature_added: + log.debug("Signature attached.", level=8) + message.add_header("X-Wallace-Signature", "YES") + + (fp, new_filepath) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/signature/ACCEPT") + os.write(fp, message.as_string()) + os.close(fp) + os.unlink(filepath) + + exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','signature', new_filepath))