diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 17dc71c..602326d 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -1,1451 +1,1449 @@
 # -*- coding: utf-8 -*-
 # Copyright 2014 Kolab Systems AG (http://www.kolabsys.com)
 #
 # Thomas Bruederli (Kolab Systems) <bruederli@kolabsys.com>
 #
 # 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 <http://www.gnu.org/licenses/>.
 #
 
 import datetime
 import os
 import tempfile
 import time
 from urlparse import urlparse
 import urllib
 import hashlib
 import traceback
 import re
 
 from email import message_from_string
 from email.parser import Parser
 from email.utils import formataddr
 from email.utils import getaddresses
 
 import modules
 
 import pykolab
 import kolabformat
 
 from pykolab import utils
 from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
 from pykolab.xml import to_dt
 from pykolab.xml import utils as xmlutils
 from pykolab.xml import todo_from_message
 from pykolab.xml import event_from_message
 from pykolab.xml import participant_status_label
 from pykolab.itip import objects_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.itip import send_reply
 from pykolab.translate import _
 
 # define some contstants used in the code below
 ACT_MANUAL         = 1
 ACT_ACCEPT         = 2
 ACT_DELEGATE       = 4
 ACT_REJECT         = 8
 ACT_UPDATE         = 16
 ACT_CANCEL_DELETE  = 32
 ACT_SAVE_TO_FOLDER = 64
 
 COND_IF_AVAILABLE  = 128
 COND_IF_CONFLICT   = 256
 COND_TENTATIVE     = 512
 COND_NOTIFY        = 1024
 COND_FORWARD       = 2048
 COND_TYPE_EVENT    = 4096
 COND_TYPE_TASK     = 8192
 COND_TYPE_ALL      = COND_TYPE_EVENT + COND_TYPE_TASK
 
 ACT_TENTATIVE         = ACT_ACCEPT + COND_TENTATIVE
 ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY
 ACT_SAVE_AND_FORWARD  = ACT_SAVE_TO_FOLDER + COND_FORWARD
 ACT_CANCEL_DELETE_AND_NOTIFY = ACT_CANCEL_DELETE + COND_NOTIFY
 
 FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
 
 MESSAGE_PROCESSED = 1
 MESSAGE_FORWARD   = 2
 
 policy_name_map = {
     # policy values applying to all object types
     'ALL_MANUAL':                     ACT_MANUAL + COND_TYPE_ALL,
     'ALL_ACCEPT':                     ACT_ACCEPT + COND_TYPE_ALL,
     'ALL_REJECT':                     ACT_REJECT + COND_TYPE_ALL,
     'ALL_DELEGATE':                   ACT_DELEGATE + COND_TYPE_ALL,  # not implemented
     'ALL_UPDATE':                     ACT_UPDATE + COND_TYPE_ALL,
     'ALL_UPDATE_AND_NOTIFY':          ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
     'ALL_SAVE_TO_FOLDER':             ACT_SAVE_TO_FOLDER + COND_TYPE_ALL,
     'ALL_SAVE_AND_FORWARD':           ACT_SAVE_AND_FORWARD + COND_TYPE_ALL,
     'ALL_CANCEL_DELETE':              ACT_CANCEL_DELETE + COND_TYPE_ALL,
     'ALL_CANCEL_DELETE_AND_NOTIFY':   ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL,
     # event related policy values
     'EVENT_MANUAL':                   ACT_MANUAL + COND_TYPE_EVENT,
     'EVENT_ACCEPT':                   ACT_ACCEPT + COND_TYPE_EVENT,
     'EVENT_TENTATIVE':                ACT_TENTATIVE + COND_TYPE_EVENT,
     'EVENT_REJECT':                   ACT_REJECT + COND_TYPE_EVENT,
     'EVENT_DELEGATE':                 ACT_DELEGATE + COND_TYPE_EVENT,  # not implemented
     'EVENT_UPDATE':                   ACT_UPDATE + COND_TYPE_EVENT,
     'EVENT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY + COND_TYPE_EVENT,
     'EVENT_ACCEPT_IF_NO_CONFLICT':    ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
     'EVENT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
     'EVENT_DELEGATE_IF_CONFLICT':     ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
     'EVENT_REJECT_IF_CONFLICT':       ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
     'EVENT_SAVE_TO_FOLDER':           ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
     'EVENT_SAVE_AND_FORWARD':         ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT,
     'EVENT_CANCEL_DELETE':            ACT_CANCEL_DELETE + COND_TYPE_EVENT,
     'EVENT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_EVENT,
     # task related policy values
     'TASK_MANUAL':                    ACT_MANUAL + COND_TYPE_TASK,
     'TASK_ACCEPT':                    ACT_ACCEPT + COND_TYPE_TASK,
     'TASK_REJECT':                    ACT_REJECT + COND_TYPE_TASK,
     'TASK_DELEGATE':                  ACT_DELEGATE + COND_TYPE_TASK,  # not implemented
     'TASK_UPDATE':                    ACT_UPDATE + COND_TYPE_TASK,
     'TASK_UPDATE_AND_NOTIFY':         ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK,
     'TASK_SAVE_TO_FOLDER':            ACT_SAVE_TO_FOLDER + COND_TYPE_TASK,
     'TASK_SAVE_AND_FORWARD':          ACT_SAVE_AND_FORWARD + COND_TYPE_TASK,
     'TASK_CANCEL_DELETE':             ACT_CANCEL_DELETE + COND_TYPE_TASK,
     'TASK_CANCEL_DELETE_AND_NOTIFY':  ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_TASK,
     # legacy values
     'ACT_MANUAL':                     ACT_MANUAL + COND_TYPE_ALL,
     'ACT_ACCEPT':                     ACT_ACCEPT + COND_TYPE_ALL,
     'ACT_ACCEPT_IF_NO_CONFLICT':      ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
     'ACT_TENTATIVE':                  ACT_TENTATIVE + COND_TYPE_EVENT,
     'ACT_TENTATIVE_IF_NO_CONFLICT':   ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
     'ACT_DELEGATE':                   ACT_DELEGATE + COND_TYPE_ALL,
     'ACT_DELEGATE_IF_CONFLICT':       ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
     'ACT_REJECT':                     ACT_REJECT + COND_TYPE_ALL,
     'ACT_REJECT_IF_CONFLICT':         ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
     'ACT_UPDATE':                     ACT_UPDATE + COND_TYPE_ALL,
     'ACT_UPDATE_AND_NOTIFY':          ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
     'ACT_CANCEL_DELETE':              ACT_CANCEL_DELETE + COND_TYPE_ALL,
     'ACT_CANCEL_DELETE_AND_NOTIFY':   ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL,
     'ACT_SAVE_TO_CALENDAR':           ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
     'ACT_SAVE_AND_FORWARD':           ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT,
 }
 
 policy_value_map = dict([(v &~ COND_TYPE_ALL, k) for (k, v) in policy_name_map.iteritems()])
 
 object_type_conditons = {
     'event': COND_TYPE_EVENT,
     'task':  COND_TYPE_TASK
 }
 
 log = pykolab.getLogger('pykolab.wallace')
 conf = pykolab.getConf()
 
 mybasepath = '/var/spool/pykolab/wallace/invitationpolicy/'
 
 auth = None
 imap = None
 write_locks = []
 
 def __init__():
     modules.register('invitationpolicy', execute, description=description())
 
 def accept(filepath):
     new_filepath = os.path.join(
         mybasepath,
         'ACCEPT',
         os.path.basename(filepath)
     )
 
     cleanup()
     os.rename(filepath, new_filepath)
     filepath = new_filepath
     exec('modules.cb_action_ACCEPT(%r, %r)' % ('invitationpolicy',filepath))
 
 def reject(filepath):
     new_filepath = os.path.join(
         mybasepath,
         'REJECT',
         os.path.basename(filepath)
     )
 
     os.rename(filepath, new_filepath)
     filepath = new_filepath
     exec('modules.cb_action_REJECT(%r, %r)' % ('invitationpolicy',filepath))
 
 def description():
     return """Invitation policy execution module."""
 
 def cleanup():
     global auth, imap, write_locks
 
     log.debug("cleanup(): %r, %r" % (auth, imap), level=8)
 
     auth.disconnect()
     del auth
 
     # Disconnect IMAP or we lock the mailbox almost constantly
     imap.disconnect()
     del imap
 
     # remove remaining write locks
     for key in write_locks:
         remove_write_lock(key, False)
 
 def execute(*args, **kw):
     global auth, imap
 
     # (re)set language to default
     pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
 
     if not os.path.isdir(mybasepath):
         os.makedirs(mybasepath)
 
     for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER', 'locks']:
         if not os.path.isdir(os.path.join(mybasepath, stage)):
             os.makedirs(os.path.join(mybasepath, stage))
 
     log.debug(_("Invitation policy called for %r, %r") % (args, kw), level=8)
 
     auth = Auth()
     imap = IMAP()
 
     filepath = args[0]
 
     # ignore calls on lock files
     if '/locks/' in filepath or kw.has_key('stage') and kw['stage'] == 'locks':
         return False
 
     log.debug("Invitation policy executing for %r, %r" % (filepath, '/locks/' in filepath), level=8)
 
     if kw.has_key('stage'):
         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'],
                     'invitationpolicy',
                     filepath
                 )
             )
 
             return filepath
     else:
         # Move to incoming
         new_filepath = os.path.join(
             mybasepath,
             'incoming',
             os.path.basename(filepath)
         )
 
         if not filepath == new_filepath:
             log.debug("Renaming %r to %r" % (filepath, new_filepath))
             os.rename(filepath, new_filepath)
             filepath = new_filepath
 
     # parse full message
     message = Parser().parse(open(filepath, 'r'))
 
     # invalid message, skip
     if not message.get('X-Kolab-To'):
         return filepath
 
     recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))]
     sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0]
 
     any_itips = False
     recipient_email = None
     recipient_emails = []
     recipient_user_dn = None
 
     # An iTip message may contain multiple events. Later on, test if the message
     # is an iTip message by checking the length of this list.
     try:
         itip_events = objects_from_message(message, ['VEVENT','VTODO'], ['REQUEST', 'REPLY', 'CANCEL'])
     except Exception, errmsg:
         log.error(_("Failed to parse iTip objects from message: %r" % (errmsg)))
         itip_events = []
 
     if not len(itip_events) > 0:
         log.info(_("Message is not an iTip message or does not contain any (valid) iTip objects."))
 
     else:
         any_itips = True
         log.debug(_("iTip objects attached to this message contain the following information: %r") % (itip_events), level=8)
 
     # See if any iTip actually allocates a user.
     if any_itips and len([x['uid'] for x in itip_events if x.has_key('attendees') or x.has_key('organizer')]) > 0:
         auth.connect()
 
         # we're looking at the first itip object
         itip_event = itip_events[0]
 
         for recipient in recipients:
             recipient_user_dn = user_dn_from_email_address(recipient)
             if recipient_user_dn:
                 receiving_user = auth.get_entry_attributes(None, recipient_user_dn, ['*'])
                 recipient_emails = auth.extract_recipient_addresses(receiving_user)
                 recipient_email = recipient
 
                 # extend with addresses from delegators
                 # (only do this lookup for REPLY messages)
                 receiving_user['_delegated_mailboxes'] = []
 
                 if itip_event['method'] == 'REPLY':
                     for _delegator in auth.list_delegators(recipient_user_dn):
                         if not _delegator['_mailbox_basename'] == None:
                             receiving_user['_delegated_mailboxes'].append(
                                     _delegator['_mailbox_basename'].split('@')[0]
                                 )
 
                 log.debug(_("Recipient emails for %s: %r") % (recipient_user_dn, recipient_emails), level=8)
                 break
 
     if not any_itips:
         log.debug(_("No itips, no users, pass along %r") % (filepath), level=5)
         return filepath
     elif recipient_email is None:
         log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5)
         return filepath
 
     # for replies, the organizer is the recipient
     if itip_event['method'] == 'REPLY':
         organizer_mailto = str(itip_event['organizer']).split(':')[-1]
         user_attendees = [organizer_mailto] if organizer_mailto in recipient_emails else []
 
     else:
         # Limit the attendees to the one that is actually invited with the current message.
         attendees = [str(a).split(':')[-1] for a in (itip_event['attendees'] if itip_event.has_key('attendees') else [])]
         user_attendees = [a for a in attendees if a in recipient_emails]
 
         if itip_event.has_key('organizer'):
             sender_email = itip_event['xml'].get_organizer().email()
 
     # abort if no attendee matches the envelope recipient
     if len(user_attendees) == 0:
         log.info(_("No user attendee matching envelope recipient %s, skip message") % (recipient_email))
         return filepath
 
     log.debug(_("Receiving user: %r") % (receiving_user), level=8)
 
     # set recipient_email to the matching attendee mailto: address
     recipient_email = user_attendees[0]
 
     # change gettext language to the preferredlanguage setting of the receiving user
     if receiving_user.has_key('preferredlanguage'):
         pykolab.translate.setUserLanguage(receiving_user['preferredlanguage'])
 
     # find user's kolabInvitationPolicy settings and the matching policy values
     type_condition = object_type_conditons.get(itip_event['type'], COND_TYPE_ALL)
     policies = get_matching_invitation_policies(receiving_user, sender_email, type_condition)
 
     # select a processing function according to the iTip request method
     method_processing_map = {
         'REQUEST': process_itip_request,
         'REPLY':   process_itip_reply,
         'CANCEL':  process_itip_cancel
     }
 
     done = None
     if method_processing_map.has_key(itip_event['method']):
         processor_func = method_processing_map[itip_event['method']]
 
         # connect as cyrus-admin
         imap.connect()
 
         for policy in policies:
             log.debug(_("Apply invitation policy %r for sender %r") % (policy_value_map[policy], sender_email), level=8)
             done = processor_func(itip_event, policy, recipient_email, sender_email, receiving_user)
 
             # matching policy found
             if done is not None:
                 break
 
             # remove possible write lock from this iteration
             remove_write_lock(get_lock_key(receiving_user, itip_event['uid']))
 
     else:
         log.debug(_("Ignoring '%s' iTip method") % (itip_event['method']), level=8)
 
     # message has been processed by the module, remove it
     if done == MESSAGE_PROCESSED:
         log.debug(_("iTip message %r consumed by the invitationpolicy module") % (message.get('Message-ID')), level=5)
         os.unlink(filepath)
         cleanup()
         return None
 
     # accept message into the destination inbox
     accept(filepath)
 
 
 def process_itip_request(itip_event, policy, recipient_email, sender_email, receiving_user):
     """
         Process an iTip REQUEST message according to the given policy
     """
 
     # if invitation policy is set to MANUAL, pass message along
     if policy & ACT_MANUAL:
         log.info(_("Pass invitation for manual processing"))
         return MESSAGE_FORWARD
 
     try:
         receiving_attendee = itip_event['xml'].get_attendee_by_email(recipient_email)
         log.debug(_("Receiving Attendee: %r") % (receiving_attendee), level=8)
     except Exception, errmsg:
         log.error("Could not find envelope attendee: %r" % (errmsg))
         return MESSAGE_FORWARD
 
     # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
     is_task = itip_event['type'] == 'task'
     nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant
     partstat = receiving_attendee.get_participant_status()
     save_object = not nonpart or not partstat == kolabformat.PartNeedsAction
     rsvp = receiving_attendee.get_rsvp()
     scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction
     respond_with = receiving_attendee.get_participant_status(True)
     condition_fulfilled = True
 
     # find existing event in user's calendar
     (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True)
 
     # compare sequence number to determine a (re-)scheduling request
     if existing is not None:
         scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence()
         log.debug(_("Scheduling required: %r, for existing %s: %s") % (scheduling_required, existing.type, existing.get_uid()), level=8)
         save_object = True
 
     # if scheduling: check availability (skip that for tasks)
     if scheduling_required:
         if not is_task and policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
             condition_fulfilled = check_availability(itip_event, receiving_user)
         if not is_task and policy & COND_IF_CONFLICT:
             condition_fulfilled = not condition_fulfilled
 
         log.debug(_("Precondition for object %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
 
         if existing:
             respond_with = None
 
         if policy & ACT_ACCEPT and condition_fulfilled:
             respond_with = 'TENTATIVE' if policy & COND_TENTATIVE else 'ACCEPTED'
 
         elif policy & ACT_REJECT and condition_fulfilled:
             respond_with = 'DECLINED'
             # TODO: only save declined invitation when a certain config option is set?
 
         elif policy & ACT_DELEGATE and condition_fulfilled:
             # TODO: delegate (but to whom?)
             return None
 
     # auto-update changes if enabled for this user
     elif policy & ACT_UPDATE and existing:
         # compare sequence number to avoid outdated updates
         if not itip_event['sequence'] == existing.get_sequence():
             log.info(_("The iTip request sequence (%r) doesn't match the referred object version (%r). Ignoring.") % (
                 itip_event['sequence'], existing.get_sequence()
             ))
             return None
 
         log.debug(_("Auto-updating %s %r on iTip REQUEST (no re-scheduling)") % (existing.type, existing.uid), level=8)
         save_object = True
         rsvp = False
 
         # retain task status and percent-complete properties from my old copy
         if is_task:
             itip_event['xml'].set_status(existing.get_status())
             itip_event['xml'].set_percentcomplete(existing.get_percentcomplete())
 
         if policy & COND_NOTIFY:
             sender = itip_event['xml'].get_organizer()
             comment = itip_event['xml'].get_comment()
             send_update_notification(itip_event['xml'], receiving_user, existing, False,
                                      sender, comment)
 
     # if RSVP, send an iTip REPLY
     if rsvp or scheduling_required:
         # set attendee's CN from LDAP record if yet missing
         if not receiving_attendee.get_name() and receiving_user.has_key('cn'):
             receiving_attendee.set_name(receiving_user['cn'])
 
         # send iTip reply
         if respond_with is not None and not respond_with == 'NEEDS-ACTION':
             receiving_attendee.set_participant_status(respond_with)
             send_reply(recipient_email, itip_event, invitation_response_text(itip_event['type']),
                 subject=_('"%(summary)s" has been %(status)s'))
 
         elif policy & ACT_SAVE_TO_FOLDER:
             # copy the invitation into the user's default folder with PARTSTAT=NEEDS-ACTION
             itip_event['xml'].set_attendee_participant_status(receiving_attendee, respond_with or 'NEEDS-ACTION')
             save_object = True
 
         else:
             # policy doesn't match, pass on to next one
             return None
 
     if save_object:
         targetfolder = None
 
         # delete old version from IMAP
         if existing:
             targetfolder = existing._imap_folder
             delete_object(existing)
         elif master and hasattr(master, '_imap_folder'):
             targetfolder = master._imap_folder
             delete_object(master)
 
         if not nonpart or existing:
             # save new copy from iTip
             if store_object(itip_event['xml'], receiving_user, targetfolder, master):
                 if policy & COND_FORWARD:
                     log.debug(_("Forward invitation for notification"), level=5)
                     return MESSAGE_FORWARD
                 else:
                     return MESSAGE_PROCESSED
 
     return None
 
 
 def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiving_user):
     """
         Process an iTip REPLY message according to the given policy
     """
 
     # if invitation policy is set to MANUAL, pass message along
     if policy & ACT_MANUAL:
         log.info(_("Pass reply for manual processing"))
         return MESSAGE_FORWARD
 
     # auto-update is enabled for this user
     if policy & ACT_UPDATE:
         try:
             sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email)
             log.debug(_("Sender Attendee: %r") % (sender_attendee), level=8)
         except Exception, errmsg:
             log.error("Could not find envelope sender attendee: %r" % (errmsg))
             return MESSAGE_FORWARD
 
         # find existing event in user's calendar
         # sets/checks lock to avoid concurrent wallace processes trying to update the same event simultaneously
         (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True)
 
         if existing:
             # compare sequence number to avoid outdated replies?
             if not itip_event['sequence'] == existing.get_sequence():
                 log.info(_("The iTip reply sequence (%r) doesn't match the referred object version (%r). Forwarding to Inbox.") % (
                     itip_event['sequence'], existing.get_sequence()
                 ))
                 remove_write_lock(existing._lock_key)
                 return MESSAGE_FORWARD
 
             log.debug(_("Auto-updating %s %r on iTip REPLY") % (existing.type, existing.uid), level=8)
             updated_attendees = []
             try:
                 existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), rsvp=False)
                 existing_attendee = existing.get_attendee(sender_email)
                 updated_attendees.append(existing_attendee)
             except Exception, errmsg:
                 log.error("Could not find corresponding attende in organizer's copy: %r" % (errmsg))
 
                 # append delegated-from attendee ?
                 if len(sender_attendee.get_delegated_from()) > 0:
                     existing.add_attendee(sender_attendee)
                     updated_attendees.append(sender_attendee)
                 else:
                     # TODO: accept new participant if ACT_ACCEPT ?
                     remove_write_lock(existing._lock_key)
                     return MESSAGE_FORWARD
 
             # append delegated-to attendee
             if len(sender_attendee.get_delegated_to()) > 0:
                 try:
                     delegatee_email = sender_attendee.get_delegated_to(True)[0]
                     sender_delegatee = itip_event['xml'].get_attendee_by_email(delegatee_email)
                     existing_delegatee = existing.find_attendee(delegatee_email)
 
                     if not existing_delegatee:
                         existing.add_attendee(sender_delegatee)
                         log.debug(_("Add delegatee: %r") % (sender_delegatee.to_dict()), level=8)
                     else:
                         existing_delegatee.copy_from(sender_delegatee)
                         log.debug(_("Update existing delegatee: %r") % (existing_delegatee.to_dict()), level=8)
 
                     updated_attendees.append(sender_delegatee)
 
                     # copy all parameters from replying attendee (e.g. delegated-to, role, etc.)
                     existing_attendee.copy_from(sender_attendee)
                     existing.update_attendees([existing_attendee])
                     log.debug(_("Update delegator: %r") % (existing_attendee.to_dict()), level=8)
 
                 except Exception, errmsg:
                     log.error("Could not find delegated-to attendee: %r" % (errmsg))
 
             # update the organizer's copy of the object
             if update_object(existing, receiving_user, master):
                 if policy & COND_NOTIFY:
                     send_update_notification(existing, receiving_user, existing, True,
                                              sender_attendee, itip_event['xml'].get_comment())
 
                 # update all other attendee's copies
                 if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
                     propagate_changes_to_attendees_accounts(existing, updated_attendees)
 
                 return MESSAGE_PROCESSED
 
         else:
             log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
             return MESSAGE_FORWARD
 
     return None
 
 
 def process_itip_cancel(itip_event, policy, recipient_email, sender_email, receiving_user):
     """
         Process an iTip CANCEL message according to the given policy
     """
 
     # if invitation policy is set to MANUAL, pass message along
     if policy & ACT_MANUAL:
         log.info(_("Pass cancellation for manual processing"))
         return MESSAGE_FORWARD
 
     # auto-update the local copy
     if policy & ACT_UPDATE or policy & ACT_CANCEL_DELETE:
         # find existing object in user's folders
         (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True)
         remove_object = policy & ACT_CANCEL_DELETE
 
         if existing:
             # on this-and-future cancel requests, set the recurrence until date on the master event
             if itip_event['recurrence-id'] and master and itip_event['xml'].get_thisandfuture():
                 rrule = master.get_recurrence()
                 rrule.set_count(0)
                 rrule.set_until(existing.get_start() + datetime.timedelta(days=-1))
                 master.set_recurrence(rrule)
                 existing.set_recurrence_id(existing.get_recurrence_id(), True)
                 remove_object = False
 
             # delete the local copy
             if remove_object:
                 # remove exception and register an exdate to the main event
                 if master:
                     log.debug(_("Remove cancelled %s instance %s from %r") % (existing.type, itip_event['recurrence-id'], existing.uid), level=8)
                     master.add_exception_date(existing.get_start())
                     master.del_exception(existing)
                     success = update_object(master, receiving_user)
 
                 # delete main event
                 else:
                     success = delete_object(existing)
 
             # update the local copy with STATUS=CANCELLED
             else:
                 log.debug(_("Update cancelled %s %r with STATUS=CANCELLED") % (existing.type, existing.uid), level=8)
                 existing.set_status('CANCELLED')
                 existing.set_transparency(True)
                 success = update_object(existing, receiving_user, master)
 
             if success:
                 # send cancellation notification
                 if policy & COND_NOTIFY:
                     sender = itip_event['xml'].get_organizer()
                     comment = itip_event['xml'].get_comment()
                     send_cancel_notification(existing, receiving_user, remove_object, sender, comment)
 
                 return MESSAGE_PROCESSED
 
         else:
             log.error(_("The object referred by this cancel request was not found in the user's folders. Forwarding to Inbox."))
             return MESSAGE_FORWARD
 
     return None
 
 
 def user_dn_from_email_address(email_address):
     """
         Resolves the given email address to a Kolab user entity
     """
     global auth
 
     if not auth:
         auth = Auth()
         auth.connect()
 
     # return cached value
     if user_dn_from_email_address.cache.has_key(email_address):
         return user_dn_from_email_address.cache[email_address]
 
     local_domains = auth.list_domains()
 
     if local_domains is not None:
         local_domains = list(set(local_domains.keys()))
 
     if not email_address.split('@')[1] in local_domains:
         user_dn_from_email_address.cache[email_address] = None
         return None
 
     log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8)
 
     user_dn = auth.find_user_dn(email_address, True)
 
     if isinstance(user_dn, basestring):
         log.debug(_("User DN: %r") % (user_dn), level=8)
     else:
         log.debug(_("No user record(s) found for %r") % (email_address), level=8)
 
     # remember this lookup
     user_dn_from_email_address.cache[email_address] = user_dn
 
     return user_dn
 
 user_dn_from_email_address.cache = {}
 
 
 def get_matching_invitation_policies(receiving_user, sender_email, type_condition=COND_TYPE_ALL):
     # get user's kolabInvitationPolicy settings
     policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else []
     if policies and not isinstance(policies, list):
         policies = [policies]
 
     if len(policies) == 0:
         policies = conf.get_list('wallace', 'kolab_invitation_policy')
 
     # match policies agains the given sender_email
     matches = []
     for p in policies:
         if ':' in p:
             (value, domain) = p.split(':', 1)
         else:
             value = p
             domain = ''
 
         if domain == '' or domain == '*' or str(sender_email).endswith(domain):
             value = value.upper()
             if policy_name_map.has_key(value):
                 val = policy_name_map[value]
                 # append if type condition matches
                 if val & type_condition:
                     matches.append(val &~ COND_TYPE_ALL)
 
     # add manual as default action
     if len(matches) == 0:
         matches.append(ACT_MANUAL)
 
     return matches
 
 
 def imap_proxy_auth(user_rec):
     """
         Perform IMAP login using proxy authentication with admin credentials
     """
     global imap
 
     mail_attribute = conf.get('cyrus-sasl', 'result_attribute')
     if mail_attribute is None:
         mail_attribute = 'mail'
 
     mail_attribute = mail_attribute.lower()
 
     if not user_rec.has_key(mail_attribute):
         log.error(_("User record doesn't have the mailbox attribute %r set" % (mail_attribute)))
         return False
 
     # do IMAP prox auth with the given user
     backend = conf.get('kolab', 'imap_backend')
     admin_login = conf.get(backend, 'admin_login')
     admin_password = conf.get(backend, 'admin_password')
 
     try:
         imap.disconnect()
         imap.connect(login=False)
         imap.login_plain(admin_login, admin_password, user_rec[mail_attribute])
     except Exception, errmsg:
         log.error(_("IMAP proxy authentication failed: %r") % (errmsg))
         return False
 
     return True
 
 
 def list_user_folders(user_rec, _type):
     """
         Get a list of the given user's private calendar/tasks folders
     """
     global imap
 
     # return cached list
     if '_imap_folders' in user_rec:
         return user_rec['_imap_folders']
 
     result = []
 
     if not imap_proxy_auth(user_rec):
         return result
 
     folders = imap.get_metadata('*')
 
     log.debug(
         _("List %r folders for user %r: %r") % (
             _type,
             user_rec['mail'],
             folders
         ),
         level=8
     )
 
     (ns_personal, ns_other, ns_shared) = imap.namespaces()
 
     _folders = {}
 
     # Filter the folders by type relevance
     for folder, metadata in folders.items():
         key = '/shared' + FOLDER_TYPE_ANNOTATION
         if key in metadata:
             if metadata[key].startswith(_type):
                 _folders[folder] = metadata
 
         key = '/private' + FOLDER_TYPE_ANNOTATION
         if key in metadata:
             if metadata[key].startswith(_type):
                 _folders[folder] = metadata
 
     for folder, metadata in _folders.items():
         folder_delegated = False
 
         # Exclude shared and other user's namespace
         #
         # First, test if this is another users folder
         if ns_other is not None and folder.startswith(ns_other):
             # If we have no delegated mailboxes, we can skip this entirely
             if '_delegated_mailboxes' not in user_rec:
                 continue
 
             for _m in user_rec['_delegated_mailboxes']:
                 if folder.startswith(ns_other + _m + '/'):
                     folder_delegated = True
 
             if not folder_delegated:
                 continue
 
         # TODO: list shared folders the user has write privileges ?
         if ns_shared is not None:
             if len([_ns for _ns in ns_shared if folder.startswith(_ns)]) > 0:
                 continue
 
         key = '/shared' + FOLDER_TYPE_ANNOTATION
         if key in metadata:
             if metadata[key].startswith(_type):
                 result.append(folder)
 
         key = '/private' + FOLDER_TYPE_ANNOTATION
         if key in metadata:
             if metadata[key].startswith(_type):
                 result.append(folder)
 
             # store default folder in user record
             if metadata[key].endswith('.default'):
                 user_rec['_default_folder'] = folder
                 continue
 
             # store private and confidential folders in user record
             if metadata[key].endswith('.confidential'):
                 if '_confidential_folder' not in user_rec:
                     user_rec['_confidential_folder'] = folder
 
                 continue
 
             if metadata[key].endswith('.private'):
                 if '_private_folder' not in user_rec:
                     user_rec['_private_folder'] = folder
 
                 continue
 
     # cache with user record
     user_rec['_imap_folders'] = result
 
     return result
 
 
 def find_existing_object(uid, type, recurrence_id, user_rec, lock=False):
     """
         Search user's private folders for the given object (by UID+type)
     """
     global imap
 
     lock_key = None
 
     if lock:
         lock_key = get_lock_key(user_rec, uid)
         set_write_lock(lock_key)
 
     event = None
     master = None
     for folder in list_user_folders(user_rec, type):
         log.debug(_("Searching folder %r for %s %r") % (folder, type, uid), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
         res, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
         for num in reversed(data[0].split()):
             res, data = imap.imap.m.fetch(num, '(UID RFC822)')
 
             try:
                 msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1)
             except Exception, errmsg:
                 log.error(_("No UID found in IMAP response: %r") % (data[0][0]))
                 continue
 
             try:
                 if type == 'task':
                     event = todo_from_message(message_from_string(data[0][1]))
                 else:
                     event = event_from_message(message_from_string(data[0][1]))
 
                 # find instance in a recurring series
                 if recurrence_id and (event.is_recurring() or event.has_exceptions() or event.get_recurrence_id()):
                     master = event
                     event = master.get_instance(recurrence_id)
                     setattr(master, '_imap_folder', folder)
                     setattr(master, '_msguid', msguid)
 
                     # return master, even if instance is not found
                     if not event and master.uid == uid:
                         return (event, master)
 
                 if event is not None:
                     setattr(event, '_imap_folder', folder)
                     setattr(event, '_lock_key', lock_key)
                     setattr(event, '_msguid', msguid)
 
             except Exception, errmsg:
                 log.error(_("Failed to parse %s from message %s/%s: %s") % (type, folder, num, traceback.format_exc()))
                 event = None
                 master = None
                 continue
 
             if event and event.uid == uid:
                 return (event, master)
 
     if lock_key is not None:
         remove_write_lock(lock_key)
 
     return (event, master)
 
 
 def check_availability(itip_event, receiving_user):
     """
         For the receiving user, determine if the event in question is in conflict.
     """
 
     start = time.time()
     num_messages = 0
     conflict = False
 
     # return previously detected conflict
     if itip_event.has_key('_conflicts'):
         return not itip_event['_conflicts']
 
     for folder in list_user_folders(receiving_user, 'event'):
         log.debug(_("Listing events from folder %r") % (folder), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
         res, data = imap.imap.m.search(None, '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")')
         num_messages += len(data[0].split())
 
         for num in reversed(data[0].split()):
             event = None
             res, data = imap.imap.m.fetch(num, '(RFC822)')
 
             try:
                 event = event_from_message(message_from_string(data[0][1]))
             except Exception, errmsg:
                 log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, errmsg))
                 continue
 
             if event and event.uid:
                 conflict = check_event_conflict(event, itip_event)
                 if conflict:
                     log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid']))
                     break
 
         if conflict:
             break
 
     end = time.time()
     log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=8)
 
     # remember the result of this check for further iterations
     itip_event['_conflicts'] = conflict
 
     return not conflict
 
 
 def set_write_lock(key, wait=True):
     """
         Set a write-lock for the given key and wait if such a lock already exists
     """
     if not os.path.isdir(mybasepath):
         os.makedirs(mybasepath)
     if not os.path.isdir(os.path.join(mybasepath, 'locks')):
         os.makedirs(os.path.join(mybasepath, 'locks'))
 
     filename = os.path.join(mybasepath, 'locks', key + '.lock')
     locktime = 0
 
     if os.path.isfile(filename):
         locktime = os.path.getmtime(filename)
 
     # wait if file lock is in place
     while time.time() < locktime + 300:
         if not wait:
             return False
 
         log.debug(_("%r is locked, waiting...") % (key), level=8)
         time.sleep(0.5)
         locktime = os.path.getmtime(filename) if os.path.isfile(filename) else 0
 
     # touch the file
     if os.path.isfile(filename):
         os.utime(filename, None)
     else:
         open(filename, 'w').close()
 
     # register active lock
     write_locks.append(key)
 
     return True
 
 
 def remove_write_lock(key, update=True):
     """
         Remove the lock file for the given key
     """
     global write_locks
 
     if key is not None:
         file = os.path.join(mybasepath, 'locks', key + '.lock')
         if os.path.isfile(file):
             os.remove(file)
             if update:
                 write_locks = [k for k in write_locks if not k == key]
 
 
 def get_lock_key(user, uid):
     return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest()
 
 
 def update_object(object, user_rec, master=None):
     """
         Update the given object in IMAP (i.e. delete + append)
     """
     success = False
     saveobj = object
 
     # updating a single instance only: use master event
     if object.get_recurrence_id() and master:
         saveobj = master
 
     if hasattr(saveobj, '_imap_folder'):
         if delete_object(saveobj):
             saveobj.set_lastmodified()  # update last-modified timestamp
             success = store_object(object, user_rec, saveobj._imap_folder, master)
 
         # remove write lock for this event
         if hasattr(saveobj, '_lock_key') and saveobj._lock_key is not None:
             remove_write_lock(saveobj._lock_key)
 
     return success
 
 
 def store_object(object, user_rec, targetfolder=None, master=None):
     """
         Append the given object to the user's default calendar/tasklist
     """
 
     # find calendar folder to save object to if not specified
     if targetfolder is None:
         targetfolders = list_user_folders(user_rec, object.type)
         oc = object.get_classification()
 
         # use *.confidential/private folder for confidential/private invitations
         if oc == kolabformat.ClassConfidential and user_rec.has_key('_confidential_folder'):
             targetfolder = user_rec['_confidential_folder']
         elif oc == kolabformat.ClassPrivate and user_rec.has_key('_private_folder'):
             targetfolder = user_rec['_private_folder']
         # use *.default folder if exists
         elif user_rec.has_key('_default_folder'):
             targetfolder = user_rec['_default_folder']
         # fallback to any existing folder of specified type
         elif targetfolders is not None and len(targetfolders) > 0:
             targetfolder = targetfolders[0]
 
     if targetfolder is None:
         log.error(_("Failed to save %s: no target folder found for user %r") % (object.type, user_rec['mail']))
         return False
 
     saveobj = object
 
     # updating a single instance only: add exception to master event
     if object.get_recurrence_id() and master:
         object.set_lastmodified()  # update last-modified timestamp
         master.add_exception(object)
         saveobj = master
 
     log.debug(_("Save %s %r to user folder %r") % (saveobj.type, saveobj.uid, targetfolder), level=8)
 
     try:
         imap.imap.m.select(imap.folder_utf7(targetfolder))
         result = imap.imap.m.append(
             imap.folder_utf7(targetfolder),
             None,
             None,
             saveobj.to_message(creator="Kolab Server <wallace@localhost>").as_string()
         )
         return result
 
     except Exception, errmsg:
         log.error(_("Failed to save %s to user folder at %r: %r") % (
             saveobj.type, targetfolder, errmsg
         ))
 
     return False
 
 
 def delete_object(existing):
     """
         Removes the IMAP object with the given UID from a user's folder
     """
     targetfolder = existing._imap_folder
     msguid = existing._msguid if hasattr(existing, '_msguid') else None
 
     try:
         imap.imap.m.select(imap.folder_utf7(targetfolder))
 
         # delete by IMAP UID
         if msguid is not None:
             log.debug(_("Delete %s %r in %r by UID: %r") % (
                 existing.type, existing.uid, targetfolder, msguid
             ), level=8)
 
             imap.imap.m.uid('store', msguid, '+FLAGS', '(\\Deleted)')
         else:
             res, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid)
 
             log.debug(_("Delete %s %r in %r: %r") % (
                 existing.type, existing.uid, targetfolder, data
             ), level=8)
 
             for num in data[0].split():
                 imap.imap.m.store(num, '+FLAGS', '(\\Deleted)')
 
         imap.imap.m.expunge()
         return True
 
     except Exception, errmsg:
         log.error(_("Failed to delete %s from folder %r: %r") % (
             existing.type, targetfolder, errmsg
         ))
 
     return False
 
 
 def send_update_notification(object, receiving_user, old=None, reply=True, sender=None, comment=None):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
     global auth
 
