summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2009-04-21 21:42:52 -0400
committerMike Crute <mcrute@gmail.com>2009-04-21 21:42:52 -0400
commit8bf001babf419da9e830c00a93d8b0953a71c9e6 (patch)
tree6367d88bded4723f9c8333762d077cb41b61e118
downloadcalendar_proxy-8bf001babf419da9e830c00a93d8b0953a71c9e6.tar.bz2
calendar_proxy-8bf001babf419da9e830c00a93d8b0953a71c9e6.tar.xz
calendar_proxy-8bf001babf419da9e830c00a93d8b0953a71c9e6.zip
Initial import from subversion.
-rw-r--r--exchange.cfg11
-rw-r--r--exchange.py350
2 files changed, 361 insertions, 0 deletions
diff --git a/exchange.cfg b/exchange.cfg
new file mode 100644
index 0000000..6a96abe
--- /dev/null
+++ b/exchange.cfg
@@ -0,0 +1,11 @@
1[exchange]
2server = <your_server>
3user = <your_username>
4
5[local_server]
6address = 0.0.0.0
7port = 8586
8
9[alarms]
10enabled = True
11minutes_before = 15
diff --git a/exchange.py b/exchange.py
new file mode 100644
index 0000000..42ba547
--- /dev/null
+++ b/exchange.py
@@ -0,0 +1,350 @@
1"""
2Exchange API -> iCal Fie Proxy
3
4@author: Mike Crute (mcrute@gmail.com)
5@date: November 10, 2008
6@version: $Revision$
7
8This is a set of classes that starts to define a set of classes for
9fetching data using Exchange's WebDAV API. This is still pretty
10development code but it does the trick. Watch out, it doesn't consider
11many corner cases.
12
13== Config File Format ==
14[exchange]
15server = <your_mail_server>
16
17[authentication]
18user = <your_username>
19password = <your_password>
20
21[server]
22bind_address = 0.0.0.0
23bind_port = 8000
24"""
25
26######################################################################
27# Standard Library Imports
28######################################################################
29import urllib
30import xml.etree.cElementTree as etree
31
32from getpass import getpass
33from copy import copy
34from httplib import HTTPSConnection
35from string import Template
36from Cookie import SimpleCookie
37from datetime import datetime, timedelta, tzinfo
38
39######################################################################
40# Third-Party Library Imports
41######################################################################
42#: These can all be found on pypi
43import dateutil.parser
44from icalendar import Calendar, Event, Alarm
45
46
47######################################################################
48# Exceptions
49######################################################################
50class ExchangeException(Exception):
51 """
52 Exception that is thrown by all Exchange handling code.
53 """
54 pass
55
56
57######################################################################
58# Exchange Enumerations
59######################################################################
60class CalendarInstanceType(object):
61 """
62 Enum for Calendar Instance Types
63 @see: http://msdn.microsoft.com/en-us/library/ms870457(EXCHG.65).aspx
64
65 This ended up not being used but its probably good to keep around for
66 future use.
67 """
68 #: Single appointment
69 SINGLE = 0
70
71 #: Master recurring appointment
72 MASTER = 1
73
74 #: Single instance of a recurring appointment
75 INSTANCE = 2
76
77 #: Exception to a recurring appointment
78 EXCEPTION = 3
79
80
81######################################################################
82# EST Timezone
83######################################################################
84class EST(tzinfo):
85
86 def tzname(self, dt):
87 return "EST"
88
89 def utcoffset(self, dt):
90 return timedelta(0)
91
92 dst = utcoffset
93
94######################################################################
95# Exchange Authenticators
96######################################################################
97class ExchangeAuthenticator(object):
98 """
99 Exchange Authenticator Interface
100 """
101 _auth_cookie = None
102 authenticated = False
103 username = None
104
105 def __init__(self, web_server):
106 self.web_server = web_server
107
108 def authenticate(self, username, password):
109 """
110 Authenticate the user and cache the authentication so we aren't
111 hammering the auth server. Should hanlde expiration eventually.
112 """
113 self.username = username
114
115 if self._auth_cookie:
116 return self._auth_cookie
117
118 self._auth_cookie = self._do_authentication(username, password)
119 return self._auth_cookie
120
121 def _do_authentication(self, username, password):
122 raise NotImplemented("Implement in a subclass.")
123
124 def patch_headers(self, headers):
125 raise NotImplemented("Implement in a subclass.")
126
127class CookieAuthenticator(ExchangeAuthenticator):
128 #: Authentication DLL on the exchange server
129 AUTH_DLL = "/exchweb/bin/auth/owaauth.dll"
130
131 def _do_authentication(self, username, password):
132 """
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
135 support basicc HTTP auth.
136 """
137 params = urllib.urlencode({ "destination": "https://%s/exchange" % (self.web_server),
138 "flags": "0",
139 "username": username,
140 "password": password,
141 "SubmitCreds": "Log On",
142 "trusted": "4"
143 })
144
145 conn = HTTPSConnection(self.web_server)
146 conn.request("POST", self.AUTH_DLL, params)
147 response = conn.getresponse()
148
149 cookie = SimpleCookie(response.getheader("set-cookie"))
150 cookie = ("sessionid=%s" % cookie["sessionid"].value, "cadata=%s" % cookie["cadata"].value)
151
152 self.authenticated = True
153 return "; ".join(cookie)
154
155 def patch_headers(self, headers):
156 """
157 Patch the headers dictionary with authentication information and
158 return the patched dictionary. I'm not a big fan of patching
159 dictionaries in-place so just make a copy first.
160 """
161 out_headers = copy(headers)
162 out_headers["Cookie"] = self._auth_cookie
163 return out_headers
164
165######################################################################
166# Exchange Command Base Class
167######################################################################
168class ExchangeCommand(object):
169 """
170 Base class for Exchange commands. This really shouldn't be constructed
171 directly but should be subclassed to do useful things.
172 """
173
174 #: Base URL for Exchange commands.
175 BASE_URL = Template("/exchange/${username}/${method}")
176
177 #: Basic headers that are required for all requests
178 BASE_HEADERS = {
179 "Content-Type": 'text/xml; charset="UTF-8"',
180 "Depth": "0",
181 "Translate": "f",
182 }
183
184 def __init__(self, server, authenticator=None):
185 self.server = server
186 self.authenticator = authenticator
187
188 def _get_xml(self, **kwargs):
189 """
190 Try to get an XML response from the server.
191 @return: ElementTree response
192 """
193 if not self.authenticator.authenticated:
194 raise ExchangeException("Not authenticated. Call authenticate() first.")
195
196 # Lets forcibly override the username with the user we're querying as
197 kwargs["username"] = self.authenticator.username
198
199 xml = self._get_query(**kwargs)
200 url = self.BASE_URL.substitute({ "username": self.authenticator.username,
201 "method": self.exchange_method })
202 query = Template(xml).substitute(kwargs)
203 send_headers = self.authenticator.patch_headers(self.BASE_HEADERS)
204
205 conn = HTTPSConnection(self.server)
206 conn.request(self.dav_method.upper(), url, query, headers=send_headers)
207 resp = conn.getresponse()
208
209 # TODO: Lets determine authentication errors here and fix them.
210 if int(resp.status) > 299 or int(resp.status) < 200:
211 raise ExchangeException("%s %s" % (resp.status, resp.reason))
212
213 return etree.fromstring(resp.read())
214
215 def _get_query(self, **kwargs):
216 """
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
219 magic to to build the XML query.
220 """
221 declaration = etree.ProcessingInstruction("xml", 'version="1.0"')
222
223 request = etree.Element("g:searchrequest", { "xmlns:g": "DAV:" })
224 query = etree.SubElement(request, "g:sql")
225 query.text = Template(self.sql).substitute(kwargs)
226
227 output = etree.tostring(declaration)
228 output += etree.tostring(request)
229
230 return output
231
232
233######################################################################
234# Exchange Commands
235######################################################################
236class FetchCalendar(ExchangeCommand):
237 exchange_method = "calendar"
238 dav_method = "search"
239
240 sql = """
241 SELECT
242 "urn:schemas:calendar:location" AS location,
243 "urn:schemas:httpmail:normalizedsubject" AS subject,
244 "urn:schemas:calendar:dtstart" AS start_date,
245 "urn:schemas:calendar:dtend" AS end_date,
246 "urn:schemas:calendar:busystatus" AS busy_status,
247 "urn:schemas:calendar:instancetype" AS instance_type,
248 "urn:schemas:calendar:timezone" AS timezone_info,
249 "urn:schemas:httpmail:textdescription" AS description
250 FROM
251 Scope('SHALLOW TRAVERSAL OF "/exchange/${username}/calendar/"')
252 WHERE
253 NOT "urn:schemas:calendar:instancetype" = 1
254 AND "DAV:contentclass" = 'urn:content-classes:appointment'
255 ORDER BY
256 "urn:schemas:calendar:dtstart" ASC
257 """
258
259 def execute(self, alarms=True, alarm_offset=15, **kwargs):
260 exchange_xml = self._get_xml(**kwargs)
261 calendar = Calendar()
262
263 for item in exchange_xml.getchildren():
264 item = item.find("{DAV:}propstat").find("{DAV:}prop")
265 event = Event()
266
267 # These tests may look funny but the result of item.find
268 # does NOT evaluate to true even though it is not None
269 # so, we have to check the interface of the returned item
270 # to make sure its usable.
271
272 subject = item.find("subject")
273 if hasattr(subject, "text"):
274 event.add("summary", subject.text)
275
276 location = item.find("location")
277 if hasattr(location, "text"):
278 event.add("location", location.text)
279
280 description = item.find("description")
281 if hasattr(description, "text"):
282 event.add("description", description.text)
283
284 # Dates should always exist
285 start_date = dateutil.parser.parse(item.find("start_date").text)
286 event.add("dtstart", start_date)
287
288 end_date = dateutil.parser.parse(item.find("end_date").text)
289 event.add("dtend", end_date)
290
291 if item.get("timezone_info"):
292 """This comes back from Exchange as already formatted
293 ical data. We probably need to parse and re-construct
294 it unless the icalendar api lets us just dump it out.
295 """
296 pass
297
298 if alarms and start_date > datetime.now(tz=EST()):
299 alarm = Alarm()
300 alarm.add("action", "DISPLAY")
301 alarm.add("description", "REMINDER")
302 alarm.add("trigger", timedelta(minutes=alarm_offset))
303 event.add_component(alarm)
304
305 calendar.add_component(event)
306
307 return calendar
308
309
310######################################################################
311# Testing Server
312######################################################################
313if __name__ == "__main__":
314 import sys
315 from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
316 from ConfigParser import ConfigParser
317
318 config = ConfigParser()
319 config.read("exchange.cfg")
320
321 username = config.get("exchange", "user")
322 password = getpass("Exchange Password: ")
323
324 class CalendarHandler(BaseHTTPRequestHandler):
325 def do_GET(self):
326 print "> GET CALENDARS"
327
328 server = config.get("exchange", "server")
329 fetcher = FetchCalendar(server)
330
331 authenticator = CookieAuthenticator(server)
332 authenticator.authenticate(username, password)
333 fetcher.authenticator = authenticator
334
335 calendar = fetcher.execute()
336
337 self.wfile.write(calendar.as_string())
338 self.wfile.close()
339
340 try:
341 bind_address = config.get("local_server", "address")
342 bind_port = int(config.get("local_server", "port"))
343
344 print "Exchange iCal Proxy Running on port %d" % bind_port
345
346 server = HTTPServer((bind_address, bind_port), CalendarHandler)
347 server.serve_forever()
348 except KeyboardInterrupt:
349 print "\n Fine, be like that."
350 sys.exit()