diff options
Diffstat (limited to 'exchange/commands.py')
-rw-r--r-- | exchange/commands.py | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/exchange/commands.py b/exchange/commands.py new file mode 100644 index 0000000..855e1b5 --- /dev/null +++ b/exchange/commands.py | |||
@@ -0,0 +1,164 @@ | |||
1 | # -*- coding: utf-8 -*- | ||
2 | """ | ||
3 | Exchange Commands | ||
4 | |||
5 | @author: Mike Crute (mcrute@gmail.com) | ||
6 | @date: November 10, 2008 | ||
7 | @version: $Revision$ | ||
8 | |||
9 | This is a set of classes that starts to define a set of classes for | ||
10 | fetching data using Exchange's WebDAV API. This is still pretty | ||
11 | development code but it does the trick. Watch out, it doesn't consider | ||
12 | many corner cases. | ||
13 | |||
14 | $Id$ | ||
15 | """ | ||
16 | import xml.etree.cElementTree as etree | ||
17 | |||
18 | from httplib import HTTPSConnection | ||
19 | from string import Template | ||
20 | from datetime import datetime, timedelta | ||
21 | |||
22 | import dateutil.parser | ||
23 | from icalendar import Calendar, Event, Alarm | ||
24 | |||
25 | from exchange import ExchangeException | ||
26 | |||
27 | |||
28 | class ExchangeCommand(object): | ||
29 | """ | ||
30 | Base class for Exchange commands. This really shouldn't be constructed | ||
31 | directly but should be subclassed to do useful things. | ||
32 | """ | ||
33 | |||
34 | #: Base URL for Exchange commands. | ||
35 | BASE_URL = Template("/exchange/${username}/${method}") | ||
36 | |||
37 | #: Basic headers that are required for all requests | ||
38 | BASE_HEADERS = { | ||
39 | "Content-Type": 'text/xml; charset="UTF-8"', | ||
40 | "Depth": "0", | ||
41 | "Translate": "f", | ||
42 | } | ||
43 | |||
44 | def __init__(self, server, authenticator=None): | ||
45 | self.server = server | ||
46 | self.authenticator = authenticator | ||
47 | |||
48 | def _get_xml(self, **kwargs): | ||
49 | """ | ||
50 | Try to get an XML response from the server. | ||
51 | @return: ElementTree response | ||
52 | """ | ||
53 | if not self.authenticator.authenticated: | ||
54 | raise ExchangeException("Not authenticated. Call authenticate() first.") | ||
55 | |||
56 | # Lets forcibly override the username with the user we're querying as | ||
57 | kwargs["username"] = self.authenticator.username | ||
58 | |||
59 | xml = self._get_query(**kwargs) | ||
60 | url = self.BASE_URL.substitute({ "username": self.authenticator.username, | ||
61 | "method": self.exchange_method }) | ||
62 | query = Template(xml).substitute(kwargs) | ||
63 | send_headers = self.authenticator.patch_headers(self.BASE_HEADERS) | ||
64 | |||
65 | conn = HTTPSConnection(self.server) | ||
66 | conn.request(self.dav_method.upper(), url, query, headers=send_headers) | ||
67 | resp = conn.getresponse() | ||
68 | |||
69 | # TODO: Lets determine authentication errors here and fix them. | ||
70 | if int(resp.status) > 299 or int(resp.status) < 200: | ||
71 | raise ExchangeException("%s %s" % (resp.status, resp.reason)) | ||
72 | |||
73 | return etree.fromstring(resp.read()) | ||
74 | |||
75 | def _get_query(self, **kwargs): | ||
76 | """ | ||
77 | Build up the XML query for the server. Mostly just does a lot | ||
78 | of template substitutions, also does a little bit of elementtree | ||
79 | magic to to build the XML query. | ||
80 | """ | ||
81 | declaration = etree.ProcessingInstruction("xml", 'version="1.0"') | ||
82 | |||
83 | request = etree.Element("g:searchrequest", { "xmlns:g": "DAV:" }) | ||
84 | query = etree.SubElement(request, "g:sql") | ||
85 | query.text = Template(self.sql).substitute(kwargs) | ||
86 | |||
87 | output = etree.tostring(declaration) | ||
88 | output += etree.tostring(request) | ||
89 | |||
90 | return output | ||
91 | |||
92 | |||
93 | class FetchCalendar(ExchangeCommand): | ||
94 | exchange_method = "calendar" | ||
95 | dav_method = "search" | ||
96 | |||
97 | sql = """ | ||
98 | SELECT | ||
99 | "urn:schemas:calendar:location" AS location, | ||
100 | "urn:schemas:httpmail:normalizedsubject" AS subject, | ||
101 | "urn:schemas:calendar:dtstart" AS start_date, | ||
102 | "urn:schemas:calendar:dtend" AS end_date, | ||
103 | "urn:schemas:calendar:busystatus" AS busy_status, | ||
104 | "urn:schemas:calendar:instancetype" AS instance_type, | ||
105 | "urn:schemas:calendar:timezone" AS timezone_info, | ||
106 | "urn:schemas:httpmail:textdescription" AS description | ||
107 | FROM | ||
108 | Scope('SHALLOW TRAVERSAL OF "/exchange/${username}/calendar/"') | ||
109 | WHERE | ||
110 | NOT "urn:schemas:calendar:instancetype" = 1 | ||
111 | AND "DAV:contentclass" = 'urn:content-classes:appointment' | ||
112 | ORDER BY | ||
113 | "urn:schemas:calendar:dtstart" ASC | ||
114 | """ | ||
115 | |||
116 | def execute(self, alarms=True, alarm_offset=15, **kwargs): | ||
117 | exchange_xml = self._get_xml(**kwargs) | ||
118 | calendar = Calendar() | ||
119 | |||
120 | for item in exchange_xml.getchildren(): | ||
121 | item = item.find("{DAV:}propstat").find("{DAV:}prop") | ||
122 | event = Event() | ||
123 | |||
124 | # These tests may look funny but the result of item.find | ||
125 | # does NOT evaluate to true even though it is not None | ||
126 | # so, we have to check the interface of the returned item | ||
127 | # to make sure its usable. | ||
128 | |||
129 | subject = item.find("subject") | ||
130 | if hasattr(subject, "text"): | ||
131 | event.add("summary", subject.text) | ||
132 | |||
133 | location = item.find("location") | ||
134 | if hasattr(location, "text"): | ||
135 | event.add("location", location.text) | ||
136 | |||
137 | description = item.find("description") | ||
138 | if hasattr(description, "text"): | ||
139 | event.add("description", description.text) | ||
140 | |||
141 | # Dates should always exist | ||
142 | start_date = dateutil.parser.parse(item.find("start_date").text) | ||
143 | event.add("dtstart", start_date) | ||
144 | |||
145 | end_date = dateutil.parser.parse(item.find("end_date").text) | ||
146 | event.add("dtend", end_date) | ||
147 | |||
148 | if item.get("timezone_info"): | ||
149 | """This comes back from Exchange as already formatted | ||
150 | ical data. We probably need to parse and re-construct | ||
151 | it unless the icalendar api lets us just dump it out. | ||
152 | """ | ||
153 | pass | ||
154 | |||
155 | if alarms and start_date > datetime.now(tz=EST()): | ||
156 | alarm = Alarm() | ||
157 | alarm.add("action", "DISPLAY") | ||
158 | alarm.add("description", "REMINDER") | ||
159 | alarm.add("trigger", timedelta(minutes=alarm_offset)) | ||
160 | event.add_component(alarm) | ||
161 | |||
162 | calendar.add_component(event) | ||
163 | |||
164 | return calendar | ||