-    import smtplib
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
     from email.header import Header
     from email import charset
 
     # encode unicode strings with quoted-printable
     charset.add_charset('utf-8', charset.SHORTEST, charset.QP)
 
     organizer = object.get_organizer()
     orgemail = organizer.email()
     orgname = organizer.name()
 
     itip_comment = None
     if sender is not None and not comment == '':
         itip_comment = _("%s commented: %s") % (_attendee_name(sender), comment)
 
     if reply:
         log.debug(_("Compose participation status summary for %s %r to user %r") % (
             object.type, object.uid, receiving_user['mail']
         ), level=8)
 
         auto_replies_expected = 0
         auto_replies_received = 0
         is_manual_reply = True
         partstats = {'ACCEPTED': [], 'TENTATIVE': [], 'DECLINED': [], 'DELEGATED': [], 'IN-PROCESS': [], 'COMPLETED': [], 'PENDING': []}
 
         for attendee in object.get_attendees():
             parstat = attendee.get_participant_status(True)
             if partstats.has_key(parstat):
                 partstats[parstat].append(attendee.get_displayname())
             else:
                 partstats['PENDING'].append(attendee.get_displayname())
 
             # look-up kolabinvitationpolicy for this attendee
             if attendee.get_cutype() == kolabformat.CutypeResource:
                 resource_dns = auth.find_resource(attendee.get_email())
                 if isinstance(resource_dns, list):
                     attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
                 else:
                     attendee_dn = resource_dns
             else:
                 attendee_dn = user_dn_from_email_address(attendee.get_email())
 
             if attendee_dn:
                 attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
                 if is_auto_reply(attendee_rec, orgemail, object.type):
                     auto_replies_expected += 1
                     if not parstat == 'NEEDS-ACTION':
                         auto_replies_received += 1
 
                     if sender is not None and sender.get_email() == attendee.get_email():
                         is_manual_reply = False
 
         # skip notification until we got replies from all automatically responding attendees
         if not is_manual_reply and auto_replies_received < auto_replies_expected:
             log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
                 auto_replies_received, auto_replies_expected
             ), level=8)
             return
 
         # build notification message body
         roundup = ''
 
         if itip_comment is not None:
             roundup += "\n" + itip_comment
 
         for status,attendees in partstats.iteritems():
             if len(attendees) > 0:
                 roundup += "\n" + participant_status_label(status) + ":\n\t" + "\n\t".join(attendees) + "\n"
     else:
         # build notification message body
         roundup = ''
 
         if itip_comment is not None:
             roundup += "\n" + itip_comment
 
         roundup += "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
 
         # list properties changed from previous version
         if old:
             diff = xmlutils.compute_diff(old.to_dict(), object.to_dict())
             if len(diff) > 1:
                 roundup += "\n"
                 for change in diff:
                     if not change['property'] in ['created','lastmodified-date','sequence']:
                         new_value = xmlutils.property_to_string(change['property'], change['new']) if change['new'] else _("(removed)")
                         if new_value:
                             roundup += "\n- %s: %s" % (xmlutils.property_label(change['property']), new_value)
 
     # compose different notification texts for events/tasks
     if object.type == 'task':
         message_text = _("""
             The assignment for '%(summary)s' has been updated in your tasklist.
             %(roundup)s
         """) % {
             'summary': object.get_summary(),
             'roundup': roundup
         }
     else:
         message_text = _("""
             The event '%(summary)s' at %(start)s has been updated in your calendar.
             %(roundup)s
         """) % {
             'summary': object.get_summary(),
             'start': xmlutils.property_to_string('start', object.get_start()),
             'roundup': roundup
         }
 
     if object.get_recurrence_id():
         message_text += _("NOTE: This update only refers to this single occurrence!") + "\n"
 
     message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
 
     # compose mime message
     msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8')
 
     msg['To'] = receiving_user['mail']
     msg['Date'] = formatdate(localtime=True)
     msg['Subject'] = utils.str2unicode(_('"%s" has been updated') % (object.get_summary()))
     msg['From'] = Header(utils.str2unicode('%s' % orgname) if orgname else '')
     msg['From'].append("<%s>" % orgemail)
 
