Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New calendar functionalities #56

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion pyexchange/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""
import logging
from .exchange2010 import Exchange2010Service # noqa
from .connection import ExchangeNTLMAuthConnection # noqa
from .connection import ExchangeNTLMAuthConnection, ExchangeBasicAuthConnection # noqa

# Silence notification of no default logging handler
log = logging.getLogger("pyexchange")
Expand Down
12 changes: 9 additions & 3 deletions pyexchange/base/soap.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
from ..exceptions import FailedExchangeException

SOAP_NS = u'http://schemas.xmlsoap.org/soap/envelope/'
TYPE_NS = u'http://schemas.microsoft.com/exchange/services/2006/types'

SOAP_NAMESPACES = {u's': SOAP_NS}
SOAP_NAMESPACES = {u't': TYPE_NS, u's': SOAP_NS}
S = ElementMaker(namespace=SOAP_NS, nsmap=SOAP_NAMESPACES)
T = ElementMaker(namespace=TYPE_NS, nsmap=SOAP_NAMESPACES)

log = logging.getLogger('pyexchange')

Expand All @@ -37,7 +39,8 @@ def send(self, xml, headers=None, retries=4, timeout=30, encoding="utf-8"):
def _parse(self, response, encoding="utf-8"):

try:
tree = etree.XML(response.encode(encoding))
parser = etree.XMLParser(recover=True)
tree = etree.fromstring(response, parser=parser)
except (etree.XMLSyntaxError, TypeError) as err:
raise FailedExchangeException(u"Unable to parse response from Exchange - check your login information. Error: %s" % err)

