diff options
Diffstat (limited to 'exchange.py')
-rw-r--r-- | exchange.py | 116 |
1 files changed, 58 insertions, 58 deletions
diff --git a/exchange.py b/exchange.py index 42ba547..38ee312 100644 --- a/exchange.py +++ b/exchange.py | |||
@@ -6,7 +6,7 @@ Exchange API -> iCal Fie Proxy | |||
6 | @version: $Revision$ | 6 | @version: $Revision$ |
7 | 7 | ||
8 | This is a set of classes that starts to define a set of classes for | 8 | This is a set of classes that starts to define a set of classes for |
9 | fetching data using Exchange's WebDAV API. This is still pretty | 9 | fetching data using Exchange's WebDAV API. This is still pretty |
10 | development code but it does the trick. Watch out, it doesn't consider | 10 | development code but it does the trick. Watch out, it doesn't consider |
11 | many corner cases. | 11 | many corner cases. |
12 | 12 | ||
@@ -61,19 +61,19 @@ class CalendarInstanceType(object): | |||
61 | """ | 61 | """ |
62 | Enum for Calendar Instance Types | 62 | Enum for Calendar Instance Types |
63 | @see: http://msdn.microsoft.com/en-us/library/ms870457(EXCHG.65).aspx | 63 | @see: http://msdn.microsoft.com/en-us/library/ms870457(EXCHG.65).aspx |
64 | 64 | ||
65 | This ended up not being used but its probably good to keep around for | 65 | This ended up not being used but its probably good to keep around for |
66 | future use. | 66 | future use. |
67 | """ | 67 | """ |
68 | #: Single appointment | 68 | #: Single appointment |
69 | SINGLE = 0 | 69 | SINGLE = 0 |
70 | 70 | ||
71 | #: Master recurring appointment | 71 | #: Master recurring appointment |
72 | MASTER = 1 | 72 | MASTER = 1 |
73 | 73 | ||
74 | #: Single instance of a recurring appointment | 74 | #: Single instance of a recurring appointment |
75 | INSTANCE = 2 | 75 | INSTANCE = 2 |
76 | 76 | ||
77 | #: Exception to a recurring appointment | 77 | #: Exception to a recurring appointment |
78 | EXCEPTION = 3 | 78 | EXCEPTION = 3 |
79 | 79 | ||
@@ -101,61 +101,61 @@ class ExchangeAuthenticator(object): | |||
101 | _auth_cookie = None | 101 | _auth_cookie = None |
102 | authenticated = False | 102 | authenticated = False |
103 | username = None | 103 | username = None |
104 | 104 | ||
105 | def __init__(self, web_server): | 105 | def __init__(self, web_server): |
106 | self.web_server = web_server | 106 | self.web_server = web_server |
107 | 107 | ||
108 | def authenticate(self, username, password): | 108 | def authenticate(self, username, password): |
109 | """ | 109 | """ |
110 | Authenticate the user and cache the authentication so we aren't | 110 | Authenticate the user and cache the authentication so we aren't |
111 | hammering the auth server. Should hanlde expiration eventually. | 111 | hammering the auth server. Should hanlde expiration eventually. |
112 | """ | 112 | """ |
113 | self.username = username | 113 | self.username = username |
114 | 114 | ||
115 | if self._auth_cookie: | 115 | if self._auth_cookie: |
116 | return self._auth_cookie | 116 | return self._auth_cookie |
117 | 117 | ||
118 | self._auth_cookie = self._do_authentication(username, password) | 118 | self._auth_cookie = self._do_authentication(username, password) |
119 | return self._auth_cookie | 119 | return self._auth_cookie |
120 | 120 | ||
121 | def _do_authentication(self, username, password): | 121 | def _do_authentication(self, username, password): |
122 | raise NotImplemented("Implement in a subclass.") | 122 | raise NotImplemented("Implement in a subclass.") |
123 | 123 | ||
124 | def patch_headers(self, headers): | 124 | def patch_headers(self, headers): |
125 | raise NotImplemented("Implement in a subclass.") | 125 | raise NotImplemented("Implement in a subclass.") |
126 | 126 | ||
127 | class CookieAuthenticator(ExchangeAuthenticator): | 127 | class CookieAuthenticator(ExchangeAuthenticator): |
128 | #: Authentication DLL on the exchange server | 128 | #: Authentication DLL on the exchange server |
129 | AUTH_DLL = "/exchweb/bin/auth/owaauth.dll" | 129 | AUTH_DLL = "/exchweb/bin/auth/owaauth.dll" |
130 | 130 | ||
131 | def _do_authentication(self, username, password): | 131 | def _do_authentication(self, username, password): |
132 | """ | 132 | """ |
133 | Does a post to the authentication DLL to fetch a cookie for the session | 133 | Does a post to the authentication DLL to fetch a cookie for the session |
134 | this can then be passed back to the exchange API for servers that don't | 134 | this can then be passed back to the exchange API for servers that don't |
135 | support basicc HTTP auth. | 135 | support basicc HTTP auth. |
136 | """ | 136 | """ |
137 | params = urllib.urlencode({ "destination": "https://%s/exchange" % (self.web_server), | 137 | params = urllib.urlencode({ "destination": "https://%s/exchange" % (self.web_server), |
138 | "flags": "0", | 138 | "flags": "0", |
139 | "username": username, | 139 | "username": username, |
140 | "password": password, | 140 | "password": password, |
141 | "SubmitCreds": "Log On", | 141 | "SubmitCreds": "Log On", |
142 | "trusted": "4" | 142 | "trusted": "4" |
143 | }) | 143 | }) |
144 | 144 | ||
145 | conn = HTTPSConnection(self.web_server) | 145 | conn = HTTPSConnection(self.web_server) |
146 | conn.request("POST", self.AUTH_DLL, params) | 146 | conn.request("POST", self.AUTH_DLL, params) |
147 | response = conn.getresponse() | 147 | response = conn.getresponse() |
148 | 148 | ||
149 | cookie = SimpleCookie(response.getheader("set-cookie")) | 149 | cookie = SimpleCookie(response.getheader("set-cookie")) |
150 | cookie = ("sessionid=%s" % cookie["sessionid"].value, "cadata=%s" % cookie["cadata"].value) | 150 | cookie = ("sessionid=%s" % cookie["sessionid"].value, "cadata=%s" % cookie["cadata"].value) |
151 | 151 | ||
152 | self.authenticated = True | 152 | self.authenticated = True |
153 | return "; ".join(cookie) | 153 | return "; ".join(cookie) |
154 | 154 | ||
155 | def patch_headers(self, headers): | 155 | def patch_headers(self, headers): |
156 | """ | 156 | """ |
157 | Patch the headers dictionary with authentication information and | 157 | Patch the headers dictionary with authentication information and |
158 | return the patched dictionary. I'm not a big fan of patching | 158 | return the patched dictionary. I'm not a big fan of patching |
159 | dictionaries in-place so just make a copy first. | 159 | dictionaries in-place so just make a copy first. |
160 | """ | 160 | """ |
161 | out_headers = copy(headers) | 161 | out_headers = copy(headers) |
@@ -170,21 +170,21 @@ class ExchangeCommand(object): | |||
170 | Base class for Exchange commands. This really shouldn't be constructed | 170 | Base class for Exchange commands. This really shouldn't be constructed |
171 | directly but should be subclassed to do useful things. | 171 | directly but should be subclassed to do useful things. |
172 | """ | 172 | """ |
173 | 173 | ||
174 | #: Base URL for Exchange commands. | 174 | #: Base URL for Exchange commands. |
175 | BASE_URL = Template("/exchange/${username}/${method}") | 175 | BASE_URL = Template("/exchange/${username}/${method}") |
176 | 176 | ||
177 | #: Basic headers that are required for all requests | 177 | #: Basic headers that are required for all requests |
178 | BASE_HEADERS = { | 178 | BASE_HEADERS = { |
179 | "Content-Type": 'text/xml; charset="UTF-8"', | 179 | "Content-Type": 'text/xml; charset="UTF-8"', |
180 | "Depth": "0", | 180 | "Depth": "0", |
181 | "Translate": "f", | 181 | "Translate": "f", |
182 | } | 182 | } |
183 | 183 | ||
184 | def __init__(self, server, authenticator=None): | 184 | def __init__(self, server, authenticator=None): |
185 | self.server = server | 185 | self.server = server |
186 | self.authenticator = authenticator | 186 | self.authenticator = authenticator |
187 | 187 | ||
188 | def _get_xml(self, **kwargs): | 188 | def _get_xml(self, **kwargs): |
189 | """ | 189 | """ |
190 | Try to get an XML response from the server. | 190 | Try to get an XML response from the server. |
@@ -192,16 +192,16 @@ class ExchangeCommand(object): | |||
192 | """ | 192 | """ |
193 | if not self.authenticator.authenticated: | 193 | if not self.authenticator.authenticated: |
194 | raise ExchangeException("Not authenticated. Call authenticate() first.") | 194 | raise ExchangeException("Not authenticated. Call authenticate() first.") |
195 | 195 | ||
196 | # Lets forcibly override the username with the user we're querying as | 196 | # Lets forcibly override the username with the user we're querying as |
197 | kwargs["username"] = self.authenticator.username | 197 | kwargs["username"] = self.authenticator.username |
198 | 198 | ||
199 | xml = self._get_query(**kwargs) | 199 | xml = self._get_query(**kwargs) |
200 | url = self.BASE_URL.substitute({ "username": self.authenticator.username, | 200 | url = self.BASE_URL.substitute({ "username": self.authenticator.username, |
201 | "method": self.exchange_method }) | 201 | "method": self.exchange_method }) |
202 | query = Template(xml).substitute(kwargs) | 202 | query = Template(xml).substitute(kwargs) |
203 | send_headers = self.authenticator.patch_headers(self.BASE_HEADERS) | 203 | send_headers = self.authenticator.patch_headers(self.BASE_HEADERS) |
204 | 204 | ||
205 | conn = HTTPSConnection(self.server) | 205 | conn = HTTPSConnection(self.server) |
206 | conn.request(self.dav_method.upper(), url, query, headers=send_headers) | 206 | conn.request(self.dav_method.upper(), url, query, headers=send_headers) |
207 | resp = conn.getresponse() | 207 | resp = conn.getresponse() |
@@ -211,22 +211,22 @@ class ExchangeCommand(object): | |||
211 | raise ExchangeException("%s %s" % (resp.status, resp.reason)) | 211 | raise ExchangeException("%s %s" % (resp.status, resp.reason)) |
212 | 212 | ||
213 | return etree.fromstring(resp.read()) | 213 | return etree.fromstring(resp.read()) |
214 | 214 | ||
215 | def _get_query(self, **kwargs): | 215 | def _get_query(self, **kwargs): |
216 | """ | 216 | """ |
217 | Build up the XML query for the server. Mostly just does a lot | 217 | Build up the XML query for the server. Mostly just does a lot |
218 | of template substitutions, also does a little bit of elementtree | 218 | of template substitutions, also does a little bit of elementtree |
219 | magic to to build the XML query. | 219 | magic to to build the XML query. |
220 | """ | 220 | """ |
221 | declaration = etree.ProcessingInstruction("xml", 'version="1.0"') | 221 | declaration = etree.ProcessingInstruction("xml", 'version="1.0"') |
222 | 222 | ||
223 | request = etree.Element("g:searchrequest", { "xmlns:g": "DAV:" }) | 223 | request = etree.Element("g:searchrequest", { "xmlns:g": "DAV:" }) |
224 | query = etree.SubElement(request, "g:sql") | 224 | query = etree.SubElement(request, "g:sql") |
225 | query.text = Template(self.sql).substitute(kwargs) | 225 | query.text = Template(self.sql).substitute(kwargs) |
226 | 226 | ||
227 | output = etree.tostring(declaration) | 227 | output = etree.tostring(declaration) |
228 | output += etree.tostring(request) | 228 | output += etree.tostring(request) |
229 | 229 | ||
230 | return output | 230 | return output |
231 | 231 | ||
232 | 232 | ||
@@ -236,34 +236,34 @@ class ExchangeCommand(object): | |||
236 | class FetchCalendar(ExchangeCommand): | 236 | class FetchCalendar(ExchangeCommand): |
237 | exchange_method = "calendar" | 237 | exchange_method = "calendar" |
238 | dav_method = "search" | 238 | dav_method = "search" |
239 | 239 | ||
240 | sql = """ | 240 | sql = """ |
241 | SELECT | 241 | SELECT |
242 | "urn:schemas:calendar:location" AS location, | 242 | "urn:schemas:calendar:location" AS location, |
243 | "urn:schemas:httpmail:normalizedsubject" AS subject, | 243 | "urn:schemas:httpmail:normalizedsubject" AS subject, |
244 | "urn:schemas:calendar:dtstart" AS start_date, | 244 | "urn:schemas:calendar:dtstart" AS start_date, |
245 | "urn:schemas:calendar:dtend" AS end_date, | 245 | "urn:schemas:calendar:dtend" AS end_date, |
246 | "urn:schemas:calendar:busystatus" AS busy_status, | 246 | "urn:schemas:calendar:busystatus" AS busy_status, |
247 | "urn:schemas:calendar:instancetype" AS instance_type, | 247 | "urn:schemas:calendar:instancetype" AS instance_type, |
248 | "urn:schemas:calendar:timezone" AS timezone_info, | 248 | "urn:schemas:calendar:timezone" AS timezone_info, |
249 | "urn:schemas:httpmail:textdescription" AS description | 249 | "urn:schemas:httpmail:textdescription" AS description |
250 | FROM | 250 | FROM |
251 | Scope('SHALLOW TRAVERSAL OF "/exchange/${username}/calendar/"') | 251 | Scope('SHALLOW TRAVERSAL OF "/exchange/${username}/calendar/"') |
252 | WHERE | 252 | WHERE |
253 | NOT "urn:schemas:calendar:instancetype" = 1 | 253 | NOT "urn:schemas:calendar:instancetype" = 1 |
254 | AND "DAV:contentclass" = 'urn:content-classes:appointment' | 254 | AND "DAV:contentclass" = 'urn:content-classes:appointment' |
255 | ORDER BY | 255 | ORDER BY |
256 | "urn:schemas:calendar:dtstart" ASC | 256 | "urn:schemas:calendar:dtstart" ASC |
257 | """ | 257 | """ |
258 | 258 | ||
259 | def execute(self, alarms=True, alarm_offset=15, **kwargs): | 259 | def execute(self, alarms=True, alarm_offset=15, **kwargs): |
260 | exchange_xml = self._get_xml(**kwargs) | 260 | exchange_xml = self._get_xml(**kwargs) |
261 | calendar = Calendar() | 261 | calendar = Calendar() |
262 | 262 | ||
263 | for item in exchange_xml.getchildren(): | 263 | for item in exchange_xml.getchildren(): |
264 | item = item.find("{DAV:}propstat").find("{DAV:}prop") | 264 | item = item.find("{DAV:}propstat").find("{DAV:}prop") |
265 | event = Event() | 265 | event = Event() |
266 | 266 | ||
267 | # These tests may look funny but the result of item.find | 267 | # These tests may look funny but the result of item.find |
268 | # does NOT evaluate to true even though it is not None | 268 | # does NOT evaluate to true even though it is not None |
269 | # so, we have to check the interface of the returned item | 269 | # so, we have to check the interface of the returned item |
@@ -276,15 +276,15 @@ class FetchCalendar(ExchangeCommand): | |||
276 | location = item.find("location") | 276 | location = item.find("location") |
277 | if hasattr(location, "text"): | 277 | if hasattr(location, "text"): |
278 | event.add("location", location.text) | 278 | event.add("location", location.text) |
279 | 279 | ||
280 | description = item.find("description") | 280 | description = item.find("description") |
281 | if hasattr(description, "text"): | 281 | if hasattr(description, "text"): |
282 | event.add("description", description.text) | 282 | event.add("description", description.text) |
283 | 283 | ||
284 | # Dates should always exist | 284 | # Dates should always exist |
285 | start_date = dateutil.parser.parse(item.find("start_date").text) | 285 | start_date = dateutil.parser.parse(item.find("start_date").text) |
286 | event.add("dtstart", start_date) | 286 | event.add("dtstart", start_date) |
287 | 287 | ||
288 | end_date = dateutil.parser.parse(item.find("end_date").text) | 288 | end_date = dateutil.parser.parse(item.find("end_date").text) |
289 | event.add("dtend", end_date) | 289 | event.add("dtend", end_date) |
290 | 290 | ||
@@ -303,7 +303,7 @@ class FetchCalendar(ExchangeCommand): | |||
303 | event.add_component(alarm) | 303 | event.add_component(alarm) |
304 | 304 | ||
305 | calendar.add_component(event) | 305 | calendar.add_component(event) |
306 | 306 | ||
307 | return calendar | 307 | return calendar |
308 | 308 | ||
309 | 309 | ||
@@ -314,10 +314,10 @@ if __name__ == "__main__": | |||
314 | import sys | 314 | import sys |
315 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler | 315 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler |
316 | from ConfigParser import ConfigParser | 316 | from ConfigParser import ConfigParser |
317 | 317 | ||
318 | config = ConfigParser() | 318 | config = ConfigParser() |
319 | config.read("exchange.cfg") | 319 | config.read("exchange.cfg") |
320 | 320 | ||
321 | username = config.get("exchange", "user") | 321 | username = config.get("exchange", "user") |
322 | password = getpass("Exchange Password: ") | 322 | password = getpass("Exchange Password: ") |
323 | 323 | ||
@@ -327,22 +327,22 @@ if __name__ == "__main__": | |||
327 | 327 | ||
328 | server = config.get("exchange", "server") | 328 | server = config.get("exchange", "server") |
329 | fetcher = FetchCalendar(server) | 329 | fetcher = FetchCalendar(server) |
330 | 330 | ||
331 | authenticator = CookieAuthenticator(server) | 331 | authenticator = CookieAuthenticator(server) |
332 | authenticator.authenticate(username, password) | 332 | authenticator.authenticate(username, password) |
333 | fetcher.authenticator = authenticator | 333 | fetcher.authenticator = authenticator |
334 | 334 | ||
335 | calendar = fetcher.execute() | 335 | calendar = fetcher.execute() |
336 | 336 | ||
337 | self.wfile.write(calendar.as_string()) | 337 | self.wfile.write(calendar.as_string()) |
338 | self.wfile.close() | 338 | self.wfile.close() |
339 | 339 | ||
340 | try: | 340 | try: |
341 | bind_address = config.get("local_server", "address") | 341 | bind_address = config.get("local_server", "address") |
342 | bind_port = int(config.get("local_server", "port")) | 342 | bind_port = int(config.get("local_server", "port")) |
343 | 343 | ||
344 | print "Exchange iCal Proxy Running on port %d" % bind_port | 344 | print "Exchange iCal Proxy Running on port %d" % bind_port |
345 | 345 | ||
346 | server = HTTPServer((bind_address, bind_port), CalendarHandler) | 346 | server = HTTPServer((bind_address, bind_port), CalendarHandler) |
347 | server.serve_forever() | 347 | server.serve_forever() |
348 | except KeyboardInterrupt: | 348 | except KeyboardInterrupt: |