-    modules._sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    success = modules._sendmail(orgemail, receiving_user['mail'], msg.as_string())
     log.debug(_("Sent update notification to %r: %r") % (receiving_user['mail'], success), level=8)
 
 def send_cancel_notification(object, receiving_user, deleted=False, sender=None, comment=None):
     """
         Send a notification about event/task cancellation
     """
-    import smtplib
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
     from email.header import Header
     from email import charset
 
     # encode unicode strings with quoted-printable
     charset.add_charset('utf-8', charset.SHORTEST, charset.QP)
 
     log.debug(_("Send cancellation notification for %s %r to user %r") % (
         object.type, object.uid, receiving_user['mail']
     ), level=8)
 
     organizer = object.get_organizer()
     orgemail = organizer.email()
     orgname = organizer.name()
 
     # compose different notification texts for events/tasks
     if object.type == 'task':
         message_text = _("The assignment for '%(summary)s' has been cancelled by %(organizer)s.") % {
             'summary': object.get_summary(),
             'organizer': orgname if orgname else orgemail
         }
         if deleted:
             message_text += " " + _("The copy in your tasklist has been removed accordingly.")
         else:
             message_text += " " + _("The copy in your tasklist has been marked as cancelled accordingly.")
     else:
         message_text = _("The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.") % {
             'summary': object.get_summary(),
             'start': xmlutils.property_to_string('start', object.get_start()),
             'organizer': orgname if orgname else orgemail
         }
         if deleted:
             message_text += " " + _("The copy in your calendar has been removed accordingly.")
         else:
             message_text += " " + _("The copy in your calendar has been marked as cancelled accordingly.")
 
     if sender is not None and not comment == '':
         message_text += "\n" + _("%s commented: %s") % (_attendee_name(sender), comment)
 
     if object.get_recurrence_id():
         message_text += "\n" + _("NOTE: This cancellation only refers to this single occurrence!")
 
     message_text += "\n\n" + _("*** This is an automated message. Please do not reply. ***")
 
     # compose mime message
     msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8')
 
     msg['To'] = receiving_user['mail']
     msg['Date'] = formatdate(localtime=True)
     msg['Subject'] = utils.str2unicode(_('"%s" has been cancelled') % (object.get_summary()))
     msg['From'] = Header(utils.str2unicode('%s' % orgname) if orgname else '')
     msg['From'].append("<%s>" % orgemail)
 