Expand Down Expand Up @@ -65,8 +68,11 @@ def _send_soap_request(self, xml, headers=None, retries=2, timeout=30, encoding=
response = self.connection.send(body, headers, retries, timeout)
return response

def _exchange_header(self):
return T.RequestServerVersion({u'Version': u'Exchange2010'})

def _wrap_soap_xml_request(self, exchange_xml):
root = S.Envelope(S.Body(exchange_xml))
root = S.Envelope(S.Header(self._exchange_header()), S.Body(exchange_xml))
return root

def _parse_date(self, date_string):
Expand Down
66 changes: 60 additions & 6 deletions pyexchange/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
import requests
from requests_ntlm import HttpNtlmAuth
from requests.auth import HTTPBasicAuth

import logging

Expand All @@ -17,7 +18,7 @@
class ExchangeBaseConnection(object):
""" Base class for Exchange connections."""

def send(self, body, headers=None, retries=2, timeout=30, encoding="utf-8"):
def send(self, body, headers=None, retries=2, timeout=30):
raise NotImplementedError


Expand All @@ -37,7 +38,7 @@ def build_password_manager(self):
if self.password_manager:
return self.password_manager

log.debug(u'Constructing password manager')
log.debug(u'Constructing NTLM auth password manager')

self.password_manager = HttpNtlmAuth(self.username, self.password)

Expand All @@ -47,7 +48,7 @@ def build_session(self):
if self.session:
return self.session

log.debug(u'Constructing opener')
log.debug(u'Constructing NTLM auth opener')

self.password_manager = self.build_password_manager()

Expand All @@ -56,7 +57,60 @@ def build_session(self):

return self.session

def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
def send(self, body, headers=None, retries=2, timeout=30):
if not self.session:
self.session = self.build_session()

try:
response = self.session.post(self.url, data=body, headers=headers, verify = self.verify_certificate)
response.raise_for_status()
except requests.exceptions.RequestException as err:
log.debug(getattr(err.response, 'content', 'No response.'))
raise FailedExchangeException(u'Unable to connect to Exchange with NTLM: %s' % err)

log.info(u'Got response: {code}'.format(code=response.status_code))
log.debug(u'Got response headers: {headers}'.format(headers=response.headers))
log.debug(u'Got body: {body}'.format(body=response.text))

return response.content


class ExchangeBasicAuthConnection(ExchangeBaseConnection):
""" Connection to Exchange, Office365 that uses Basic authentication """

def __init__(self, url, username, password, verify_certificate=True, **kwargs):
self.url = url
self.username = username
self.password = password
self.verify_certificate = verify_certificate
self.handler = None
self.session = None
self.password_manager = None

def build_password_manager(self):
if self.password_manager:
return self.password_manager

log.debug(u'Constructing basic auth password manager')

self.password_manager = HTTPBasicAuth(self.username, self.password)

return self.password_manager

def build_session(self):
if self.session:
return self.session

log.debug(u'Constructing opener with Basic auth')

self.password_manager = self.build_password_manager()

self.session = requests.Session()
self.session.auth = self.password_manager

return self.session

def send(self, body, headers=None, retries=2, timeout=30):
if not self.session:
self.session = self.build_session()

Expand All @@ -65,10 +119,10 @@ def send(self, body, headers=None, retries=2, timeout=30, encoding=u"utf-8"):
response.raise_for_status()
except requests.exceptions.RequestException as err:
log.debug(err.response.content)
raise FailedExchangeException(u'Unable to connect to Exchange: %s' % err)
raise FailedExchangeException(u'Unable to connect to Exchange with Basic auth: %s' % err)

log.info(u'Got response: {code}'.format(code=response.status_code))
log.debug(u'Got response headers: {headers}'.format(headers=response.headers))
log.debug(u'Got body: {body}'.format(body=response.text))

return response.text
return response.content
6 changes: 6 additions & 0 deletions pyexchange/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ class ExchangeInternalServerTransientErrorException(FailedExchangeException):
pass


class ExchangeMailboxNotFoundException(FailedExchangeException):
"""Raised when the mailbox is not found"""
pass


class InvalidEventType(Exception):
"""Raised when a method for an event gets called on the wrong type of event."""
pass

173 changes: 170 additions & 3 deletions pyexchange/exchange2010/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from ..base.calendar import BaseExchangeCalendarEvent, BaseExchangeCalendarService, ExchangeEventOrganizer, ExchangeEventResponse
from ..base.folder import BaseExchangeFolder, BaseExchangeFolderService
from ..base.soap import ExchangeServiceSOAP
from ..exceptions import FailedExchangeException, ExchangeStaleChangeKeyException, ExchangeItemNotFoundException, ExchangeInternalServerTransientErrorException, ExchangeIrresolvableConflictException, InvalidEventType
from ..exceptions import FailedExchangeException, ExchangeStaleChangeKeyException, ExchangeItemNotFoundException, ExchangeInternalServerTransientErrorException, ExchangeIrresolvableConflictException, InvalidEventType, ExchangeMailboxNotFoundException
from ..compat import BASESTRING_TYPES

from . import soap_request
Expand All @@ -31,11 +31,14 @@ def mail(self):
raise NotImplementedError("Sorry - nothin' here. Feel like adding it? :)")

def contacts(self):
raise NotImplementedError("Sorry - nothin' here. Feel like adding it? :)")
return Exchange2010ContactsService(service=self)

def folder(self):
return Exchange2010FolderService(service=self)

def rooms(self):
return Exchange2010RoomService(service=self)

def _send_soap_request(self, body, headers=None, retries=2, timeout=30, encoding="utf-8"):
headers = {
"Accept": "text/xml",
Expand Down Expand Up @@ -76,10 +79,32 @@ def _check_for_exchange_fault(self, xml_tree):
elif code.text == u"ErrorCalendarOccurrenceIndexIsOutOfRecurrenceRange":
# just means some or all of the requested instances are out of range
pass
elif code.text == u"ErrorNonExistentMailbox":
# exchange mailbox is not existing
raise ExchangeMailboxNotFoundException(u"Exchange Fault (%s) from Exchange server" % code.text)
elif code.text != u"NoError":
raise FailedExchangeException(u"Exchange Fault (%s) from Exchange server" % code.text)


class Exchange2010RoomService(object):
def __init__(self, service):
self.service = service

def list_room_lists(self):
return Exchange2010RoomList(service=self.service)

def list_rooms(self, roomList_email):
return Exchange2010Rooms(service=self.service, roomList_email=roomList_email)


class Exchange2010ContactsService(object):
def __init__(self, service):
self.service = service

def search_contacts(self, search_key):
return Exchange2010Contacts(service=self.service, search_key=search_key)


class Exchange2010CalendarService(BaseExchangeCalendarService):

def event(self, id=None, **kwargs):
Expand All @@ -95,6 +120,138 @@ def list_events(self, start=None, end=None, details=False, delegate_for=None):
return Exchange2010CalendarEventList(service=self.service, calendar_id=self.calendar_id, start=start, end=end, details=details, delegate_for=delegate_for)


class Exchange2010RoomList(object):
def __init__(self, service):
self.service = service
body = soap_request.get_room_lists()

response_xml = self.service.send(body)

self.roomLists = self._parse_all_roomLists(response_xml)
log.debug("Parsed room lists %s" % self.roomLists)

return

def _parse_all_roomLists(self, response):
roomListsDict = list()
roomLists = response.xpath(u'//m:RoomLists/t:Address', namespaces=soap_request.NAMESPACES)
self.count = len(roomLists)
log.debug(u'Found %s room lists' % self.count)

for room in roomLists:
roomListsDict.append(self._parse_roomList_properties(room))

return roomListsDict

def _parse_roomList_properties(self, response):
property_map = {
u'name': {
u'xpath': u't:Name',
},
u'email':
{
u'xpath': u't:EmailAddress',
},
u'routingType': {
u'xpath': u't:RoutingType',
},
u'mailboxType': {
u'xpath': u't:MailboxType',
},
}
return self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)


class Exchange2010Contacts(object):
def __init__(self, service, search_key):
self.service = service
body = soap_request.resolve_names(search_key)
log.debug("Searching contacts %s " % search_key)
response_xml = self.service.send(body)

self.contacts = self._parse_all_contacts(response_xml)

return

def _parse_all_contacts(self, response):
contactsDict = list()
contacts = response.xpath(u'//m:ResolveNamesResponseMessage/m:ResolutionSet/t:Resolution', namespaces=soap_request.NAMESPACES)
self.count = len(contacts)
log.debug(u'Found %s contacts' % self.count)

for contact in contacts:
contactsDict.append(self._parse_contact_properties(contact))

return contactsDict

def _parse_contact_properties(self, response):
property_map = {
u'name': {
u'xpath': u't:Mailbox/t:Name',
},
u'displayName': {
u'xpath': u't:Contact/t:DisplayName',
},
u'contactSource': {
u'xpath': u't:Contact/t:ContactSource',
},
u'email':
{
u'xpath': u't:Mailbox/t:EmailAddress',
},
u'routingType': {
u'xpath': u't:Mailbox/t:RoutingType',
},
u'mailboxType': {
u'xpath': u't:Mailbox/t:MailboxType',
},
}

return self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)


