-
-
Save philippkeller/5175842 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from ewstype import EWSType | |
from suds.sax.element import Element | |
from suds.transport.https import HttpAuthenticated | |
from suds.client import Client | |
from suds import WebFault | |
from datetime import datetime | |
import urllib2 | |
import os | |
def exchange(username, password): | |
transport = HttpAuthenticated(username=username, password=password) | |
t = HttpAuthenticated(username=username, password=password) | |
t.handler = urllib2.HTTPBasicAuthHandler(t.pm) | |
t.urlopener = urllib2.build_opener(t.handler) | |
cwd = os.path.dirname(os.path.realpath(__file__)) | |
wsdl = "file://%s/wsdl/Services.wsdl" % cwd | |
client = Client(wsdl, transport=t) | |
w = Wrapper(client) | |
return w | |
class Wrapper: | |
'''EWSWrapper functions''' | |
hasMoreItems = False | |
synchState = None | |
#Time Zone settings | |
BaseOffset = "-P0DT1H0M0.0S" | |
Offset = "-P0DT1H0M0.0S" | |
DaylightTime = "02:00:00.0000000" | |
StandardOffset = "P0DT0H0M0.0S" | |
StandardTime = "03:00:00.0000000" | |
TimeZoneName = "(GMT+01:00) Warsaw" | |
def __init__(self, client, version='Exchange2007_SP1'): | |
""" | |
version: see in your types.xsd: the root element has an argument 'version'. That's the version. In my case it was Exchange2007_SP1 | |
""" | |
# Enums | |
self.types = EWSType | |
self.client = client | |
self.version = version | |
def wrap(self, xml): | |
'''Generate the necessary boilerplate XML for a raw SOAP request. | |
The XML is specific to the server version.''' | |
header = Element('t:RequestServerVersion') | |
header.set('Version', self.version) | |
return '''<?xml version="1.0" encoding="UTF-8"?> | |
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"> | |
<s:Header>%s</s:Header> | |
<s:Body>%s</s:Body> | |
</s:Envelope>''' % (header, xml) | |
def _restriction(self, operator, field, value, extended = False): | |
'''Helper function to build retriction call''' | |
if not extended: | |
uri = Element('t:FieldURI') | |
uri.set('FieldURI', field) | |
else: | |
uri = field | |
val = Element('t:Constant') | |
val.set('Value', value) | |
if operator == '==': | |
op = Element('t:IsEqualTo') | |
elif operator == '!=': | |
op = Element('t:IsNotEqualTo') | |
elif operator == '>': | |
op = Element('t:IsGreaterThan') | |
elif operator == '<': | |
op = Element('t:IsLessThan') | |
elif operator == '<=': | |
op = Element('t:IsLessThanOrEqualTo') | |
elif operator == '>=': | |
op = Element('t:IsGreaterThanOrEqualTo') | |
elif operator == 'contains': | |
op = Element('t:Contains') | |
op.set('ContainmentMode', 'Substring') | |
op.set('ContainmentComparison', 'Exact') | |
op.append(uri) | |
op.append(val) | |
return op | |
else: | |
raise Exception('Operator not supported') | |
op.append(uri) | |
const = Element('t:FieldURIOrConstant') | |
const.append(val) | |
op.append(const) | |
return op | |
def _mapi_reference(self, property_id, property_type, set_id): | |
'''construct a mapi field reference''' | |
path = Element('t:ExtendedFieldURI') | |
path.set('DistinguishedPropertySetId', set_id) | |
path.set('PropertyId', property_id) | |
path.set('PropertyType', property_type) | |
return path | |
def listCalendarEvent(self, id=None, start=None, end=None, on_behalf=None, shape='DEFAULT_PROPERTIES', categories=None): | |
'''====================================== | |
// List Calendar Events | |
//====================================== | |
/* @param string id - item id. takes precendense over timeframe | |
* @param string $onbehalf - "on behalf" seneder's email | |
* @param int $start - event start timestamp | |
* @param int $end - event end time | |
* | |
* @return object response | |
*/ | |
''' | |
type = 'CALENDAR' | |
return self.listItems(type, id, start, end, on_behalf, shape, categories) | |
def synchCalendarEvent(self, on_behalf=None, num_to_synch=10, synch_state=None, shape='DEFAULT_PROPERTIES'): | |
'''====================================== | |
// Synchronize Calendar Events | |
//====================================== | |
* @param string $onbehalf - "on behalf": item owner email | |
* @param int num_to_synch - how many items to synch in one go (max 500) | |
* @param string synch_state - current synch state, blank on initial synch | |
* @param string $shape - detail level (enumarted in DefaultShapeNamesType) | |
* | |
* @return object response | |
*/ | |
''' | |
type = 'CALENDAR' | |
return self.synchFolder(type, on_behalf, num_to_synch, synch_state, shape) | |
def addCalendarEvent(self, subject, body, start, end, attendees, on_behalf=None, \ | |
location=None, allday = False, bodyType="TEXT", category="default"): | |
'''===================================== | |
// Add Calendar Event | |
//====================================== | |
/* @param string $subject - event subject | |
* @param int $start - event start timestamp | |
* @param int $end - event end time | |
* @param array $anttendees - array of email addresses of invited poeople | |
* @param string $body - event body | |
* @param string $onbehalf - "on behalf" seneder's email | |
* @param string $location - event loaction | |
* @param bool $allday - is it an all-day event? | |
* @param string $bodyType - body format (Text/HTML) | |
* @param string $category - event actegory | |
* | |
* @return object response | |
* | |
''' | |
createitem = Element('m:CreateItem') | |
createitem.set('SendMeetingInvitations', self.types.EWSType_CalendarItemCreateOrDeleteOperationType.SEND_TO_NONE) | |
saveditemfolderid = Element('m:SavedItemFolderId') | |
distinguishedfolderid = Element('t:DistinguishedFolderId') | |
distinguishedfolderid.set('Id', self.types.EWSType_DistinguishedFolderIdNameType.CALENDAR) | |
if on_behalf is not None: | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(on_behalf) | |
mailbox.append(emailaddress) | |
distinguishedfolderid.append(mailbox) | |
saveditemfolderid.append(distinguishedfolderid) | |
createitem.append(saveditemfolderid) | |
calitems = Element('m:Items') | |
# Prepare objects for CalendarItem creation | |
calitem = Element('t:CalendarItem') | |
timeZone = Element('t:MeetingTimeZone') | |
timeZone.set('TimeZoneName', TimeZoneName) | |
timeZone.append(Element('t:BaseOffset').setText(BaseOffset)) | |
standard = Element('t:Standard') | |
standard.append(Element('t:Offset').setText(StandardOffset)) | |
standard.append(Element('t:Time').setText(StandardTime)) | |
timeZone.append(standard) | |
daylight = Element('t:Daylight') | |
daylight.append(Element('t:Offset').setText(Offset)) | |
daylight.append(Element('t:Time').setText(DaylightTime)) | |
timeZone.append(daylight) | |
Subject = Element('t:Subject') | |
Body = Element('t:Body') | |
Body.set('BodyType', getattr(self.types.EWSType_BodyTypeResponseType, bodyType)) | |
cats = None | |
if category: | |
cats = Element('t:Categories') | |
if not isinstance(category, list): | |
cats.append(Element('t:String').setText(category)) | |
else: | |
for cat in category: | |
cats.append(Element('t:String').setText(cat)) | |
Start = Element('t:Start') | |
End = Element('t:End') | |
Location = Element('t:Location') | |
alldayevent = Element('t:IsAllDayEvent') | |
calitem.append(Subject.setText(subject)) | |
calitem.append(Body.setText(body)) | |
calitem.append(cats) | |
calitem.append(Start.setText(start.ewsformat())) | |
calitem.append(End.setText(end.ewsformat())) | |
calitem.append(alldayevent.setText(int(allday))) | |
if location: | |
calitem.append(Location.setText(location)) | |
atts = Element('t:RequiredAttendees') | |
for attendee in attendees: | |
att = Element('t:Attendee') | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(attendee) | |
mailbox.append(emailaddress) | |
att.append(mailbox) | |
atts.append(att) | |
calitem.append(atts) | |
calitem.append(timeZone) | |
#add item to items | |
calitems.append(calitem) | |
#add item to CreateItems | |
createitem.append(calitems) | |
#make the call | |
xml = self.wrap(createitem) | |
try: | |
result = self.client.service.CreateItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
# Result may come as a single object or a list of objects | |
if type(result.CreateItemResponseMessage).__name__ == 'list': | |
msgs = result.CreateItemResponseMessage | |
else: | |
msgs = [result.CreateItemResponseMessage] | |
idlist = [] | |
for msg in msgs: | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
item = msg.Items.CalendarItem.ItemId | |
idlist.extend([item._Id, item._ChangeKey]) | |
return idlist | |
def editCalendarEvent(self, id, ckey, subject=None, body=None, bodytype="TEXT", \ | |
start=None, end=None, location=None, attendees=[], \ | |
allday=None, category=None): | |
'''====================================== | |
// Edit Calendar Event | |
//====================================== | |
/* @param string $id - event id | |
* @param string $ckey - event change key | |
* @param string $subject - event subject | |
* @param string $body - event body | |
* @param int $start - event start timestamp | |
* @param int $end - event end time | |
* @param string $location - event location | |
* @param array $anttendees - array of email addresses of invited poeople | |
* @param bool $allday - is it an all-day event? | |
* @param string $category - event actegory | |
* | |
* @return object response | |
*/ | |
''' | |
Updates = { | |
'calendar:Start' : start.ewsformat() if start else None, | |
'calendar:End' : end.ewsformat() if end else None, | |
'calendar:Location' : location, | |
'calendar:IsAllDayEvent' : allday, | |
'item:Subject' : subject, | |
} | |
updateitem = Element('m:UpdateItem') | |
updateitem.set('SendMeetingInvitationsOrCancellations', self.types.EWSType_CalendarItemCreateOrDeleteOperationType.SEND_TO_NONE) | |
updateitem.set('MessageDisposition', self.types.EWSType_MessageDispositionType.SAVEONLY) | |
updateitem.set('ConflictResolution', self.types.EWSType_ConflictResolutionType.ALWAYSOVERWRITE) | |
itemchanges = Element('m:ItemChanges') | |
itemchange = Element('t:ItemChange') | |
itemid = Element('t:ItemId') | |
itemid.set('Id', id) | |
itemid.set('ChangeKey', ckey) | |
itemchange.append(itemid) | |
updates = Element('t:Updates') | |
#popoulate update array | |
for key,value in Updates.items(): | |
if value: | |
prop = key.split(':').pop() | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', key) | |
itemfield.append(Element('t:CalendarItem')) | |
itemfield.children[1].append(Element('t:'+prop).setText(value)) | |
updates.append(itemfield) | |
if attendees: | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', 'calendar:RequiredAttendees') | |
itemfield.append(Element('t:CalendarItem')) | |
reqatts = Element('t:RequiredAttendees') | |
for attendee in attendees: | |
att = Element('t:Attendee') | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(attendee) | |
mailbox.append(emailaddress) | |
att.append(mailbox) | |
reqatts.append(att) | |
itemfield.children[1].append(reqatts) | |
updates.append(itemfield) | |
if category: | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', 'item:Categories') | |
itemfield.append(Element('t:CalendarItem')) | |
categories = Element('t:Categories') | |
if not isinstance(category, list): | |
categories.append(Element('t:String').setText(category)) | |
else: | |
for cat in category: | |
categories.append(Element('t:String').setText(cat)) | |
itemfield.children[1].append(categories) | |
updates.append(itemfield) | |
if body: | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', 'item:Body') | |
itemfield.append(Element('t:CalendarItem')) | |
Body = Element('t:Body') | |
Body.set('BodyType', getattr(self.types.EWSType_BodyTypeResponseType, bodytype)) | |
Body.setText(body) | |
itemfield.children[1].append(Body) | |
updates.append(itemfield) | |
#timezone | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', 'calendar:MeetingTimeZone') | |
itemfield.append(Element('t:CalendarItem')) | |
timeZone = Element('t:MeetingTimeZone') | |
timeZone.set('TimeZoneName', TimeZoneName) | |
timeZone.append(Element('t:BaseOffset').setText(BaseOffset)) | |
standard = Element('t:Standard') | |
standard.append(Element('t:Offset').setText(StandardOffset)) | |
#ryr = Element(t:'RelativeYearlyRecurrence') | |
#ryr.append(t:'DaysOfWeek').setText('DaysOfWeek') | |
#ryr.append(t:'DayOfWeekIndex').setText('First') | |
#ryr.append(t:'Month').setText('November') | |
#standard.append(ryr) | |
standard.append(Element('t:Time').setText(StandardTime)) | |
timeZone.append(standard) | |
daylight = Element('t:Daylight') | |
daylight.append(Element('t:Offset').setText(Offset)) | |
daylight.append(Element('t:Time').setText(DaylightTime)) | |
timeZone.append(daylight) | |
itemfield.children[1].append(timeZone) | |
updates.append(itemfield) | |
itemchange.append(updates) | |
itemchanges.append(itemchange) | |
updateitem.append(itemchanges) | |
#make the call | |
xml = self.wrap(updateitem) | |
try: | |
result = self.client.service.UpdateItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
# Result may come as a single object or a list of objects | |
if type(result.UpdateItemResponseMessage).__name__ == 'list': | |
msgs = result.UpdateItemResponseMessage | |
else: | |
msgs = [result.UpdateItemResponseMessage] | |
idlist = [] | |
for msg in msgs: | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
item = msg.Items.CalendarItem.ItemId | |
idlist.append((item._Id, item._ChangeKey)) | |
return idlist | |
def deleteCalendarEvent(self, ids): | |
'''====================================== | |
// Delete Calendar Event Items | |
//====================================== | |
/* @param array $ids - array of event ids to delete | |
* | |
* @return object response | |
*/ | |
''' | |
return self.deleteItems(ids) | |
def addTask(self, subject, on_behalf, due, body=None, reminderdue=None, reminderStart="30",\ | |
importance="NORMAL", sensitivity="NORMAL", bodyType="TEXT", category="default"): | |
'''====================================== | |
// Add Task | |
//====================================== | |
/* @param string $subject - task subject | |
* @param string $body - task body | |
* @param string $onbehalf - "on behalf" seneder's email | |
* @param int $due - task due date timestamp | |
* @param int $reminderdue - reminder due date timestamp | |
* @param int $reminderStart - realtive negative offset for reminder start in nimutes | |
* @param string $importance - task importance | |
* @param string $sensitivity - task sensitivity | |
* @param string $bodytype - task body type (TEXT/HTML) | |
* @param string $category - task category | |
* | |
* @return object response | |
*/ | |
''' | |
createitem = Element('m:CreateItem') | |
saveditemfolderid = Element('m:SavedItemFolderId') | |
distinguishedfolderid = Element('t:DistinguishedFolderId') | |
distinguishedfolderid.set('Id', self.types.EWSType_DistinguishedFolderIdNameType.TASKS) | |
if on_behalf is not None: | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(on_behalf) | |
mailbox.append(emailaddress) | |
distinguishedfolderid.append(mailbox) | |
saveditemfolderid.append(distinguishedfolderid) | |
createitem.append(saveditemfolderid) | |
tasks = Element('m:Items') | |
# Prepare objects for Task creation | |
task = Element('t:Task') | |
Subject = Element('t:Subject') | |
Sensitivity = Element('t:Sensitivity') | |
cats = None | |
if category: | |
cats = Element('t:Categories') | |
cats.append(Element('t:String').setText(category)) | |
Importance = Element('t:Importance') | |
ReminderDueBy = Element('t:ReminderDueBy') | |
ReminderMinutesBeforeStart = Element('t:ReminderMinutesBeforeStart') | |
ReminderIsSet = Element('t:ReminderIsSet') | |
DueDate = Element('t:DueDate') | |
task.append(Subject.setText(subject)) | |
task.append(Sensitivity.setText(getattr(self.types.EWSType_SensitivityChoicesType, sensitivity))) | |
if body: | |
Body = Element('t:Body') | |
Body.set('BodyType', getattr(self.types.EWSType_BodyTypeResponseType, bodyType)) | |
task.append(Body.setText(body)) | |
task.append(cats) | |
task.append(Importance.setText(getattr(self.types.EWSType_ImportanceChoicesType,importance))) | |
if(reminderdue): | |
task.append(ReminderDueBy.setText(reminderdue.ewsformat())) | |
task.append(ReminderIsSet.setText(1)) | |
task.append(ReminderMinutesBeforeStart.setText(reminderStart)) | |
task.append(DueDate.setText(due.ewsformat())) | |
#add item to items | |
tasks.append(task) | |
#add item to CreateItems | |
createitem.append(tasks) | |
#make the call | |
xml = self.wrap(createitem) | |
try: | |
result = self.client.service.CreateItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
# Result may come as a single object or a list of objects | |
if type(result.CreateItemResponseMessage).__name__ == 'list': | |
msgs = result.CreateItemResponseMessage | |
else: | |
msgs = [result.CreateItemResponseMessage] | |
idlist = [] | |
for msg in msgs: | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
item = msg.Items.Task.ItemId | |
idlist.append((item._Id, item._ChangeKey)) | |
return idlist | |
def editTask(self, id, ckey, subject=None, body=None, bodytype='TEXT', due=None, \ | |
reminderdue=None, reminderStart=None, status=None, percentComplete=None, \ | |
sensitivity=None, importance=None, category=None): | |
'''====================================== | |
// Edit Task | |
//====================================== | |
/* @param string $id - event id | |
* @param string $ckey - event change key | |
* @param string $subject - event subject | |
* @param string $body - task body | |
* @param string $bodytype - task body type (TEXT/HTML) | |
* @param int $due - task due date timestamp | |
* @param int $reminderdue - reminder due date timestamp | |
* @param int $reminderStart - realtive negative offset for reminder start in nimutes | |
* @param string $status - task status (enumarted in TaskStatusType) | |
* @param int $percentComplete - task complitionprocentage | |
* @param string $sensitivity - task sensitivity (enumarted in SensitivityChoicesType) | |
* @param string $importance - task importance (enumarted in ImportanceChoicesType) | |
* @param string $category - task category | |
* | |
* @return object response | |
*/ | |
''' | |
Updates = { | |
'task:DueDate' : due.ewsformat() if due else None, | |
'item:ReminderDueBy' : reminderdue.ewsformat() if reminderdue else None, | |
'item:ReminderMinutesBeforeStart' : reminderStart, | |
'item:ReminderIsSet' : 1 if (reminderStart or reminderdue) else 0, | |
'item:Subject' : subject, | |
'task:Status' : getattr(self.types.EWSType_TaskStatusType, status) if status else None, | |
'item:Sensitivity' : getattr(self.types.EWSType_SensitivityChoicesType, sensitivity) if sensitivity else None, | |
'item:Importance' : getattr(self.types.EWSType_ImportanceChoicesType, importance) if importance else None, | |
'task:PercentComplete' : percentComplete | |
} | |
updateitem = Element('m:UpdateItem') | |
updateitem.set('SendMeetingInvitationsOrCancellations', self.types.EWSType_CalendarItemCreateOrDeleteOperationType.SEND_TO_ALL_AND_SAVE_COPY) | |
updateitem.set('MessageDisposition', self.types.EWSType_MessageDispositionType.SAVEONLY) | |
updateitem.set('ConflictResolution', self.types.EWSType_ConflictResolutionType.ALWAYSOVERWRITE) | |
itemchanges = Element('m:ItemChanges') | |
itemchange = Element('t:ItemChange') | |
itemid = Element('t:ItemId') | |
itemid.set('Id', id) | |
itemid.set('ChangeKey', ckey) | |
itemchange.append(itemid) | |
updates = Element('t:Updates') | |
#popoulate update array | |
for key,value in Updates.items(): | |
if value: | |
prop = key.split(':').pop() | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', key) | |
itemfield.append(Element('t:Task')) | |
itemfield.children[1].append(Element('t:'+prop).setText(value)) | |
updates.append(itemfield) | |
if category: | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', 'item:Categories') | |
itemfield.append(Element('t:Task')) | |
categories = Element('t:Categories') | |
categories.append(Element('t:String').setText(category)) | |
itemfield.children[1].append(categories) | |
updates.append(itemfield) | |
if body: | |
itemfield = Element('t:SetItemField') | |
itemfield.append(Element('t:FieldURI')) | |
itemfield.children[0].set('FieldURI', 'item:Body') | |
itemfield.append(Element('t:Task')) | |
Body = Element('t:Body') | |
Body.set('BodyType', getattr(self.types.EWSType_BodyTypeResponseType, bodytype)) | |
Body.setText(body) | |
itemfield.children[1].append(Body) | |
updates.append(itemfield) | |
itemchange.append(updates) | |
itemchanges.append(itemchange) | |
updateitem.append(itemchanges) | |
#make the call | |
xml = self.wrap(updateitem) | |
try: | |
result = self.client.service.UpdateItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
# Result may come as a single object or a list of objects | |
if type(result.UpdateItemResponseMessage).__name__ == 'list': | |
msgs = result.UpdateItemResponseMessage | |
else: | |
msgs = [result.UpdateItemResponseMessage] | |
idlist = [] | |
for msg in msgs: | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
item = msg.Items.Task.ItemId | |
idlist.append((item._Id, item._ChangeKey)) | |
return idlist | |
def deleteTask(self, ids): | |
'''====================================== | |
// Delete Task Items | |
//====================================== | |
/* @param array $ids - array of taks ids to delete | |
* | |
* @return object response | |
*/ | |
''' | |
return self.deleteItems(ids) | |
def listTask(self, id=None, start=None, end=None, on_behalf=None, shape='DEFAULT_PROPERTIES', categories=None): | |
'''====================================== | |
// List Calendar Events | |
//====================================== | |
/* @param string id - item id. takes precendense over timeframe | |
* @param string $onbehalf - "on behalf" seneder's email | |
* @param int $start - event start timestamp | |
* @param int $end - event end time | |
* | |
* @return object response | |
*/ | |
''' | |
type = 'TASKS' | |
return self.listItems(type, id, start, end, on_behalf, shape, categories) | |
def deleteItems(self, ids): | |
'''====================================== | |
// Delete Items | |
//====================================== | |
/* @param array $ids - list of item ids to delete | |
* | |
* @return list of tuples (success[True|False], errormessage) | |
* | |
''' | |
status = [] | |
if not ids: | |
# Nothing to do | |
return status | |
deleteitem = Element('m:DeleteItem') | |
deleteitem.set('DeleteType', self.types.EWSType_DisposalType.MOVE_TO_DELETED_ITEMS) | |
deleteitem.set('SendMeetingCancellations', self.types.EWSType_CalendarItemCreateOrDeleteOperationType.SEND_TO_NONE) | |
deleteitem.set('AffectedTaskOccurrences', self.types.EWSType_AffectedTaskOccurrencesType.ALL_OCCURRENCES) | |
itemids = Element('m:ItemIds') | |
itemid = Element('t:ItemId') | |
# copy.deepcopy() is faster than having Element() inside the loop. | |
for iid in ids: | |
i = deepcopy(itemid) | |
i.set('Id', iid) | |
#i.set('ChangeKey', changekey) | |
itemids.append(i) | |
deleteitem.append(itemids) | |
xml = self.wrap(deleteitem) | |
try: | |
result = self.client.service.DeleteItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
# Result may come as a single object or a list of objects | |
if type(result.DeleteItemResponseMessage).__name__ == 'list': | |
msgs = result.DeleteItemResponseMessage | |
else: | |
msgs = [result.DeleteItemResponseMessage] | |
msglen = len(msgs) | |
if len(msgs) != len(ids): | |
raise Exception('Returned response count doesn\'t match: \ | |
got %s, expected %s' % (len(msgs), len(ids))) | |
for msg in msgs: | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
if rspcode == 'ErrorItemNotFound': | |
# Should not happen, so don't worry about performance | |
status.append( (False, '%s: %s' % ( msg.MessageText, \ | |
ids[len(status)] ) ) ) | |
continue | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if not rspcode == 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
status.append((True, None)) | |
return status | |
def listItems(self, type, id=None, start=None, end=None, on_behalf=None, shape='ID_ONLY', categories=[], additional=[]): | |
'''====================================== | |
// List Items | |
// Note: currenttly only Taska are | |
// searcheble by category | |
//====================================== | |
/* @param string type - item type | |
* @param string id - item id. takes precendense over timeframe | |
* @param string $onbehalf - "on behalf": item owner email | |
* @param int $start - search start timestamp | |
* @param int $end - search end timestamp | |
* | |
* @return object response | |
*/ | |
''' | |
if(id): | |
getitem = Element('m:GetItem') | |
itemshape = Element('m:ItemShape') | |
baseshape = Element('t:BaseShape').setText(getattr(self.types.EWSType_DefaultShapeNamesType, shape)) | |
itemshape.append(baseshape) | |
if additional: | |
additionalproperties = Element('t:AdditionalProperties') | |
for URI in additional: | |
fielduri = Element('t:FieldURI') | |
fielduri.set('FieldURI', URI) | |
additionalproperties.append(fielduri) | |
itemshape.append(additionalproperties) | |
getitem.append(itemshape) | |
itemids = Element('m:ItemIds') | |
if isinstance(id, list): | |
for single in id: | |
itemid = Element('t:ItemId') | |
itemid.set('Id', single) | |
itemids.append(itemid) | |
else: | |
itemid = Element('t:ItemId') | |
itemid.set('Id', id) | |
itemids.append(itemid) | |
getitem.append(itemids) | |
xml = self.wrap(getitem) | |
try: | |
result = self.client.service.GetItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
msg = result.GetItemResponseMessage | |
if isinstance(msg, list): | |
fullitems = [] | |
for el in msg: | |
rspclass = el._ResponseClass | |
rspcode = el.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
fullitems.extend([el.Items[0]]) | |
#print fullitems | |
if not categories: | |
return fullitems | |
# Filter for category. Searching for categories only works with | |
# 'Or' operator, so we need to ignore items with only some | |
# but not all categories present. | |
items = [] | |
for item in fullitems: | |
itemcats = item.Categories[0] | |
if set(categories).issubset(set(itemcats)): | |
items.append(item) | |
return items | |
else: | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
if isinstance(msg.Items[0], list): | |
fullitems = msg.RootFolder.Items[0] | |
else: | |
# Only one item returned | |
fullitems = [msg.Items].pop() | |
if not categories: | |
return fullitems | |
# Filter for category. Searching for categories only works with | |
# 'Or' operator, so we need to ignore items with only some | |
# but not all categories present. | |
items = [] | |
for item in fullitems: | |
itemcats = item.Categories[0] | |
if set(categories).issubset(set(itemcats)): | |
items.append(item) | |
return items | |
else: | |
finditem = Element('m:FindItem') | |
finditem.set('Traversal', self.types.EWSType_FolderQueryTraversalType.SHALLOW) | |
itemshape = Element('m:ItemShape') | |
baseshape = Element('t:BaseShape').setText(getattr(self.types.EWSType_DefaultShapeNamesType, shape)) | |
itemshape.append(baseshape) | |
additionalproperties = None | |
# additional properties | |
if categories: | |
additionalproperties = Element('t:AdditionalProperties') | |
fielduri = Element('t:FieldURI') | |
fielduri.set('FieldURI', 'item:Categories') | |
additionalproperties.append(fielduri) | |
#append all additional properties, if any | |
if additionalproperties: | |
itemshape.append(additionalproperties) | |
finditem.append(itemshape) | |
#type-sepcific options | |
if(type == 'CALENDAR'): | |
if start or end: | |
calendarview = Element('m:CalendarView') | |
if start: | |
calendarview.set('StartDate', start.ewsformat()) | |
if end: | |
calendarview.set('EndDate', end.ewsformat()) | |
finditem.append(calendarview) | |
elif(type == 'TASKS'): | |
# if start or end: | |
# res = Element('m:Restriction') | |
# restriction = Element('t:And') | |
# needed to replace the above with this - ok..? | |
res = Element('m:Restriction') | |
restriction = Element('t:And') | |
if not start: | |
start = EWSDateTime(0) | |
if not end: | |
end = EWSDateTime(2038, 1, 1, 0, 0, 0) | |
rcount = len([f for f in [start, end, categories] if f != None]) | |
if categories: | |
if not res: | |
restriction = Element('m:Restriction') | |
if len(categories) == 1: | |
restriction.append(self._restriction('contains', 'item:Categories', categories[0])) | |
else: | |
ortype = Element('t:Or') | |
for cat in categories: | |
ortype.append(self._restriction('contains', 'item:Categories', cat)) | |
restriction.append(ortype) | |
extended = self._mapi_reference('33029', 'SystemTime', 'Task') | |
gteq = self._restriction('>=', extended, start.ewsformat(), True) | |
restriction.append(gteq) | |
lseq = self._restriction('<=', extended, end.ewsformat(), True) | |
restriction.append(lseq) | |
if rcount > 1: | |
res.append(restriction) | |
finditem.append(res) | |
else: | |
finditem.append(restriction) | |
distinguishedfolderid = Element('t:DistinguishedFolderId') | |
distinguishedfolderid.set('Id', getattr(self.types.EWSType_DistinguishedFolderIdNameType, type)) | |
parentfolderids = Element('m:ParentFolderIds') | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(on_behalf) | |
mailbox.append(emailaddress) | |
distinguishedfolderid.append(mailbox) | |
parentfolderids.append(distinguishedfolderid) | |
finditem.append(parentfolderids) | |
xml = self.wrap(finditem) | |
try: | |
result = self.client.service.FindItem(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
msg = result.FindItemResponseMessage | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
if not msg.RootFolder._IncludesLastItemInRange: | |
raise Exception('Not all items were transferred') | |
if msg.RootFolder._TotalItemsInView == 0: | |
return [] | |
if isinstance(msg.RootFolder.Items[0], list): | |
fullitems = msg.RootFolder.Items[0] | |
else: | |
# Only one item returned | |
fullitems = msg.RootFolder.Items | |
#if we have addtional proerties to fetch do so | |
#get ids from response | |
ids = self.getids(fullitems, False) | |
#call self with ids and required props | |
if type=="CALENDAR": | |
additional.append("calendar:RequiredAttendees") | |
additional.append("calendar:IsAllDayEvent") | |
additional.append("calendar:Duration") | |
additional.append("item:Categories") | |
additional.append("item:Body") | |
additional.append("item:Sensitivity") | |
additional.append("item:Importance") | |
extended_items = self.listItems(type="CALENDAR", id=ids, additional=additional) | |
#print extended_items | |
#print ids | |
#exit() | |
extended_add, extended_fday, extended_cats, extended_body = ({} for i in range(4)) | |
extended_sen, extended_imp = ({} for i in range(2)) | |
for i in range(0, len(extended_items)): | |
if hasattr(extended_items[i], 'RequiredAttendees'): | |
extended_add[extended_items[i].ItemId._Id] = extended_items[i].RequiredAttendees | |
else: | |
extended_add[extended_items[i].ItemId._Id] = [] | |
extended_fday[extended_items[i].ItemId._Id] = extended_items[i].IsAllDayEvent | |
extended_cats[extended_items[i].ItemId._Id] = extended_items[i].Categories if hasattr(extended_items[i], "Categories") else None | |
extended_body[extended_items[i].ItemId._Id] = extended_items[i].Body if hasattr(extended_items[i], "Body") else None | |
extended_sen[extended_items[i].ItemId._Id] = extended_items[i].Sensitivity | |
extended_imp[extended_items[i].ItemId._Id] = extended_items[i].Importance | |
for i in range(0,len(fullitems)): | |
fullitems[i].RequiredAttendees = extended_add.get(fullitems[i].ItemId._Id) | |
fullitems[i].IsAllDayEvent = extended_fday.get(fullitems[i].ItemId._Id) | |
fullitems[i].Categories = extended_cats.get(fullitems[i].ItemId._Id) | |
fullitems[i].Body = extended_body.get(fullitems[i].ItemId._Id) | |
fullitems[i].Sensitivity = extended_sen.get(fullitems[i].ItemId._Id) | |
fullitems[i].Importance = extended_imp.get(fullitems[i].ItemId._Id) | |
if not categories: | |
return fullitems | |
# Filter for category. Searching for categories only works with | |
# 'Or' operator, so we need to ignore items with only some | |
# but not all categories present. | |
items = [] | |
for item in fullitems: | |
itemcats = item.Categories[0] | |
if set(categories).issubset(set(itemcats)): | |
items.append(item) | |
return items | |
def listFolders(self, type, on_behalf, shape='DEFAULT_PROPERTIES', depth='SHALLOW'): | |
'''====================================== | |
// List Folders | |
//====================================== | |
/* @param string type - folder type (enumarted in DistinguishedFolderIdNameType) | |
* @param string $onbehalf - "on behalf": item owner email | |
* @param string $shape - detail level (enumarted in DefaultShapeNamesType) | |
* @param string $depth - list normal /include subfolders (enumarted in FolderQueryTraversalType) | |
* | |
* @return object response | |
*/ | |
''' | |
# start building the find folder request | |
request = Element('m:FindFolder') | |
request.set('Traversal', getattr(self.types.EWSType_FolderQueryTraversalType, depth)) | |
itemshape = Element('m:FolderShape') | |
baseshape = Element('t:BaseShape').setText(getattr(self.types.EWSType_DefaultShapeNamesType,shape)) | |
itemshape.append(baseshape) | |
request.append(itemshape) | |
# configure the view | |
indexedpage = Element('m:IndexedPageFolderView') | |
indexedpage.set('BasePoint', 'Beginning') | |
indexedpage.set('Offset', '0') | |
request.append(indexedpage) | |
# set the starting folder as the inbox | |
distinguishedfolderid = Element('t:DistinguishedFolderId') | |
distinguishedfolderid.set('Id', getattr(self.types.EWSType_DistinguishedFolderIdNameType, type)) | |
parentfolderids = Element('m:ParentFolderIds') | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(on_behalf) | |
mailbox.append(emailaddress) | |
distinguishedfolderid.append(mailbox) | |
parentfolderids.append(distinguishedfolderid) | |
request.append(parentfolderids) | |
xml = self.wrap(request) | |
try: | |
result = self.client.service.FindFolder(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
msg = result.FindFolderResponseMessage | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
if not msg.RootFolder._IncludesLastItemInRange: | |
raise Exception('Not all items were transferred') | |
if msg.RootFolder._TotalItemsInView == 0: | |
return [] | |
if isinstance(msg.RootFolder.Folders[0], list): | |
fullitems = msg.RootFolder.Folders[0] | |
else: | |
# Only one item returned | |
fullitems = [msg.RootFolder.Folders].pop() | |
return fullitems | |
def getids(self, items, include_change_key=True): | |
'''Takes a list of items with full or partial properties as | |
produced from getitems() and returns a list of (id, changekey) | |
tuples.''' | |
idlist = [] | |
if len(items) > 1: | |
try: | |
for item in [i.ItemId for i in items]: | |
if(include_change_key): | |
idlist.append((item._Id, item._ChangeKey)) | |
else: | |
idlist.append(item._Id) | |
except AttributeError: | |
for item in [i.ItemId for i in [j[0] for j in items]]: | |
if(include_change_key): | |
idlist.append((item._Id, item._ChangeKey)) | |
else: | |
idlist.append(item._Id) | |
else: | |
try: | |
for item in [i.ItemId for i in [j[1] for j in items]]: | |
if(include_change_key): | |
idlist.append((item._Id, item._ChangeKey)) | |
else: | |
idlist.append(item._Id) | |
except (IndexError,AttributeError): | |
for item in [j.ItemId for j in items]: | |
if(include_change_key): | |
idlist.append((item._Id, item._ChangeKey)) | |
else: | |
idlist.append(item._Id) | |
return idlist | |
def synchFolder(self, type, on_behalf=None, num_to_synch=10, synch_state=None, shape='ID_ONLY', additional=[], ignored_items=None): | |
'''====================================== | |
// Synch Folder - synchronizes given folder | |
//====================================== | |
/* @param string type - folder type (enumarted in DistinguishedFolderIdNameType) | |
* @param string $onbehalf - "on behalf": item owner email | |
* @param int num_to_synch - how many items to synch in one go (max 500) | |
* @param string synch_state - current synch state, blank on initial synch | |
* @param string $shape - detail level (enumarted in DefaultShapeNamesType) | |
* @param string $ignored_items - list of IDs of ignored items (currently not implemented) | |
* | |
* @return object response | |
*/ | |
''' | |
synchfolder = Element('m:SyncFolderItems') | |
itemshape = Element('m:ItemShape') | |
baseshape = Element('t:BaseShape').setText(getattr(self.types.EWSType_DefaultShapeNamesType, shape)) | |
itemshape.append(baseshape) | |
synchfolder.append(itemshape) | |
distinguishedfolderid = Element('t:DistinguishedFolderId') | |
distinguishedfolderid.set('Id', getattr(self.types.EWSType_DistinguishedFolderIdNameType, type)) | |
parentfolderids = Element('m:SyncFolderId') | |
mailbox = Element('t:Mailbox') | |
emailaddress = Element('t:EmailAddress').setText(on_behalf) | |
mailbox.append(emailaddress) | |
distinguishedfolderid.append(mailbox) | |
parentfolderids.append(distinguishedfolderid) | |
synchfolder.append(parentfolderids) | |
if synch_state is not None: | |
synchstate = Element('m:SyncState').setText(synch_state) | |
synchfolder.append(synchstate) | |
if ignored_items: | |
#@TODO : add ignored items... | |
pass | |
numresults = Element('m:MaxChangesReturned').setText(num_to_synch) | |
synchfolder.append(numresults) | |
xml = self.wrap(synchfolder) | |
try: | |
result = self.client.service.SyncFolderItems(__inject={'msg':xml}) | |
except WebFault as e: | |
raise e | |
msg = result.SyncFolderItemsResponseMessage | |
rspclass = msg._ResponseClass | |
rspcode = msg.ResponseCode | |
if rspclass == 'Error': | |
raise Exception('Error code: %s message: %s' % \ | |
(rspcode, msg.MessageText)) | |
if rspclass != 'Success': | |
raise Exception('Unknown response class: %s code: %s' % \ | |
(rspclass, rspcode)) | |
if rspcode != 'NoError': | |
raise Exception('Unknown response code: %s' % rspcode) | |
if not msg.IncludesLastItemInRange: | |
self.hasMoreItems = True | |
else: | |
self.hasMoreItems = False | |
if msg.SyncState: | |
self.synchState = msg.SyncState | |
#if isinstance(msg.Changes[0], list): | |
# fullitems = msg.Changes[0] | |
#else: | |
# Only one item returned | |
changes = msg.Changes | |
fullitems = [] | |
__fullitems = [] | |
ids = [] | |
fullitems_add = [] | |
fullitems_update = [] | |
#add/update events | |
if hasattr(changes, 'Create'): | |
if isinstance(changes.Create[0], list): | |
__fullitems.extend(changes.Create[0]) | |
else: | |
__fullitems.extend(changes.Create) | |
#fix attributes | |
if len(__fullitems) > 1: | |
for elem in __fullitems: | |
fullitems.append(elem[0]) | |
else: | |
for elem in __fullitems: | |
fullitems.append(elem[1]) | |
#get ids from response | |
ids.extend(self.getids(fullitems, False)) | |
fullitems_add = fullitems | |
fullitems = [] | |
__fullitems = [] | |
if hasattr(changes, 'Update'): | |
if isinstance(changes.Update[0], list): | |
__fullitems.extend(changes.Update[0]) | |
else: | |
__fullitems.extend(changes.Update) | |
#fix attributes | |
if len(__fullitems) > 1: | |
for elem in __fullitems: | |
fullitems.append(elem[0]) | |
else: | |
for elem in __fullitems: | |
fullitems.append(elem[1]) | |
#get ids from response | |
ids.extend(self.getids(fullitems, False)) | |
fullitems_update = fullitems | |
fullitems = [] | |
__fullitems = [] | |
#if we have addtional proerties to fetch do so | |
#join fulltimes toghether | |
fullitems = fullitems_add + fullitems_update | |
#call self with ids and required props | |
if type=="CALENDAR": | |
if ids: | |
additional.append("calendar:RequiredAttendees") | |
additional.append("calendar:IsAllDayEvent") | |
additional.append("calendar:Duration") | |
additional.append("item:Categories") | |
additional.append("item:Body") | |
additional.append("item:Sensitivity") | |
additional.append("item:Importance") | |
extended_items = self.listItems(type="CALENDAR", id=ids, additional=additional) | |
#print extended_items | |
#print ids | |
#exit() | |
extended_add, extended_fday, extended_cats, extended_body = ({} for i in range(4)) | |
extended_sen, extended_imp = ({} for i in range(2)) | |
for i in range(0, len(extended_items)): | |
if hasattr(extended_items[i], 'RequiredAttendees'): | |
extended_add[extended_items[i].ItemId._Id] = extended_items[i].RequiredAttendees | |
else: | |
extended_add[extended_items[i].ItemId._Id] = [] | |
extended_fday[extended_items[i].ItemId._Id] = extended_items[i].IsAllDayEvent | |
extended_cats[extended_items[i].ItemId._Id] = extended_items[i].Categories if hasattr(extended_items[i], "Categories") else None | |
extended_body[extended_items[i].ItemId._Id] = extended_items[i].Body if hasattr(extended_items[i], "Body") else None | |
extended_sen[extended_items[i].ItemId._Id] = extended_items[i].Sensitivity | |
extended_imp[extended_items[i].ItemId._Id] = extended_items[i].Importance | |
try: | |
for i in range(0,len(fullitems)): | |
fullitems[i].RequiredAttendees = extended_add.get(fullitems[i].ItemId._Id) | |
fullitems[i].IsAllDayEvent = extended_fday.get(fullitems[i].ItemId._Id) | |
fullitems[i].Categories = extended_cats.get(fullitems[i].ItemId._Id) | |
fullitems[i].Body = extended_body.get(fullitems[i].ItemId._Id) | |
fullitems[i].Sensitivity = extended_sen.get(fullitems[i].ItemId._Id) | |
fullitems[i].Importance = extended_imp.get(fullitems[i].ItemId._Id) | |
except AttributeError: | |
fullitems.RequiredAttendees = extended_add.get(fullitems.ItemId._Id) | |
fullitems.IsAllDayEvent = extended_fday.get(fullitems.ItemId._Id) | |
fullitems.Categories = extended_cats.get(fullitems.ItemId._Id) | |
fullitems.Body = extended_body.get(fullitems.ItemId._Id) | |
fullitems.Sensitivity = extended_sen.get(fullitems.ItemId._Id) | |
fullitems.Importance = extended_imp.get(fullitems.ItemId._Id) | |
if(len(fullitems) < 2): | |
fullitems = [("EventSpoofedType",fullitems.pop())] | |
#allitems = type('', (object,), {}) | |
allitems = lambda:0 | |
allitems.addupdate = fullitems | |
if hasattr(changes, 'Delete'): | |
if len(changes.Delete) < 2: | |
allitems.delete = [changes.Delete] | |
else: | |
allitems.delete = changes.Delete | |
return allitems | |
class EWSDateTime(datetime): | |
'''Extends the normal datetime implementation to satisfy Exchange''' | |
def ewsformat(self): | |
'''ISO 8601 format to satisfy xs:datetime as interpreted by Exchange''' | |
return self.strftime('%Y-%m-%dT%H:%M:%S') | |
def midnightfix(self): | |
return self - timedelta(seconds=1) | |
def __new__(cls, y=None, m=None, d=None, h=None, mi=None, s=None): | |
if((y>=0) and (all(x is None for x in (m,d,h,mi,s)))): | |
t = datetime.fromtimestamp(y) | |
return super(EWSDateTime, cls).__new__(cls,t.year,t.month,t.day,t.hour,t.minute,t.second) | |
else: | |
return super(EWSDateTime, cls).__new__(cls,y,m,d,h,mi,s) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
/* EWSWrapper_py | |
* ==================================================== | |
* @author Michal Korzeniowski <mko_san@lafiel.net> | |
* @version 0.1 | |
* @date 10-2011 | |
* @website http://ewswrapper.lafiel.net/ | |
* ==================================================== | |
* Desciption | |
* Provides enumerable types from Exchange Web Services | |
* for EWSWrapper. Accessible from EWSWrapper under | |
* self.types | |
* | |
* ==================================================*/ | |
''' | |
class EWSType: | |
class EWSType_AffectedTaskOccurrencesType: | |
## | |
# Specifies that a DeleteItem request deletes the master task, and therefore all recurring tasks that are associated with the master task. | |
# | |
# @var string | |
# | |
ALL_OCCURRENCES='AllOccurrences' | |
## | |
# Specifies that a DeleteItem request deletes only the current occurrence of a task. | |
# | |
# @var string | |
# | |
SPECIFIED_OCCURRENCES_ONLY='SpecifiedOccurrenceOnly' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class CalendarItemCreateOrDeleteOperationType | |
class EWSType_BodyTypeResponseType: | |
## | |
# All properties are retured in the response | |
# | |
# @var string | |
# | |
BEST='Best' | |
## | |
# Default properties are returned in the respoonse | |
# | |
# @var string | |
# | |
HTML='HTML' | |
## | |
# Plain text body | |
# | |
# @var string | |
# | |
TEXT='Text' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_BodyTypeResponseType | |
class EWSType_CalendarItemCreateOrDeleteOperationType: | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_TO_NONE='SendToNone' | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_ONLY_TO_ALL='SendOnlyToAll' | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_TO_ALL_AND_SAVE_COPY='SendToAllAndSaveCopy' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class CalendarItemCreateOrDeleteOperationType | |
class EWSType_CalendarItemUpdateOperationType: | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_TO_NONE='SendToNone' | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_ONLY_TO_ALL='SendOnlyToAll' | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_TO_ALL_AND_SAVE_COPY='SendToAllAndSaveCopy' | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_ONLY_TO_CHANGED='SendOnlyToChanged' | |
## | |
# Send to | |
# | |
# @var string | |
# | |
SEND_TO_CHANGED_AND_SAVE_COPY='SendToChangedAndSaveCopy' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class CalendarItemUpdateOperationType | |
class EWSType_DefaultShapeNamesType: | |
## | |
# All properties are retured in the response | |
# | |
# @var string | |
# | |
ALL_PROPERTIES='AllProperties' | |
## | |
# Default properties are returned in the respoonse | |
# | |
# @var string | |
# | |
DEFAULT_PROPERTIES='Default' | |
## | |
# Only folder ids are returned in the response | |
# | |
# @var string | |
# | |
ID_ONLY='IdOnly' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_DefaultShapeNamesType | |
class EWSType_DistinguishedFolderIdNameType: | |
## | |
# Calendar folder | |
# | |
# @var string | |
# | |
CALENDAR='calendar' | |
## | |
# Contacts folder | |
# | |
# @var string | |
# | |
CONTACTS='contacts' | |
## | |
# Deleted Items folder (ie. trash) | |
# | |
# @var string | |
# | |
DELETED_ITEMS='deleteditems' | |
## | |
# Drafts folder | |
# | |
# @var string | |
# | |
DRAFTS='drafts' | |
## | |
# Inbox folder | |
# | |
# @var string | |
# | |
INBOX='inbox' | |
## | |
# Journal folder | |
# | |
# @var string | |
# | |
JOURNAL='journal' | |
## | |
# Notes folder | |
# | |
# @var string | |
# | |
NOTES='notes' | |
## | |
# Outbox folder | |
# | |
# @var string | |
# | |
OUTBOX='outbox' | |
## | |
# Sent Items folder | |
# | |
# @var string | |
# | |
SENT_ITEMS='sentitems' | |
## | |
# Tasks folder | |
# | |
# @var string | |
# | |
TASKS='tasks' | |
## | |
# Root of the message folders | |
# | |
# @var string | |
# | |
MESSAGE_FOLDER_ROOT='msgfolderroot' | |
## | |
# Root of the folders='' | |
# | |
# @var string | |
# | |
PUBLIC_FOLDERS_ROOT='publicfoldersroot' | |
## | |
# Root of the user's mailbox | |
# | |
# @var string | |
# | |
ROOT='root' | |
## | |
# Junk Email folder | |
# | |
# @var string | |
# | |
JUNK_EMAIL='junkemail' | |
## | |
# Search folders | |
# | |
# @var string | |
# | |
SEARCH_FOLDERS='searchfolders' | |
## | |
# Voicemail folder | |
# | |
# @var string | |
# | |
VOICEMAIL='voicemail' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_DistinguishedFolderIdNameType | |
class EWSType_DisposalType: | |
## | |
# Deletes the item irrevocably. Does not move the item to the Deleted Items | |
# Folder. | |
# | |
# @var string | |
# | |
HARD_DELETE='HardDelete' | |
## | |
# Does not actually delete the item, but instead simply moves it to the | |
# Deleted Items folder. | |
# | |
# @var string | |
# | |
MOVE_TO_DELETED_ITEMS='MoveToDeletedItems' | |
## | |
# "Deletes" the item so that it is no longer visible in the folder, but | |
# actually still exists there. Avoid using this because there is nothing | |
# that you can do with soft-deleted items from EWS aside from finding them. | |
# | |
# @var string | |
# | |
SOFT_DELETE='SoftDelete' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_DisposalType | |
class EWSType_IndexBasePointType: | |
## | |
# Specifies that a DeleteItem request deletes the master task, and therefore all recurring tasks that are associated with the master task. | |
# | |
# @var string | |
# | |
BEGINNING='Beginning' | |
## | |
# Specifies that a DeleteItem request deletes only the current occurrence of a task. | |
# | |
# @var string | |
# | |
END='End' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class CalendarItemCreateOrDeleteOperationType | |
class EWSType_ItemQueryTraversalType: | |
## | |
# Consider only folders that are direct children of the parent folder(s) in | |
# question | |
# | |
# @var string | |
# | |
SHALLOW='Shallow' | |
## | |
# Consider only those items that are soft deleted from the parent folders | |
# specified | |
# | |
# @var string | |
# | |
SOFT_DELETED='SoftDeleted' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_ItemQueryTraversalType | |
class EWSType_FolderQueryTraversalType: | |
## | |
# Consider both direct children as well as all subfolders contained within | |
# those children as well as the children's children, etc. | |
# | |
# @var string | |
# | |
DEEP='Deep' | |
## | |
# Consider only folders that are direct children of the parent folder(s) in | |
# question | |
# | |
# @var string | |
# | |
SHALLOW='Shallow' | |
## | |
# Consider only those items that are soft deleted from the parent folders | |
# specified | |
# | |
# @var string | |
# | |
SOFT_DELETED='SoftDeleted' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_FolderQueryTraversalType | |
class EWSType_FileAsMappingType: | |
## | |
# File as mapping for "company" | |
# | |
# @var string | |
# | |
COMPANY='Company' | |
## | |
# File as mapping for "last name, first name" | |
# | |
# @var | |
# | |
COMPANY_LAST_COMMA_FIRST='CompanyLastCommaFirst' | |
## | |
# File as mapping for "company last name first name" | |
# | |
# @var string | |
# | |
COMPANY_LAST_FIRST='CompanyLastFirst' | |
## | |
# File as mapping for "company last name first name" | |
# | |
# @var string | |
# | |
COMPANY_LAST_SPACE_FIRST='CompanyLastSpaceFirst' | |
## | |
# File as mapping for "first name last name" | |
# | |
# @var string | |
# | |
FIRST_SPACE_LAST='FirstSpaceLast' | |
## | |
# File as mapping for "last name first name" | |
# | |
# @var string | |
# | |
LAST_FIRST='LastFirst' | |
## | |
# File as mapping for "last name first name company" | |
# | |
# @var string | |
# | |
LAST_FIRST_COMPANY='LastFirstCompany' | |
## | |
# File as mapping for "last name first name suffix" | |
# | |
# @var string | |
# | |
LAST_FIRST_SUFFIX='LastFirstSuffix' | |
## | |
# File as mapping for "last name, first name" | |
# | |
# @var string | |
# | |
LAST_COMMA_FIRST='LastCommaFirst' | |
## | |
# File as mapping for "last name, first name company" | |
# | |
# @var string | |
# | |
LAST_COMMA_FIRST_COMPANY='LastCommaFirstCompany' | |
## | |
# File as mapping for "last name first name" | |
# | |
# @var string | |
# | |
LAST_SPACE_FIRST='LastSpaceFirst' | |
## | |
# File as mapping for "lasy name first name company" | |
# | |
# @var string | |
# | |
LAST_SPACE_FIRST_COMPANY='LastSpaceFirstCompany' | |
## | |
# File as mapping to use when no mapping is desired. | |
# | |
# @var string | |
# | |
NONE='None' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_FileAsMappingType | |
class EWSType_EmailAddressKeyType: | |
## | |
# Key for a contacts first email address | |
# | |
# @var string | |
# | |
EMAIL_ADDRESS_1='EmailAddress1' | |
## | |
# Key for a contacts second email address | |
# | |
# @var string | |
# | |
EMAIL_ADDRESS_2='EmailAddress2' | |
## | |
# Key for a contacts third email address | |
# | |
# @var string | |
# | |
EMAIL_ADDRESS_3='EmailAddress3' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_EmailAddressKeyType | |
class EWSType_PhysicalAddressKeyType: | |
## | |
# Business physical address type | |
# | |
# @var string | |
# | |
BUSINESS='Business' | |
## | |
# Home physical address type | |
# | |
# @var string | |
# | |
HOME='Home' | |
## | |
# Other physical address type | |
# | |
# @var string | |
# | |
OTHER='Other' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_PhysicalAddressKeyType | |
class EWSType_PhoneNumberKeyType: | |
## | |
# Phone number key for assistant phone number | |
# | |
# @var string | |
# | |
ASSISTANT_PHONE='AssistantPhone' | |
## | |
# Phone number key for business fax number | |
# | |
# @var string | |
# | |
BUSINESS_FAX='BusinessFax' | |
## | |
# Phone number key for business phone number | |
# | |
# @var string | |
# | |
BUSINESS_PHONE='BusinessPhone' | |
## | |
# Phone number key for second business phone number | |
# | |
# @var string | |
# | |
BUSINESS_PHONE_2='BusinessPhone2' | |
## | |
# Phone number key for callback | |
# | |
# @var string | |
# | |
CALLBACK='Callback' | |
## | |
# Phone number key for car phone | |
# | |
# @var string | |
# | |
CAR_PHONE='CarPhone' | |
## | |
# Phone number key for company main phone | |
# | |
# @var string | |
# | |
COMPANY_MAIN_PHONE='CompanyMainPhone' | |
## | |
# Phone number key for home fax number | |
# | |
# @var string | |
# | |
HOME_FAX='HomeFax' | |
## | |
# Phone number key for home phone number | |
# | |
# @var string | |
# | |
HOME_PHONE='HomePhone' | |
## | |
# Phone number key for second home phone number | |
# | |
# @var string | |
# | |
HOME_PHONE_2='HomePhone2' | |
## | |
# Phone number key for ISDN line | |
# | |
# @var string | |
# | |
ISDN='Isdn' | |
## | |
# Phone number key for mobile phone number | |
# | |
# @var string | |
# | |
MOBILE_PHONE='MobilePhone' | |
## | |
# Phone number key for other fax number | |
# | |
# @var string | |
# | |
OTHER_FAX='OtherFax' | |
## | |
# Phone number key for other phone number | |
# | |
# @var string | |
# | |
OTHER_PHONE='OtherTelephone' | |
## | |
# Phone number key for pager | |
# | |
# @var string | |
# | |
PAGER='Pager' | |
## | |
# Phone number key for primary phone number | |
# | |
# @var string | |
# | |
PRIMARY_PHONE='PrimaryPhone' | |
## | |
# Phone number key for radio phone number | |
# | |
# @var string | |
# | |
RADIO_PHONE='RadioPhone' | |
## | |
# Phone number key for telex | |
# | |
# @var string | |
# | |
TELEX='Telex' | |
## | |
# Phone number key for TTY TTD phone number | |
# | |
# @var string | |
# | |
TTY_TTD_PHONE='TtyTtdPhone' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_PhoneNumberKeyType | |
class EWSType_ImportanceChoicesType: | |
## | |
# Specifies low priority | |
# | |
# @var string | |
# | |
LOW='Low' | |
## | |
# Specifies normal priority | |
# | |
# @var string | |
# | |
NORMAL='Normal' | |
## | |
# Specifies high priority | |
# | |
# @var string | |
# | |
HIGH='High' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_ImportanceChoicesType | |
class EWSType_TaskStatusType: | |
## | |
# Specifies that the task is completed. | |
# | |
# @var string | |
# | |
COMPLETED='Completed' | |
## | |
# Specifies that the task is deferred. | |
# | |
# @var string | |
# | |
DEFERRED='Deferred' | |
## | |
# Specifies that the task is in progress. | |
# | |
# @var string | |
# | |
INPROGRESS='InProgress' | |
## | |
# Specifies that the task is in progress. | |
# | |
# @var string | |
# | |
NOTSTARTED='NotStarted' | |
## | |
# Specifies that the task is in progress. | |
# | |
# @var string | |
# | |
WAITINGONOTHERS='WaitingOnOthers' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_TaskStatusType | |
class EWSType_SortDirectionType: | |
## | |
# Items are sorted in ascending order | |
# | |
# @var string | |
# | |
ASCENDING='Ascending' | |
## | |
# Items are sorted in descending order | |
# | |
# @var string | |
# | |
DESCENDING='Descending' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_SortDirectionType | |
class EWSType_SensitivityChoicesType: | |
## | |
# Specifies normal confidentiality. | |
# | |
# @var string | |
# | |
NORMAL='Normal' | |
## | |
# Specifies personal confidentiality. | |
# | |
# @var string | |
# | |
PERSONAL='Personal' | |
## | |
# Specifies confidentiality.='' | |
# | |
# @var string | |
# | |
PRIVATESENSITIVITY='Private' | |
## | |
# Specifies confidential confidentiality. | |
# | |
# @var string | |
# | |
CONFIDENTIAL='Confidential' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_SensitivityChoicesType | |
class EWSType_MessageDispositionType: | |
## | |
# When used in the CreateItemType, the e-mail message item is saved in the folder that is specified by the SavedItemFolderId property, or in the Sent Items folder if SavedItemFolderId is not specified. | |
# | |
# @var string | |
# | |
SAVEONLY='SaveOnly' | |
## | |
# When used in the CreateItemType, the e-mail message item is sent and a copy is saved in the folder that is specified by the SavedItemFolderId property, or in the Sent Items folder if SavedItemFolderId is not specified. | |
# | |
# @var string | |
# | |
SENDANDSAVECOPY='SendAndSaveCopy' | |
## | |
#When used in the CreateItemType, the e-mail message item is sent but no copy is saved. | |
# | |
# @var string | |
# | |
SENDONLY='SendOnly' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_MessageDispositionType | |
class EWSType_ConflictResolutionType: | |
## | |
# If there is a conflict, the UpdateItem operation will overwrite information. | |
# | |
# @var string | |
# | |
ALWAYSOVERWRITE='AlwaysOverwrite' | |
## | |
# The UpdateItem operation automatically resolves any conflict. The AutoResolve option will in most cases overwrite the existing value for a property. In some cases, the new value is ignored and the original value is retained. For example, user A changes the Sensitivity property from Normal to Confidential. Then user B sets the value to Public. In this example, the Confidential setting is retained and user B's update is ignored. | |
# | |
# @var string | |
# | |
AUTORESOLVE='AutoResolve' | |
## | |
# If conflict exists, the UpdateItem operation fails and an error is returned. | |
# @var string | |
# | |
NEVEROVERWRITE='NeverOverwrite' | |
## | |
# Constructor | |
# | |
def __init__() : | |
pass | |
# end class EWSType_MessageDispositionType | |
## | |
# Definition of the CalendarItemType type | |
class EWSType_CalendarItemType: | |
def __init__(self, subject, start, end, body=None, location=None): | |
## | |
# UID property | |
# | |
# @var string | |
# | |
self.UID='' | |
## | |
# RecurrenceId property | |
# | |
# @var EWSType_dateTime | |
# | |
self.RecurrenceId='' | |
## | |
# DateTimeStamp property | |
# | |
# @var EWSType_dateTime | |
# | |
self.DateTimeStamp='' | |
## | |
# Start property | |
# | |
# @var EWSType_dateTime | |
# | |
self.Start='' | |
## | |
# End property | |
# | |
# @var EWSType_dateTime | |
# | |
self.End='' | |
## | |
# OriginalStart property | |
# | |
# @var EWSType_dateTime | |
# | |
self.OriginalStart='' | |
## | |
# IsAllDayEvent property | |
# | |
# @var EWSType_boolean | |
# | |
self.IsAllDayEvent='' | |
## | |
# LegacyFreeBusyStatus property | |
# | |
# @var EWSType_LegacyFreeBusyType | |
# | |
self.LegacyFreeBusyStatus='' | |
## | |
# Location property | |
# | |
# @var string | |
# | |
self.Location='' | |
## | |
# When property | |
# | |
# @var string | |
# | |
self.When='' | |
## | |
# IsMeeting property | |
# | |
# @var EWSType_boolean | |
# | |
self.IsMeeting='' | |
## | |
# IsCancelled property | |
# | |
# @var EWSType_boolean | |
# | |
self.IsCancelled='' | |
## | |
# IsRecurring property | |
# | |
# @var EWSType_boolean | |
# | |
self.IsRecurring='' | |
## | |
# MeetingRequestWasSent property | |
# | |
# @var EWSType_boolean | |
# | |
self.MeetingRequestWasSent='' | |
## | |
# IsResponseRequested property | |
# | |
# @var EWSType_boolean | |
# | |
self.IsResponseRequested='' | |
## | |
# CalendarItemType property | |
# | |
# @var EWSType_CalendarItemTypeType | |
# | |
self.CalendarItemType='' | |
## | |
# MyResponseType property | |
# | |
# @var EWSType_ResponseTypeType | |
# | |
self.MyResponseType='' | |
## | |
# Organizer property | |
# | |
# @var EWSType_SingleRecipientType | |
# | |
self.Organizer='' | |
## | |
# RequiredAttendees property | |
# | |
# @var EWSType_NonEmptyArrayOfAttendeesType | |
# | |
self.RequiredAttendees='' | |
## | |
# OptionalAttendees property | |
# | |
# @var EWSType_NonEmptyArrayOfAttendeesType | |
# | |
self.OptionalAttendees='' | |
## | |
# Resources property | |
# | |
# @var EWSType_NonEmptyArrayOfAttendeesType | |
# | |
self.Resources='' | |
## | |
# ConflictingMeetingCount property | |
# | |
# @var EWSType_int | |
# | |
self.ConflictingMeetingCount='' | |
## | |
# AdjacentMeetingCount property | |
# | |
# @var EWSType_int | |
# | |
self.AdjacentMeetingCount='' | |
## | |
# ConflictingMeetings property | |
# | |
# @var EWSType_NonEmptyArrayOfAllItemsType | |
# | |
self.ConflictingMeetings='' | |
## | |
# AdjacentMeetings property | |
# | |
# @var EWSType_NonEmptyArrayOfAllItemsType | |
# | |
self.AdjacentMeetings='' | |
## | |
# Duration property | |
# | |
# @var string | |
# | |
self.Duration='' | |
## | |
# TimeZone property | |
# | |
# @var string | |
# | |
self.TimeZone='' | |
## | |
# AppointmentReplyTime property | |
# | |
# @var EWSType_dateTime | |
# | |
self.AppointmentReplyTime='' | |
## | |
# AppointmentSequenceNumber property | |
# | |
# @var EWSType_int | |
# | |
self.AppointmentSequenceNumber='' | |
## | |
# AppointmentState property | |
# | |
# @var EWSType_int | |
# | |
self.AppointmentState='' | |
## | |
# Recurrence property | |
# | |
# @var EWSType_RecurrenceType | |
# | |
self.Recurrence='' | |
## | |
# FirstOccurrence property | |
# | |
# @var EWSType_OccurrenceInfoType | |
# | |
self.FirstOccurrence='' | |
## | |
# LastOccurrence property | |
# | |
# @var EWSType_OccurrenceInfoType | |
# | |
self.LastOccurrence='' | |
## | |
# ModifiedOccurrences property | |
# | |
# @var EWSType_NonEmptyArrayOfOccurrenceInfoType | |
# | |
self.ModifiedOccurrences='' | |
## | |
# DeletedOccurrences property | |
# | |
# @var EWSType_NonEmptyArrayOfDeletedOccurrencesType | |
# | |
self.DeletedOccurrences='' | |
## | |
# MeetingTimeZone property | |
# | |
# @var EWSType_TimeZoneType | |
# | |
self.MeetingTimeZone='' | |
## | |
# ConferenceType property | |
# | |
# @var EWSType_int | |
# | |
self.ConferenceType='' | |
## | |
# AllowNewTimeProposal property | |
# | |
# @var EWSType_boolean | |
# | |
self.AllowNewTimeProposal='' | |
## | |
# IsOnlineMeeting property | |
# | |
# @var EWSType_boolean | |
# | |
self.IsOnlineMeeting='' | |
## | |
# MeetingWorkspaceUrl property | |
# | |
# @var string | |
# | |
self.MeetingWorkspaceUrl='' | |
## | |
# NetShowUrl property | |
# | |
# @var string | |
# | |
self.NetShowUrl='' | |
self.Subject = subject | |
self.Start = start | |
self.End = end | |
self.Body = body | |
self.Location = location | |
#self.hasreminder = hasreminder | |
def __str__(self): | |
return '''Subject: %s | |
Start: %s | |
End: %s | |
Location: %s | |
Body: %s''' % (self.subject, self.start, self.end, self.location, self.body) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment