Page MenuHomePhorge

module_resources.py
No OneTemporary

Authored By
Unknown
Size
27 KB
Referenced Files
None
Subscribers
None

module_resources.py

# -*- coding: utf-8 -*-
# Copyright 2010-2012 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; version 3 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
import datetime
import icalendar
import json
import os
import pytz
import random
import tempfile
import time
from urlparse import urlparse
import urllib
from email import message_from_file
from email import message_from_string
from email.utils import formataddr
from email.utils import getaddresses
import modules
import pykolab
from pykolab.auth import Auth
from pykolab.conf import Conf
from pykolab.imap import IMAP
from pykolab.xml import event_from_ical
from pykolab.xml import event_from_string
from pykolab.xml import to_dt
from pykolab.translate import _
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())
def accept(filepath):
new_filepath = os.path.join(
mybasepath,
'ACCEPT',
os.path.basename(filepath)
)
os.rename(filepath, new_filepath)
filepath = new_filepath
exec('modules.cb_action_ACCEPT(%r, %r)' % ('resources',filepath))
def description():
return """Resource management module."""
def execute(*args, **kw):
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=9)
auth = Auth()
auth.connect()
imap = IMAP()
imap.connect()
# 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
else:
# Move to incoming
new_filepath = os.path.join(
mybasepath,
'incoming',
os.path.basename(filepath)
)
os.rename(filepath, new_filepath)
filepath = new_filepath
message = message_from_file(open(filepath, 'r'))
any_itips = False
any_resources = False
possibly_any_resources = True
# An iTip message may contain multiple events. Later on, test if the message
# is an iTip message by checking the length of this list.
itip_events = itip_events_from_message(message)
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=9
)
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:
if len([x['attendees'] for x in itip_events if x.has_key('attendees')]) == 0:
possibly_any_resources = False
else:
possibly_any_resources = False
if possibly_any_resources:
recipients = {
"To": getaddresses(message.get_all('To', [])),
"Cc": getaddresses(message.get_all('Cc', []))
# TODO: Are those all recipient addresses?
}
log.debug("Recipients: %r" % recipients)
for recipient in recipients['Cc'] + recipients['To']:
if not len(resource_record_from_email_address(recipient[1])) == 0:
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
else:
# Continue. Resources and iTips. We like.
pass
else:
if not any_itips:
log.debug(_("No itips, no resources, pass along"), level=5)
accept(filepath)
return
else:
log.debug(_("iTips, but no resources, pass along"), level=5)
accept(filepath)
return
# A simple list of merely resource entry IDs that hold any relevance to the
# iTip events
resource_records = resource_records_from_itip_events(itip_events)
# Get the resource details, which includes details on the IMAP folder
resources = {}
for resource_record in list(set(resource_records)):
# 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_record, ['*'])
if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
if resource_attrs.has_key('uniquemember'):
resources[resource_record] = resource_attrs
for uniquemember in resource_attrs['uniquemember']:
resource_attrs = auth.get_entry_attributes(
None,
uniquemember,
['*']
)
if 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
resources[uniquemember] = resource_attrs
resources[uniquemember]['memberof'] = resource_record
else:
resources[resource_record] = resource_attrs
log.debug(_("Resources: %r") % (resources), level=8)
# For each resource, determine if any of the events in question is in
# conflict.
#
# Store the (first) conflicting event(s) alongside the resource information.
start = time.time()
for resource in resources.keys():
if not resources[resource].has_key('kolabtargetfolder'):
continue
mailbox = resources[resource]['kolabtargetfolder']
resources[resource]['conflict'] = False
resources[resource]['conflicting_events'] = []
log.debug(
_("Checking events in resource folder %r") % (mailbox),
level=8
)
try:
imap.imap.m.select(mailbox)
except:
log.error(_("Mailbox for resource %r doesn't exist") % (resource))
continue
typ, data = imap.imap.m.search(None, 'ALL')
num_messages = len(data[0].split())
for num in data[0].split():
# For efficiency, makes the routine non-deterministic
if resources[resource]['conflict']:
continue
log.debug(
_("Fetching message UID %r from folder %r") % (num, mailbox),
level=9
)
typ, data = imap.imap.m.fetch(num, '(RFC822)')
event_message = message_from_string(data[0][1])
if event_message.is_multipart():
for part in event_message.walk():
if part.get_content_type() == "application/calendar+xml":
payload = part.get_payload(decode=True)
event = pykolab.xml.event_from_string(payload)
for itip in itip_events:
_es = to_dt(event.get_start())
_is = to_dt(itip['start'].dt)
_ee = to_dt(event.get_end())
_ie = to_dt(itip['end'].dt)
if _es < _is:
if _es <= _ie:
if _ee <= _is:
conflict = False
else:
conflict = True
else:
conflict = True
elif _es == _is:
conflict = True
else: # _es > _is
if _es <= _ie:
conflict = True
else:
conflict = False
if conflict:
log.info(
_("Event %r conflicts with event " + \
"%r") % (
itip['xml'].get_uid(),
event.get_uid()
)
)
resources[resource]['conflicting_events'].append(event)
resources[resource]['conflict'] = True
end = time.time()
log.debug(
_("start: %r, end: %r, total: %r, messages: %r") % (
start, end, (end-start), num_messages
),
level=1
)
for resource in resources.keys():
log.debug(_("Polling for resource %r") % (resource), level=9)
if not resources.has_key(resource):
log.debug(
_("Resource %r has been popped from the list") % (resource),
level=9
)
continue
if not resources[resource].has_key('conflicting_events'):
log.debug(_("Resource is a collection"), level=9)
continue
if len(resources[resource]['conflicting_events']) > 0:
# This is the event being conflicted with!
for itip_event in itip_events:
# Now we have the event that was conflicting
if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
itip_event['xml'].set_attendee_participant_status(
[a for a in itip_event['xml'].get_attendees() if a.get_email() == resources[resource]['mail']][0],
"DECLINED"
)
send_response(resources[resource]['mail'], itip_events)
# TODO: Accept the message to the other attendees
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']]
_target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
# unset all
for _r in original_resource['uniquemember']:
del resources[_r]
if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
itip_event['xml'].set_attendee_participant_status(
[a for a in itip_event['xml'].get_attendees() if a.get_email() == original_resource['mail']][0],
"DECLINED"
)
send_response(original_resource['mail'], itip_events)
# TODO: Accept the message to the other attendees
else:
# No conflicts, go accept
for itip_event in itip_events:
if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
itip_event['xml'].set_attendee_participant_status(
[a for a in itip_event['xml'].get_attendees() if a.get_email() == resources[resource]['mail']][0],
"ACCEPTED"
)
log.debug(
_("Adding event to %r") % (
resources[resource]['kolabtargetfolder']
),
level=9
)
imap.imap.m.setacl(resources[resource]['kolabtargetfolder'], "cyrus-admin", "lrswipkxtecda")
imap.imap.m.append(
resources[resource]['kolabtargetfolder'],
None,
None,
itip_event['xml'].to_message().as_string()
)
send_response(resources[resource]['mail'], itip_event)
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 selects a target resource from the resource
# collection.
_target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
# Remove all resources from the collection.
for _r in original_resource['uniquemember']:
del resources[_r]
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'],
_target_resource['mail']
)
itip_event['xml'].set_attendee_participant_status(
[a for a in itip_event['xml'].get_attendees() if a.get_email() == _target_resource['mail']][0],
"ACCEPTED"
)
log.debug(
_("Adding event to %r") % (
_target_resource['kolabtargetfolder']
),
level=9
)
# TODO: The Cyrus IMAP (or Dovecot) Administrator login
# name comes from configuration.
imap.imap.m.setacl(_target_resource['kolabtargetfolder'], "cyrus-admin", "lrswipkxtecda")
imap.imap.m.append(
_target_resource['kolabtargetfolder'],
None,
None,
itip_event['xml'].to_message().as_string()
)
send_response(original_resource['mail'], itip_event)
# Disconnect IMAP or we lock the mailbox almost constantly
imap.disconnect()
del imap
os.unlink(filepath)
def itip_events_from_message(message):
"""
Obtain the iTip payload from email.message <message>
"""
# Placeholder for any itip_events found in the message.
itip_events = []
# iTip methods we are actually interested in. Other methods will be ignored.
itip_methods = [ "REQUEST", "REPLY", "ADD", "CANCEL" ]
# TODO: Are all iTip messages multipart? RFC 6047, section 2.4 states "A
# MIME body part containing content information that conforms to this
# document MUST have (...)" but does not state whether an iTip message must
# therefore also be multipart.
if message.is_multipart():
# Check each part
for part in message.walk():
# The iTip part MUST be Content-Type: text/calendar (RFC 6047,
# section 2.4)
if part.get_content_type() == "text/calendar":
if not part.get_param('method') in itip_methods:
log.error(
_("Method %r not really interesting for us.") % (
part.get_param('method')
)
)
# Get the itip_payload
itip_payload = part.get_payload(decode=True)
log.debug(_("Raw iTip payload: %s") % (itip_payload))
# Python iCalendar prior to 3.0 uses "from_string".
if hasattr(icalendar.Calendar, 'from_ical'):
cal = icalendar.Calendar.from_ical(itip_payload)
elif hasattr(icalendar.Calendar, 'from_string'):
cal = icalendar.Calendar.from_string(itip_payload)
# If we can't read it, we're out
else:
log.error(_("Could not read iTip from message."))
return []
for c in cal.walk():
if c.name == "VEVENT":
itip = {}
# From the event, take the following properties:
#
# - start
# - end (if any)
# - duration (if any)
# - organizer
# - attendees (if any)
# - resources (if any)
# - TODO: recurrence rules (if any)
# Where are these stored actually?
#
if c.has_key('dtstart'):
itip['start'] = c['dtstart']
else:
log.error(_("iTip event without a start"))
continue
if c.has_key('dtend'):
itip['end'] = c['dtend']
if c.has_key('duration'):
itip['duration'] = c['duration']
itip['organizer'] = c['organizer']
itip['attendees'] = c['attendee']
if c.has_key('resources'):
itip['resources'] = c['resources']
itip['raw'] = itip_payload
itip['xml'] = event_from_ical(c.to_ical())
itip_events.append(itip)
# end if c.name == "VEVENT"
# end for c in cal.walk()
# end if part.get_content_type() == "text/calendar"
# end for part in message.walk()
else: # if message.is_multipart()
log.debug(_("Message is not an iTip message (non-multipart message)"), level=5)
return itip_events
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):
auth = Auth()
auth.connect()
resource_records = []
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(
_("No resource (collection) records found for %r") % (
email_address
),
level=9
)
else:
log.debug(
_("Resource record(s): %r") % (resource_records),
level=8
)
elif isinstance(resource_records, basestring):
log.debug(
_("Resource record: %r") % (resource_records),
level=8
)
resource_records = [ resource_records ]
return resource_records
def resource_records_from_itip_events(itip_events):
"""
Given a list of itip_events, determine which resources have been
invited as attendees and/or resources.
"""
auth = Auth()
auth.connect()
resource_records = []
log.debug(_("Raw itip_events: %r") % (itip_events), level=9)
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=9)
# 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=9)
# 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]
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:
log.debug(
_("No resource (collection) records found for %r") % (
attendee
),
level=9
)
else:
log.debug(
_("Resource record(s): %r") % (_resource_records),
level=8
)
resource_records.extend(_resource_records)
elif isinstance(_resource_records, basestring):
log.debug(
_("Resource record: %r") % (_resource_records),
level=8
)
resource_records.append(_resource_records)
else:
log.warning(
_("Resource reservation made but no resource records found")
)
# TODO: Escape the non-implementation of the free-form, undefined RESOURCES
# list(s) in iTip. We don't know how to handle this yet!
resources_raw = []
# TODO: We expect the format of an resource line to literally be:
#
# RESOURCES:MAILTO:resource-car@kolabsys.com
#
# which makes the resources_raw contain:
#
# MAILTO:resource-car@kolabsys.com
#
resources = [x.split(':')[-1] for x in resources_raw]
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:
log.debug(
_("No resource (collection) records found for %r") % (
resource
),
level=8
)
else:
log.debug(
_("Resource record(s): %r") % (_resource_records),
level=8
)
resource_records.extend(_resource_records)
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 send_response(from_address, itip_events):
import smtplib
smtp = smtplib.SMTP("localhost", 10027)
if conf.debuglevel > 8:
smtp.set_debuglevel(True)
if isinstance(itip_events, dict):
itip_events = [ itip_events ]
for itip_event in itip_events:
attendee = [a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address][0]
participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
if participant_status == "DELEGATED":
# Extra actions to take
delegator = [a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address][0]
delegatee = [a for a in itip_event['xml'].get_attendees() if from_address in [b.email() for b in a.get_delegated_from()]][0]
itip_event['xml'].event.setAttendees([ delegator, delegatee ])
message = itip_event['xml'].to_message_itip(delegatee.get_email(), method="REPLY", participant_status=itip_event['xml'].get_ical_attendee_participant_status(delegatee))
smtp.sendmail(message['From'], message['To'], message.as_string())
itip_event['xml'].event.setAttendees([a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address])
message = itip_event['xml'].to_message_itip(from_address, method="REPLY", participant_status=participant_status)
smtp.sendmail(message['From'], message['To'], message.as_string())
smtp.quit()

File Metadata

Mime Type
text/x-script.python
Expires
Sun, Apr 5, 11:07 PM (2 w, 1 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
51/44/d5b6b0c3f52f576f9f879099ddd5
Default Alt Text
module_resources.py (27 KB)

Event Timeline