class Exchange2010Rooms(object):
def __init__(self, service, roomList_email):
self.service = service
body = soap_request.get_rooms(roomList_email)
log.debug("Searching rooms in %s list" % roomList_email)
response_xml = self.service.send(body)

self.rooms = self._parse_all_rooms(response_xml)

return

def _parse_all_rooms(self, response):
roomsDict = list()
rooms = response.xpath(u'//m:Rooms/t:Room', namespaces=soap_request.NAMESPACES)
self.count = len(rooms)
log.debug(u'Found %s rooms' % self.count)

for room in rooms:
roomsDict.append(self._parse_room_properties(room))

return roomsDict

def _parse_room_properties(self, response):
property_map = {
u'name': {
u'xpath': u't:Id/t:Name',
},
u'email':
{
u'xpath': u't:Id/t:EmailAddress',
},
u'routingType': {
u'xpath': u't:Id/t:RoutingType',
},
u'mailboxType': {
u'xpath': u't:Id/t:MailboxType',
},
}

return self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)


class Exchange2010CalendarEventList(object):
"""
Creates & Stores a list of Exchange2010CalendarEvent items in the "self.events" variable.
Expand Down Expand Up @@ -572,6 +729,17 @@ def _parse_event_properties(self, response):
{
u'xpath': u'//m:Items/t:CalendarItem/t:Recurrence/t:WeeklyRecurrence/t:DaysOfWeek',
},
u'sensitivity':
{
u'xpath': u'//m:Items/t:CalendarItem/t:Sensitivity',
},
u'is_cancelled':
{
u'xpath': u'//m:Items/t:CalendarItem/t:IsCancelled',
u'cast': u'bool',
},


}

result = self.service._xpath_to_dict(element=response, property_map=property_map, namespace_map=soap_request.NAMESPACES)
Expand Down Expand Up @@ -769,7 +937,6 @@ def find_folder(self, parent_id):
for folder in folders:
folder.delete()
"""

body = soap_request.find_folder(parent_id=parent_id, format=u'AllProperties')
response_xml = self.service.send(body)
return self._parse_response_for_find_folder(response_xml)
Expand Down