-    modules._sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    success = modules._sendmail(orgemail, receiving_user['mail'], msg.as_string())
     log.debug(_("Sent cancel notification to %r: %r") % (receiving_user['mail'], success), level=8)
 
 def is_auto_reply(user, sender_email, type):
     accept_available = False
     accept_conflicts = False
     for policy in get_matching_invitation_policies(user, sender_email, object_type_conditons.get(type, COND_TYPE_EVENT)):
         if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE):
             if check_policy_condition(policy, True):
                 accept_available = True
             if check_policy_condition(policy, False):
                 accept_conflicts = True
 
         # we have both cases covered by a policy
         if accept_available and accept_conflicts:
             return True
 
         # manual action reached
         if policy & (ACT_MANUAL | ACT_SAVE_TO_FOLDER):
             return False
 
     return False
 
 
 def check_policy_condition(policy, available):
     condition_fulfilled = True
     if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
         condition_fulfilled = available
     if policy & COND_IF_CONFLICT:
         condition_fulfilled = not condition_fulfilled
     return condition_fulfilled
 
 
 def propagate_changes_to_attendees_accounts(object, updated_attendees=None):
     """
         Find and update copies of this object in all attendee's personal folders
     """
     recurrence_id = object.get_recurrence_id()
 
     for attendee in object.get_attendees():
         attendee_user_dn = user_dn_from_email_address(attendee.get_email())
         if attendee_user_dn:
             attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
             (attendee_object, master_object) = find_existing_object(object.uid, object.type, recurrence_id, attendee_user, True)  # does IMAP authenticate
             if attendee_object:
                 # find attendee's entry by one of its email addresses
                 attendee_emails = auth.extract_recipient_addresses(attendee_user)
                 for attendee_email in attendee_emails:
                     try:
                         attendee_entry = attendee_object.get_attendee_by_email(attendee_email)
                     except:
                         attendee_entry = None
                     if attendee_entry:
                         break
 
                 # copy all attendees from master object (covers additions and removals)
                 new_attendees = []
                 for a in object.get_attendees():
                     # keep my own entry intact
                     if attendee_entry is not None and attendee_entry.get_email() == a.get_email():
                         new_attendees.append(attendee_entry)
                     else:
                         new_attendees.append(a)
 
                 attendee_object.set_attendees(new_attendees)
 
                 if updated_attendees and not recurrence_id:
                     log.debug("Update Attendees %r for %s" % ([a.get_email()+':'+a.get_participant_status(True) for a in updated_attendees], attendee_user['mail']), level=8)
                     attendee_object.update_attendees(updated_attendees, False)
 
                 success = update_object(attendee_object, attendee_user, master_object)
                 log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], object.uid, success), level=8)
 
             else:
                 log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], object.uid), level=8)
 
         else:
             log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8)
 
 
 def invitation_response_text(type):
     footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***")
 
     if type == 'task':
         return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer
     else:
         return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer
 
 
 def _attendee_name(attendee):
     # attendee here can be Attendee or ContactReference
     try:
         name = attendee.get_name()
     except Exception:
         name = attendee.name()
 
     if name == '':
         try:
             name = attendee.get_email()
         except Exception:
             name = attendee.email()
 
     return name
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index c216276..f5f8382 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -1,1456 +1,1445 @@
 # -*- coding: utf-8 -*-
 # Copyright 2010-2015 Kolab Systems AG (http://www.kolabsys.com)
 #
 # Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen a kolabsys.com>
 #
 # 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 <http://www.gnu.org/licenses/>.
 #
 
 import datetime
 import icalendar
 import os
 import pytz
 import random
 import tempfile
 import time
 from urlparse import urlparse
 from dateutil.tz import tzlocal
 import base64
 import uuid
 import re
 
 from email import message_from_string
 from email.parser import Parser
 from email.utils import formataddr
 from email.utils import getaddresses
 
 import modules
 
 import pykolab
 import kolabformat
 
 from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
 from pykolab.xml import to_dt
 from pykolab.xml import utils as xmlutils
 from pykolab.xml import event_from_message
 from pykolab.xml import participant_status_label
 from pykolab.itip import events_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.translate import _
 
 # define some contstants used in the code below
 COND_NOTIFY = 256
 ACT_MANUAL = 1
 ACT_ACCEPT = 2
 ACT_REJECT = 8
 ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY
 
 policy_name_map = {
     'ACT_MANUAL':            ACT_MANUAL,
     'ACT_ACCEPT':            ACT_ACCEPT,
     'ACT_REJECT':            ACT_REJECT,
     'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY
 }
 
 log = pykolab.getLogger('pykolab.wallace')
 conf = pykolab.getConf()
 
 mybasepath = '/var/spool/pykolab/wallace/resources/'
 
 auth = None
 imap = None
 
 def __init__():
     modules.register('resources', execute, description=description(), heartbeat=heartbeat)
 
 def accept(filepath):
     new_filepath = os.path.join(
             mybasepath,
             'ACCEPT',
             os.path.basename(filepath)
         )
 
     cleanup()
     os.rename(filepath, new_filepath)
     filepath = new_filepath
     exec('modules.cb_action_ACCEPT(%r, %r)' % ('resources',filepath))
 
 def description():
     return """Resource management module."""
 
 def cleanup():
     global auth, imap
 
     log.debug("cleanup(): %r, %r" % (auth, imap), level=8)
 
     auth.disconnect()
     del auth
 
     # Disconnect IMAP or we lock the mailbox almost constantly
     imap.disconnect()
     del imap
 
 def execute(*args, **kw):
     global auth, imap
 
     # (re)set language to default
     pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
 
     if not os.path.isdir(mybasepath):
         os.makedirs(mybasepath)
 
     for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]:
         if not os.path.isdir(os.path.join(mybasepath, stage)):
             os.makedirs(os.path.join(mybasepath, stage))
 
     log.debug(_("Resource Management called for %r, %r") % (args, kw), level=8)
 
     auth = Auth()
     imap = IMAP()
 
     # TODO: Test for correct call.
     filepath = args[0]
 
     if kw.has_key('stage'):
         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'],
                             'resources',
                             filepath
                         )
                 )
 
             return filepath
     else:
         # Move to incoming
         new_filepath = os.path.join(
                 mybasepath,
                 'incoming',
                 os.path.basename(filepath)
             )
 
         if not filepath == new_filepath:
             log.debug("Renaming %r to %r" % (filepath, new_filepath))
             os.rename(filepath, new_filepath)
             filepath = new_filepath
 
     # parse full message
     message = Parser().parse(open(filepath, 'r'))
 
     # invalid message, skip
     if not message.get('X-Kolab-To'):
         return filepath
 
     recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))]
     sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0]
 
     any_itips = False
     any_resources = False
     possibly_any_resources = False
     reference_uid = None
 
     # An iTip message may contain multiple events. Later on, test if the message
     # is an iTip message by checking the length of this list.
     try:
         itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL'])
     except Exception, e:
         log.error(_("Failed to parse iTip events from message: %r" % (e)))
         itip_events = []
 
     if not len(itip_events) > 0:
         log.info(
                 _("Message is not an iTip message or does not contain any " + \
                     "(valid) iTip.")
             )
 
     else:
         any_itips = True
 
         log.debug(
                 _("iTip events attached to this message contain the " + \
                     "following information: %r") % (itip_events),
                 level=8
             )
 
     if any_itips:
         # See if any iTip actually allocates a resource.
         if len([x['resources'] for x in itip_events if x.has_key('resources')]) > 0 \
             or len([x['attendees'] for x in itip_events if x.has_key('attendees')]) > 0:
                 possibly_any_resources = True
 
     if possibly_any_resources:
         auth.connect()
 
         for recipient in recipients:
             # extract reference UID from recipients like resource+UID@domain.org
             if re.match('.+\+[A-Za-z0-9=/-]+@', recipient):
                 try:
                     (prefix, host) = recipient.split('@')
                     (local, uid) = prefix.split('+')
                     reference_uid = base64.b64decode(uid, '-/')
                     recipient = local + '@' + host
                 except:
                     continue
 
             if not len(resource_record_from_email_address(recipient)) == 0:
                 resource_recipient = recipient
                 any_resources = True
 
     if any_resources:
         if not any_itips:
             log.debug(_("Not an iTip message, but sent to resource nonetheless. Reject message"), level=5)
             reject(filepath)
             return False
         else:
             # Continue. Resources and iTips. We like.
             pass
     else:
         if not any_itips:
             log.debug(_("No itips, no resources, pass along %r") % (filepath), level=5)
             return filepath
         else:
             log.debug(_("iTips, but no resources, pass along %r") % (filepath), level=5)
             return filepath
 
     # A simple list of merely resource entry IDs that hold any relevance to the
     # iTip events
     resource_dns = resource_records_from_itip_events(itip_events, resource_recipient)
 
     # check if resource attendees match the envelope recipient
     if len(resource_dns) == 0:
         log.info(_("No resource attendees matching envelope recipient %s, Reject message") % (resource_recipient))
         log.debug("%r" % (itip_events), level=8)
         reject(filepath)
         return False
 
 
     # Get the resource details, which includes details on the IMAP folder
     # This may append resource collection members to recource_dns
     resources = get_resource_records(resource_dns)
 
     log.debug(_("Resources: %r; %r") % (resource_dns, resources), level=8)
 
     imap.connect()
 
     done = False
     receiving_resource = resources[resource_dns[0]]
 
     for itip_event in itip_events:
         if itip_event['method'] == 'REPLY':
             done = True
 
             # find initial reservation referenced by the reply
             if reference_uid:
                 (event, master) = find_existing_event(reference_uid, itip_event['recurrence-id'], receiving_resource)
                 log.debug(_("iTip REPLY to %r, %r; matches %r") % (reference_uid, itip_event['recurrence-id'], type(event)), level=8)
 
                 if event:
                     try:
                         sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email)
                         owner_reply = sender_attendee.get_participant_status()
                         log.debug(_("Sender Attendee: %r => %r") % (sender_attendee, owner_reply), level=8)
                     except Exception, e:
                         log.error(_("Could not find envelope sender attendee: %r") % (e))
                         continue
 
                     # compare sequence number to avoid outdated replies
                     if not itip_event['sequence'] == event.get_sequence():
                         log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Ignoring.") % (
                             itip_event['sequence'], event.get_sequence()
                         ))
                         continue
 
                     # forward owner response comment
                     comment = itip_event['xml'].get_comment()
                     if comment:
                         event.set_comment(str(comment))
 
                     _itip_event = dict(xml=event, uid=event.get_uid(), _master=master)
                     _itip_event['recurrence-id'] = event.get_recurrence_id()
 
                     if owner_reply == kolabformat.PartAccepted:
                         event.set_status(kolabformat.StatusConfirmed)
                         accept_reservation_request(_itip_event, receiving_resource, confirmed=True)
                     elif owner_reply == kolabformat.PartDeclined:
                         decline_reservation_request(_itip_event, receiving_resource)
                     else:
                         log.info(_("Invalid response (%r) received from resource owner for event %r") % (
                             sender_attendee.get_participant_status(True), reference_uid
                         ))
                 else:
                     log.info(_("Event referenced by this REPLY (%r) not found in resource calendar") % (reference_uid))
 
             else:
                 log.info(_("No event reference found in this REPLY. Ignoring."))
 
             # exit for-loop
             break
 
         # else:
 
         try:
             receiving_attendee = itip_event['xml'].get_attendee_by_email(receiving_resource['mail'])
             log.debug(_("Receiving Resource: %r; %r") % (receiving_resource, receiving_attendee), level=8)
         except Exception, e:
             log.error(_("Could not find envelope attendee: %r") % (e))
             continue
 
         # ignore updates and cancellations to resource collections who already delegated the event
         if len(receiving_attendee.get_delegated_to()) > 0 or receiving_attendee.get_role() == kolabformat.NonParticipant:
             done = True
             log.debug(_("Recipient %r is non-participant, ignoring message") % (receiving_resource['mail']), level=8)
 
         # process CANCEL messages
         if not done and itip_event['method'] == "CANCEL":
             for resource in resource_dns:
                 if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()] \
                     and resources[resource].has_key('kolabtargetfolder'):
                     (event, master) = find_existing_event(itip_event['uid'], itip_event['recurrence-id'], resources[resource])
                     # remove entire event
                     if event and master is None:
                         log.debug(_("Cancellation for entire event %r: deleting") % (itip_event['uid']), level=8)
                         delete_resource_event(itip_event['uid'], resources[resource], event._msguid)
                     # just cancel one single occurrence: add exception with status=cancelled
                     elif master is not None:
                         log.debug(_("Cancellation for a single occurrence %r of %r: updating...") % (itip_event['recurrence-id'], itip_event['uid']), level=8)
                         event.set_status('CANCELLED')
                         event.set_transparency(True)
                         _itip_event = dict(xml=event, uid=event.get_uid(), _master=master)
                         _itip_event['recurrence-id'] = event.get_recurrence_id()
                         save_resource_event(_itip_event, resources[resource])
 
             done = True
 
     if done:
         os.unlink(filepath)
         cleanup()
         return
 
 
     # do the magic for the receiving attendee
     (available_resource, itip_event) = check_availability(itip_events, resource_dns, resources, receiving_attendee)
 
     reject = False
     resource = None
     original_resource = None
 
     # accept reservation
     if available_resource is not None:
         if available_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
             # check if reservation was delegated
             if available_resource['mail'] != receiving_resource['mail'] and receiving_attendee.get_participant_status() == kolabformat.PartDelegated:
                 original_resource = receiving_resource
 
             resource = available_resource
         else:
             # This must have been a resource collection originally.
             # We have inserted the reference to the original resource
             # record in 'memberof'.
             if available_resource.has_key('memberof'):
                 original_resource = resources[available_resource['memberof']]
 
                 if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
                     #
                     # Delegate:
                     # - delegator: the original resource collection
                     # - delegatee: the target resource
                     #
                     itip_event['xml'].delegate(original_resource['mail'], available_resource['mail'], available_resource['cn'])
 
                     # set delegator to NON-PARTICIPANT and RSVP=FALSE
                     delegator = itip_event['xml'].get_attendee_by_email(original_resource['mail'])
                     delegator.set_role(kolabformat.NonParticipant)
                     delegator.set_rsvp(False)
 
                     log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], available_resource['mail']), level=8)
                     resource = available_resource
 
     # Look for ACT_REJECT policy
     if resource is not None:
         invitationpolicy = get_resource_invitationpolicy(resource)
         log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=8)
 
         if invitationpolicy is not None:
             for policy in invitationpolicy:
                 if policy & ACT_REJECT:
                     reject = True
                     break
 
     if resource is not None and not reject:
         log.debug(_("Accept invitation for individual resource %r / %r") % (resource['dn'], resource['mail']), level=8)
         accept_reservation_request(itip_event, resource, original_resource, False, invitationpolicy)
     else:
         resource = resources[resource_dns[0]]  # this is the receiving resource record
         log.debug(_("Decline invitation for individual resource %r / %r") % (resource['dn'], resource['mail']), level=8)
         decline_reservation_request(itip_event, resource)
 
     cleanup()
 
     os.unlink(filepath)
 
 
 def heartbeat(lastrun):
     global imap
 
     # run archival job every hour only
     now = int(time.time())
     if lastrun == 0 or now - heartbeat._lastrun < 3600:
         return
 
     log.debug(_("module_resources.heartbeat(%d)") % (heartbeat._lastrun), level=8)
 
     # get a list of resource records from LDAP
     auth = Auth()
     auth.connect()
 
     resource_dns = auth.find_resource('*')
 
     # Remove referrals
     resource_dns = [dn for dn in resource_dns if dn is not None]
 
     # filter by resource_base_dn
     resource_base_dn = conf.get('ldap', 'resource_base_dn', None)
     if resource_base_dn is not None:
         resource_dns = [dn for dn in resource_dns if resource_base_dn in dn]
 
     if len(resource_dns) > 0:
         imap = IMAP()
         imap.connect()
 
         for resource_dn in resource_dns:
             resource_attrs = auth.get_entry_attributes(None, resource_dn, ['kolabtargetfolder'])
             if resource_attrs.has_key('kolabtargetfolder'):
                 try:
                     expunge_resource_calendar(resource_attrs['kolabtargetfolder'])
                 except Exception, e:
                     log.error(_("Expunge resource calendar for %s (%s) failed: %r") % (
                         resource_dn, resource_attrs['kolabtargetfolder'], e
                     ))
 
         imap.disconnect()
 
     auth.disconnect()
 
     heartbeat._lastrun = now
 
 heartbeat._lastrun = 0
 
 
 def expunge_resource_calendar(mailbox):
     """
         Cleanup routine to remove events older than 100 days from the given resource calendar
     """
     global imap
 
     days = int(conf.get('wallace', 'resource_calendar_expire_days'))
     now = datetime.datetime.now(tzlocal())
     expire_date = now - datetime.timedelta(days=days)
 
     log.debug(
         _("Expunge events in resource folder %r older than %d days") % (mailbox, days),
         level=8
     )
 
     # might raise an exception, let that bubble
     targetfolder = imap.folder_quote(mailbox)
     imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
     imap.imap.m.select(targetfolder)
 
     typ, data = imap.imap.m.search(None, 'UNDELETED')
 
     for num in data[0].split():
         log.debug(
             _("Fetching message ID %r from folder %r") % (num, mailbox),
             level=8
         )
 
         typ, data = imap.imap.m.fetch(num, '(RFC822)')
 
         event_message = message_from_string(data[0][1])
 
         try:
             event = event_from_message(message_from_string(data[0][1]))
         except Exception, e:
             log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e))
             continue
 
         if event:
             dt_end = to_dt(event.get_end())
 
             # consider recurring events and get real end date
             if event.is_recurring():
                 dt_end = to_dt(event.get_last_occurrence())
                 if dt_end is None:
                     # skip if recurring forever
                     continue
 
             if dt_end and dt_end < expire_date:
                 age = now - dt_end
                 log.debug(_("Flag event %s from message %s/%s as deleted (age = %d days)") % (event.uid, mailbox, num, age.days), level=8)
                 imap.imap.m.store(num, '+FLAGS', '\\Deleted')
 
     imap.imap.m.expunge()
 
 
 def check_availability(itip_events, resource_dns, resources, receiving_attendee=None):
     """
         For each resource, determine if any of the events in question are in conflict.
     """
 
     # Store the (first) conflicting event(s) alongside the resource information.
     start = time.time()
     num_messages = 0
     available_resource = None
 
     for resource in resources.keys():
         # skip this for resource collections
         if not resources[resource].has_key('kolabtargetfolder'):
             continue
 
         # sets the 'conflicting' flag and adds a list of conflicting events found
         try:
             num_messages += read_resource_calendar(resources[resource], itip_events)
         except Exception, e:
             log.error(_("Failed to read resource calendar for %r: %r") % (resource, e))
 
     end = time.time()
 
     log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=8)
 
 
     # For each resource (collections are first!)
     # check conflicts and either accept or decline the reservation request
     for resource in resource_dns:
         log.debug(_("Polling for resource %r") % (resource), level=8)
 
         if not resources.has_key(resource):
             log.debug(_("Resource %r has been popped from the list") % (resource), level=8)
             continue
 
         if not resources[resource].has_key('conflicting_events'):
             log.debug(_("Resource is a collection"), level=8)
 
             # check if there are non-conflicting collection members
             conflicting_members = [x for x in resources[resource]['uniquemember'] if resources[x]['conflict']]
 
             # found at least one non-conflicting member, remove the conflicting ones and continue
             if len(conflicting_members) < len(resources[resource]['uniquemember']):
                 for member in conflicting_members:
                     resources[resource]['uniquemember'] = [x for x in resources[resource]['uniquemember'] if x != member]
                     del resources[member]
 
                 log.debug(_("Removed conflicting resources from %r: (%r) => %r") % (
                     resource, conflicting_members, resources[resource]['uniquemember']
                 ), level=8)
 
             else:
                 # TODO: shuffle existing bookings of collection members in order
                 # to make one available for the requested time
                 pass
 
             continue
 
         if len(resources[resource]['conflicting_events']) > 0:
             log.debug(_("Conflicting events: %r for resource %r") % (resources[resource]['conflicting_events'], resource), level=8)
 
             done = False
 
             # This is the event being conflicted with!
             for itip_event in itip_events:
                 # do not re-assign single occurrences to another resource
                 if itip_event['recurrence-id'] is not None:
                     continue
 
                 # Now we have the event that was conflicting
                 if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
                     # this resource initially was delegated from a collection ?
                     if receiving_attendee and receiving_attendee.get_email() == resources[resource]['mail'] \
                             and len(receiving_attendee.get_delegated_from()) > 0:
                         for delegator in receiving_attendee.get_delegated_from():
                             collection_data = get_resource_collection(delegator.email())
                             if collection_data is not None:
                                 # check if another collection member is available
                                 (available_resource, dummy) = check_availability(itip_events, collection_data[0], collection_data[1])
                                 break
 
                         if available_resource is not None:
                             log.debug(_("Delegate to another resource collection member: %r to %r") % \
                                 (resources[resource]['mail'], available_resource['mail']), level=8)
 
                             # set this new resource as delegate for the receiving_attendee
                             itip_event['xml'].delegate(resources[resource]['mail'], available_resource['mail'], available_resource['cn'])
 
                             # set delegator to NON-PARTICIPANT and RSVP=FALSE
                             receiving_attendee.set_role(kolabformat.NonParticipant)
                             receiving_attendee.set_rsvp(False)
                             receiving_attendee.setDelegatedFrom([])
 
                             # remove existing_events as we now delegated back to the collection
                             if len(resources[resource]['existing_events']) > 0:
                                 for existing in resources[resource]['existing_events']:
                                     delete_resource_event(existing.uid, resources[resource], existing._msguid)
 
                     done = True
 
                 if done:
                     break
 
         else:
             # No conflicts, go accept
             for itip_event in itip_events:
                 # directly invited resource
                 if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
                     available_resource = resources[resource]
                     done = True
 
                 else:
                     # This must have been a resource collection originally.
                     # We have inserted the reference to the original resource
                     # record in 'memberof'.
                     if resources[resource].has_key('memberof'):
                         original_resource = resources[resources[resource]['memberof']]
 
                         # Randomly select a target resource from the resource collection.
                         available_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
                         done = True
 
         if done:
             break
 
     # end for resource in resource_dns:
 
     return (available_resource, itip_event)
 
 
 def read_resource_calendar(resource_rec, itip_events):
     """
         Read all booked events from the given resource's calendar
         and check for conflicts with the given list if itip events
     """
     global imap
 
     resource_rec['conflict'] = False
     resource_rec['conflicting_events'] = []
     resource_rec['existing_events'] = []
 
     mailbox = resource_rec['kolabtargetfolder']
 
     log.debug(
         _("Checking events in resource folder %r") % (mailbox),
         level=8
     )
 
     # set read ACLs for admin user
     imap.set_acl(mailbox, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrs")
 
     # might raise an exception, let that bubble
     imap.imap.m.select(imap.folder_quote(mailbox))
     typ, data = imap.imap.m.search(None, 'UNDELETED')
 
     num_messages = len(data[0].split())
 
     for num in data[0].split():
         # For efficiency, makes the routine non-deterministic
         if resource_rec['conflict']:
             continue
 
         log.debug(
             _("Fetching message UID %r from folder %r") % (num, mailbox),
             level=8
         )
 
         typ, data = imap.imap.m.fetch(num, '(UID RFC822)')
 
         try:
             msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1)
         except Exception, e:
             log.error(_("No UID found in IMAP response: %r") % (data[0][0]))
             continue
 
         event_message = message_from_string(data[0][1])
 
         try:
             event = event_from_message(message_from_string(data[0][1]))
         except Exception, e:
             log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e))
             continue
 
         if event:
             for itip in itip_events:
                 conflict = check_event_conflict(event, itip)
 
                 if event.get_uid() == itip['uid']:
                     setattr(event, '_msguid', msguid)
                     if event.is_recurring() or itip['recurrence-id']:
                         resource_rec['existing_master'] = event
                     else:
                         resource_rec['existing_events'].append(event)
 
                 if conflict:
                     log.info(
                         _("Event %r conflicts with event %r") % (
                             itip['xml'].get_uid(),
                             event.get_uid()
                         )
                     )
 
                     resource_rec['conflicting_events'].append(event.get_uid())
                     resource_rec['conflict'] = True
 
     return num_messages
 
 
 def find_existing_event(uid, recurrence_id, resource_rec):
     """
         Search the resources's calendar folder for the given event (by UID)
     """
     global imap
 
     event = None
     master = None
     mailbox = resource_rec['kolabtargetfolder']
 
     log.debug(_("Searching %r for event %r") % (mailbox, uid), level=8)
 
     try:
         imap.imap.m.select(imap.folder_quote(mailbox))
         typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
     except Exception, e:
         log.error(_("Failed to access resource calendar:: %r") % (e))
         return event
 
     for num in reversed(data[0].split()):
         typ, data = imap.imap.m.fetch(num, '(UID RFC822)')
 
         try:
             msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1)
         except Exception, e:
             log.error(_("No UID found in IMAP response: %r") % (data[0][0]))
             continue
 
         try:
             event = event_from_message(message_from_string(data[0][1]))
 
             # find instance in a recurring series
             if recurrence_id and (event.is_recurring() or event.has_exceptions()):
                 master = event
                 event = master.get_instance(recurrence_id)
                 setattr(master, '_msguid', msguid)
 
                 # return master, even if instance is not found
                 if not event and master.uid == uid:
                     return (event, master)
 
             # compare recurrence-id and skip to next message if not matching
             elif recurrence_id and not xmlutils.dates_equal(recurrence_id, event.get_recurrence_id()):
                 log.debug(_("Recurrence-ID not matching on message %s, skipping: %r != %r") % (
                     msguid, recurrence_id, event.get_recurrence_id()
                 ), level=8)
                 continue
 
             if event is not None:
                 setattr(event, '_msguid', msguid)
 
         except Exception, e:
             log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e))
             event = None
             master = None
             continue
 
         if event and event.uid == uid:
             return (event, master)
 
     return (event, master)
 
 
 def accept_reservation_request(itip_event, resource, delegator=None, confirmed=False, invitationpolicy=None):
     """
         Accepts the given iTip event by booking it into the resource's
         calendar. Then set the attendee status of the given resource to
         ACCEPTED and sends an iTip reply message to the organizer.
     """
     owner = get_resource_owner(resource)
     confirmation_required = False
 
     if not confirmed and owner:
         if invitationpolicy is None:
             invitationpolicy = get_resource_invitationpolicy(resource)
         log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=8)
 
         if invitationpolicy is not None:
             for policy in invitationpolicy:
                 if policy & ACT_MANUAL and owner['mail']:
                     confirmation_required = True
                     break
 
     partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
 
     itip_event['xml'].set_transparency(False);
     itip_event['xml'].set_attendee_participant_status(
         itip_event['xml'].get_attendee_by_email(resource['mail']),
         partstat
     )
 
     saved = save_resource_event(itip_event, resource)
 
     log.debug(
         _("Adding event to %r: %r") % (resource['kolabtargetfolder'], saved),
         level=8
     )
 
     if saved:
         send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner)
 
     if owner and confirmation_required:
         send_owner_confirmation(resource, owner, itip_event)
     elif owner:
         send_owner_notification(resource, owner, itip_event, saved)
 
 
 def decline_reservation_request(itip_event, resource):
     """
         Set the attendee status of the given resource to
         DECLINED and send an according iTip reply to the organizer.
     """
 
     itip_event['xml'].set_attendee_participant_status(
         itip_event['xml'].get_attendee_by_email(resource['mail']),
         "DECLINED"
     )
 
     # update master event
     if resource.get('existing_master') is not None or itip_event.get('_master') is not None:
         save_resource_event(itip_event, resource)
 
     # remove old copy of the reservation
     elif resource.get('existing_events', []) and len(resource['existing_events']) > 0:
         for existing in resource['existing_events']:
             delete_resource_event(existing.uid, resource, existing._msguid)
 
     # delete old event referenced by itip_event (from owner confirmation)
     elif hasattr(itip_event['xml'], '_msguid'):
         delete_resource_event(itip_event['xml'].uid, resource, itip_event['xml']._msguid)
 
     # send response and notification
     owner = get_resource_owner(resource)
     send_response(resource['mail'], itip_event, owner)
 
     if owner:
         send_owner_notification(resource, owner, itip_event, True)
 
 
 def save_resource_event(itip_event, resource):
     """
         Append the given event object to the resource's calendar
     """
     try:
         save_event = itip_event['xml']
         targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
 
         # add exception to existing recurring main event
         if resource.get('existing_master') is not None:
             save_event = resource['existing_master']
             save_event.add_exception(itip_event['xml'])
 
         elif itip_event.get('_master') is not None:
             save_event = itip_event['_master']
             save_event.add_exception(itip_event['xml'])
 
         # remove old copy of the reservation (also sets ACLs)
         if resource.has_key('existing_events') and len(resource['existing_events']) > 0:
             for existing in resource['existing_events']:
                 delete_resource_event(existing.uid, resource, existing._msguid)
 
         # delete old version referenced save_event
         elif hasattr(save_event, '_msguid'):
             delete_resource_event(save_event.uid, resource, save_event._msguid)
 
         else:
             imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
 
         # append new version
         result = imap.imap.m.append(
             targetfolder,
             None,
             None,
             save_event.to_message(creator="Kolab Server <wallace@localhost>").as_string()
         )
         return result
 
     except Exception, e:
         log.error(_("Failed to save event to resource calendar at %r: %r") % (
             resource['kolabtargetfolder'], e
         ))
 
     return False
 
 
 def delete_resource_event(uid, resource, msguid=None):
     """
         Removes the IMAP object with the given UID from a resource's calendar folder
     """
     targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
 
     try:
         imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
         imap.imap.m.select(targetfolder)
 
         # delete by IMAP UID
         if msguid is not None:
             log.debug(_("Delete resource calendar object from %r by UID %r") % (
                 targetfolder, msguid
             ), level=8)
 
             imap.imap.m.uid('store', msguid, '+FLAGS', '(\\Deleted)')
         else:
             typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % uid)
 
             log.debug(_("Delete resource calendar object %r in %r: %r") % (
                 uid, resource['kolabtargetfolder'], data
             ), level=8)
 
             for num in data[0].split():
                 imap.imap.m.store(num, '+FLAGS', '\\Deleted')
 
         imap.imap.m.expunge()
         return True
 
     except Exception, e:
         log.error(_("Failed to delete calendar object %r from folder %r: %r") % (
             uid, targetfolder, e
         ))
 
     return False
 
 
 def reject(filepath):
     new_filepath = os.path.join(
             mybasepath,
             'REJECT',
             os.path.basename(filepath)
         )
 
     os.rename(filepath, new_filepath)
     filepath = new_filepath
     exec('modules.cb_action_REJECT(%r, %r)' % ('resources',filepath))
 
 
 def resource_record_from_email_address(email_address):
     """
         Resolves the given email address to a resource entity
     """
     global auth
 
     if not auth:
         auth = Auth()
         auth.connect()
 
     resource_records = []
 
     local_domains = auth.list_domains()
 
     if not local_domains == None:
         local_domains = list(set(local_domains.keys()))
 
     if not email_address.split('@')[1] in local_domains:
         return []
 
     log.debug(
         _("Checking if email address %r belongs to a resource (collection)") % (email_address),
         level=8
     )
 
     resource_records = auth.find_resource(email_address)
 
     if isinstance(resource_records, list):
         if len(resource_records) > 0:
             log.debug(_("Resource record(s): %r") % (resource_records), level=8)
         else:
             log.debug(_("No resource (collection) records found for %r") % (email_address), level=8)
 
     elif isinstance(resource_records, basestring):
         resource_records = [ resource_records ]
         log.debug(_("Resource record: %r") % (resource_records), level=8)
 
     return resource_records
 
 
 def resource_records_from_itip_events(itip_events, recipient_email=None):
     """
         Given a list of itip_events, determine which resources have been
         invited as attendees and/or resources.
     """
     global auth
 
     if not auth:
         auth = Auth()
         auth.connect()
 
     resource_records = []
 
     log.debug(_("Raw itip_events: %r") % (itip_events), level=8)
     attendees_raw = []
     for list_attendees_raw in [x for x in [y['attendees'] for y in itip_events if y.has_key('attendees') and isinstance(y['attendees'], list)]]:
         attendees_raw.extend(list_attendees_raw)
 
     for list_attendees_raw in [y['attendees'] for y in itip_events if y.has_key('attendees') and isinstance(y['attendees'], basestring)]:
         attendees_raw.append(list_attendees_raw)
 
     log.debug(_("Raw set of attendees: %r") % (attendees_raw), level=8)
 
     # TODO: Resources are actually not implemented in the format. We reset this
     # list later.
     resources_raw = []
     for list_resources_raw in [x for x in [y['resources'] for y in itip_events if y.has_key('resources')]]:
         resources_raw.extend(list_resources_raw)
 
     log.debug(_("Raw set of resources: %r") % (resources_raw), level=8)
 
     # consider organizer (in REPLY messages), too
     organizers_raw = [re.sub('\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
 
     log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8)
 
 
     # TODO: We expect the format of an attendee line to literally be:
     #
     #   ATTENDEE:RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers@kolabsys.com
     #
     # which makes the attendees_raw contain:
     #
     #   RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers@kolabsys.com
     #
     attendees = [x.split(':')[-1] for x in attendees_raw + organizers_raw]
 
     # Limit the attendee resources to the one that is actually invited
     # with the current message. Considering all invited resources would result in
     # duplicate responses from every iTip message sent to a resource.
     if recipient_email is not None:
         attendees = [a for a in attendees if a == recipient_email]
 
     for attendee in attendees:
         log.debug(_("Checking if attendee %r is a resource (collection)") % (attendee), level=8)
 
         _resource_records = auth.find_resource(attendee)
 
         if isinstance(_resource_records, list):
             if len(_resource_records) > 0:
                 resource_records.extend(_resource_records)
                 log.debug(_("Resource record(s): %r") % (_resource_records), level=8)
             else:
                 log.debug(_("No resource (collection) records found for %r") % (attendee), level=8)
 
         elif isinstance(_resource_records, basestring):
             resource_records.append(_resource_records)
             log.debug(_("Resource record: %r") % (_resource_records), level=8)
 
         else:
             log.warning(_("Resource reservation made but no resource records found"))
 
     # Escape the non-implementation of the free-form, undefined RESOURCES
     # list(s) in iTip.
     if len(resource_records) == 0:
 
         # TODO: We don't know how to handle this yet!
         # We expect the format of an resource line to literally be:
         #   RESOURCES:MAILTO:resource-car@kolabsys.com
         resources_raw = []
 
         resources = [x.split(':')[-1] for x in resources_raw]
 
         # Limit the attendee resources to the one that is actually invited
         # with the current message.
         if recipient_email is not None:
             resources = [a for a in resources if a == recipient_email]
 
         for resource in resources:
             log.debug(_("Checking if resource %r is a resource (collection)") % (resource), level=8)
 
             _resource_records = auth.find_resource(resource)
             if isinstance(_resource_records, list):
                 if len(_resource_records) > 0:
                     resource_records.extend(_resource_records)
                     log.debug(_("Resource record(s): %r") % (_resource_records), level=8)
 
                 else:
                     log.debug(_("No resource (collection) records found for %r") % (resource), level=8)
 
             elif isinstance(_resource_records, basestring):
                 resource_records.append(_resource_records)
                 log.debug(_("Resource record: %r") % (_resource_records), level=8)
             else:
                 log.warning(_("Resource reservation made but no resource records found"))
 
 
     log.debug(_("The following resources are being referred to in the " + \
                 "iTip: %r") % (resource_records), level=8)
 
     return resource_records
 
 
 def get_resource_records(resource_dns):
     """
         Get the resource details, which includes details on the IMAP folder
     """
     global auth
 
     resources = {}
     for resource_dn in list(set(resource_dns)):
         # Get the attributes for the record
         # See if it is a resource collection
         #   If it is, expand to individual resources
         #   If it is not, ...
         resource_attrs = auth.get_entry_attributes(None, resource_dn, ['*'])
         resource_attrs['dn'] = resource_dn
         parse_kolabinvitationpolicy(resource_attrs)
 
         if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
             if resource_attrs.has_key('uniquemember'):
                 if not isinstance(resource_attrs['uniquemember'], list):
                     resource_attrs['uniquemember'] = [ resource_attrs['uniquemember'] ]
                 resources[resource_dn] = resource_attrs
                 for uniquemember in resource_attrs['uniquemember']:
                     member_attrs = auth.get_entry_attributes(
                             None,
                             uniquemember,
                             ['*']
                         )
 
                     if 'kolabsharedfolder' in [x.lower() for x in member_attrs['objectclass']]:
                         member_attrs['dn'] = uniquemember
                         parse_kolabinvitationpolicy(member_attrs, resource_attrs)
 
                         resources[uniquemember] = member_attrs
                         resources[uniquemember]['memberof'] = resource_dn
                         if not member_attrs.has_key('owner') and resources[resource_dn].has_key('owner'):
                             resources[uniquemember]['owner'] = resources[resource_dn]['owner']
                         resource_dns.append(uniquemember)
         else:
             resources[resource_dn] = resource_attrs
 
     return resources
 
 
 def parse_kolabinvitationpolicy(attrs, parent=None):
     if attrs.has_key('kolabinvitationpolicy'):
         if not isinstance(attrs['kolabinvitationpolicy'], list):
             attrs['kolabinvitationpolicy'] = [attrs['kolabinvitationpolicy']]
         attrs['kolabinvitationpolicy'] = [policy_name_map[p] for p in attrs['kolabinvitationpolicy'] if policy_name_map.has_key(p)]
 
     elif isinstance(parent, dict) and parent.has_key('kolabinvitationpolicy'):
         attrs['kolabinvitationpolicy'] = parent['kolabinvitationpolicy']
 
 
 def get_resource_collection(email_address):
     """
         
     """
     resource_dns = resource_record_from_email_address(email_address)
     if len(resource_dns) == 1:
         resource_attrs = auth.get_entry_attributes(None, resource_dns[0], ['objectclass'])
         if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
             resources = get_resource_records(resource_dns)
             return (resource_dns, resources)
 
     return None
 
 
 def get_resource_owner(resource):
     """
         Get this resource's owner record
     """
     global auth
 
     if not auth:
         auth = Auth()
         auth.connect()
 
     owners = []
 
     if resource.has_key('owner'):
         if not isinstance(resource['owner'], list):
             owners = [ resource['owner'] ]
         else:
             owners = resource['owner']
 
     else:
         # get owner attribute from collection
         collections = auth.search_entry_by_attribute('uniquemember', resource['dn'])
         if not isinstance(collections, list):
             collections = [ collections ]
 
         for dn,collection in collections:
             if collection.has_key('owner') and isinstance(collection['owner'], list):
                 owners += collection['owner']
             elif collection.has_key('owner'):
                 owners.append(collection['owner'])
 
     for dn in owners:
         owner = auth.get_entry_attributes(None, dn, ['cn','mail','telephoneNumber'])
         if owner is not None:
             return owner
 
     return None
 
 
 def get_resource_invitationpolicy(resource):
     """
         Get this resource's kolabinvitationpolicy configuration
     """
     global auth
 
     if not resource.has_key('kolabinvitationpolicy') or resource['kolabinvitationpolicy'] is None:
         if not auth:
             auth = Auth()
             auth.connect()
 
         # get kolabinvitationpolicy attribute from collection
         collections = auth.search_entry_by_attribute('uniquemember', resource['dn'])
         if not isinstance(collections, list):
             collections = [ (collections['dn'],collections) ]
 
         log.debug(_("Check collections %r for kolabinvitationpolicy attributes") % (collections), level=8)
 
         for dn,collection in collections:
             # ldap.search_entry_by_attribute() doesn't return the attributes lower-cased
             if collection.has_key('kolabInvitationPolicy'):
                 collection['kolabinvitationpolicy'] = collection['kolabInvitationPolicy']
 
             if collection.has_key('kolabinvitationpolicy'):
                 parse_kolabinvitationpolicy(collection)
                 resource['kolabinvitationpolicy'] = collection['kolabinvitationpolicy']
                 break
 
     return resource['kolabinvitationpolicy'] if resource.has_key('kolabinvitationpolicy') else None
 
 
 def send_response(from_address, itip_events, owner=None):
     """
         Send the given iCal events as a valid iTip response to the organizer.
         In case the invited resource coolection was delegated to a concrete
         resource, this will send an additional DELEGATED response message.
     """
 
     if isinstance(itip_events, dict):
         itip_events = [ itip_events ]
 
     for itip_event in itip_events:
         attendee = itip_event['xml'].get_attendee_by_email(from_address)
         participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
 
         # TODO: look-up event organizer in LDAP and change localization to its preferredlanguage
 
         message_text = reservation_response_text(participant_status, owner)
         subject_template = _("Reservation Request for %(summary)s was %(status)s")
 
         # Extra actions to take: send delegated reply
         if participant_status == "DELEGATED":
             delegatee = [a for a in itip_event['xml'].get_attendees() if from_address in a.get_delegated_from(True)][0]
 
             delegated_message_text = _("""
                 *** This is an automated response, please do not reply! ***
 
                 Your reservation was delegated to "%s" which is available for the requested time.
             """) % (delegatee.get_name())
 
             pykolab.itip.send_reply(from_address, itip_event, delegated_message_text,
                 subject=subject_template)
 
             # adjust some vars for the regular reply from the delegatee
             message_text = reservation_response_text(delegatee.get_participant_status(True), owner)
             from_address = delegatee.get_email()
             time.sleep(2)
 
         pykolab.itip.send_reply(from_address, itip_event, message_text,
             subject=subject_template)
 
 
 def reservation_response_text(status, owner):
     message_text = _("""
         *** This is an automated response, please do not reply! ***
         
         We hereby inform you that your reservation was %s.
     """) % (participant_status_label(status))
 
     if owner:
         message_text += _("""
             If you have questions about this reservation, please contact
             %s <%s> %s
         """) % (owner['cn'], owner['mail'], owner['telephoneNumber'] if owner.has_key('telephoneNumber') else '')
     
     return message_text
 
 
 def send_owner_notification(resource, owner, itip_event, success=True):
     """
         Send a reservation notification to the resource owner
     """
-    import smtplib
     from pykolab import utils
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
 
     # encode unicode strings with quoted-printable
     from email import charset
     charset.add_charset('utf-8', charset.SHORTEST, charset.QP)
 
     notify = False
     status = itip_event['xml'].get_attendee_by_email(resource['mail']).get_participant_status(True)
 
     invitationpolicy = get_resource_invitationpolicy(resource)
 
     if invitationpolicy is not None:
         for policy in invitationpolicy:
             # TODO: distingish ACCEPTED / DECLINED status notifications?
             if policy & COND_NOTIFY and owner['mail']:
                 notify = True
                 break
 
     if notify or not success:
         log.debug(
             _("Sending booking notification for event %r to %r from %r") % (
                 itip_event['uid'], owner['mail'], resource['cn']
             ),
             level=8
         )
 
         # change gettext language to the preferredlanguage setting of the resource owner
         if owner.has_key('preferredlanguage'):
             pykolab.translate.setUserLanguage(owner['preferredlanguage'])
 
         message_text = owner_notification_text(resource, owner, itip_event['xml'], success)
 
         msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8')
 
         msg['To'] = owner['mail']
         msg['From'] = resource['mail']
         msg['Date'] = formatdate(localtime=True)
         msg['Subject'] = utils.str2unicode(_('Booking for %s has been %s') % (
             resource['cn'], participant_status_label(status) if success else _('failed')
         ))
 
-        smtp = smtplib.SMTP("localhost", 10027)
-
-        if conf.debuglevel > 8:
-            smtp.set_debuglevel(True)
-
-        try:
-            smtp.sendmail(resource['mail'], owner['mail'], msg.as_string())
-        except Exception, e:
-            log.error(_("SMTP sendmail error: %r") % (e))
-
-        smtp.quit()
+        modules._sendmail(resource['mail'], owner['mail'], msg.as_string())
 
 def owner_notification_text(resource, owner, event, success):
     organizer = event.get_organizer()
     status = event.get_attendee_by_email(resource['mail']).get_participant_status(True)
 
     if success:
         message_text = _("""
             The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s.
 
             *** This is an automated message, sent to you as the resource owner. ***
         """)
     else:
         message_text = _("""
             A reservation request for %(resource)s could not be processed automatically.
             Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s.
 
             *** This is an automated message, sent to you as the resource owner. ***
         """)
 
     return message_text % {
         'resource': resource['cn'],
         'summary': event.get_summary(),
         'date': event.get_date_text(),
         'status': participant_status_label(status),
         'orgname': organizer.name(),
         'orgemail': organizer.email()
     }
 
 
 def send_owner_confirmation(resource, owner, itip_event):
     """
         Send a reservation request to the resource owner for manual confirmation (ACCEPT or DECLINE)
 
         This clones the given invtation with a new UID and setting the resource as organizer in order to
         receive the reply from the owner.
     """
 
     uid = itip_event['uid']
     event = itip_event['xml']
     organizer = event.get_organizer()
     event_attendees = [a.get_displayname() for a in event.get_attendees() if not a.get_cutype() == kolabformat.CutypeResource]
 
     log.debug(
         _("Clone invitation for owner confirmation: %r from %r") % (
             itip_event['uid'], event.get_organizer().email()
         ),
         level=8
     )
 
     # generate new UID and set the resource as organizer
     (mail, domain) = resource['mail'].split('@')
     event.set_uid(str(uuid.uuid4()))
     event.set_organizer(mail + '+' + base64.b64encode(uid, '-/') + '@' + domain, resource['cn'])
     itip_event['uid'] = event.get_uid()
 
     # add resource owner as (the sole) attendee
     event._attendees = []
     event.add_attendee(owner['mail'], owner['cn'], rsvp=True, role=kolabformat.Required, participant_status=kolabformat.PartNeedsAction)
 
     # flag this iTip message as confirmation type
     event.add_custom_property('X-Kolab-InvitationType', 'CONFIRMATION')
 
     message_text = _("""
         A reservation request for %(resource)s requires your approval!
         Please either accept or decline this invitation without saving it to your calendar.
 
         The reservation request was sent from %(orgname)s <%(orgemail)s>.
 
         Subject: %(summary)s.
         Date: %(date)s
         Participants: %(attendees)s
 
         *** This is an automated message, please don't reply by email. ***
     """)% {
         'resource': resource['cn'],
         'orgname': organizer.name(),
         'orgemail': organizer.email(),
         'summary': event.get_summary(),
         'date': event.get_date_text(),
         'attendees': ",\n+ ".join(event_attendees)
     }
 
     pykolab.itip.send_request(owner['mail'], itip_event, message_text,
         subject=_('Booking request for %s requires confirmation') % (resource['cn']),
         direct=True)