diff --git a/conf/kolab.conf b/conf/kolab.conf
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -428,9 +428,51 @@
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
diff --git a/conf/signature_IT.html b/conf/signature_IT.html
new file mode 100644
--- /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
--- /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
--- /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 "" + content + append + "