summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2009-04-26 13:37:43 -0400
committerMike Crute <mcrute@gmail.com>2009-04-26 13:37:43 -0400
commit0e23789c050b1b763736a7b3df9f8536a139e63d (patch)
treec79c8423905b7a6f35458249253e8e0b0360c714
parent1d7b31d19562823dda93b045b786606191fd3e5b (diff)
downloadcalendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.tar.bz2
calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.tar.xz
calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.zip
Moved code around into smaller packages.
-rw-r--r--exchange.py350
-rw-r--r--exchange/__init__.py15
-rw-r--r--exchange/authenticators.py79
-rw-r--r--exchange/commands.py164
-rw-r--r--exchange/timezones.py23
-rw-r--r--server.py69
6 files changed, 350 insertions, 350 deletions
diff --git a/exchange.py b/exchange.py
deleted file mode 100644
index 38ee312..0000000
--- a/exchange.py
+++ /dev/null
@@ -1,350 +0,0 @@
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()
diff --git a/exchange/__init__.py b/exchange/__init__.py
new file mode 100644
index 0000000..e3d54e0
--- /dev/null
+++ b/exchange/__init__.py
@@ -0,0 +1,15 @@
1# -*- coding: utf-8 -*-
2"""
3Exchange Server Handling Code
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: April 26, 2009
8@version: $Rev$
9
10$Id$
11"""
12
13
14class ExchangeException(Exception):
15 "Exception that is thrown by all Exchange handling code."
diff --git a/exchange/authenticators.py b/exchange/authenticators.py
new file mode 100644
index 0000000..33db4c7
--- /dev/null
+++ b/exchange/authenticators.py
@@ -0,0 +1,79 @@
1# -*- coding: utf-8 -*-
2"""
3Exchange Server Authenticators
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: April 26, 2009
8@version: $Rev$
9
10$Id$
11"""
12import urllib
13
14from copy import copy
15from httplib import HTTPSConnection
16from Cookie import SimpleCookie
17
18
19class ExchangeAuthenticator(object):
20
21 _auth_cookie = None
22 authenticated = False
23 username = None
24
25 def __init__(self, web_server):
26 self.web_server = web_server
27
28 def authenticate(self, username, password):
29 """
30 Authenticate the user and cache the authentication so we aren't
31 hammering the auth server. Should hanlde expiration eventually.
32 """
33 self.username = username
34
35 if self._auth_cookie:
36 return self._auth_cookie
37
38 self._auth_cookie = self._do_authentication(username, password)
39 return self._auth_cookie
40
41 def _do_authentication(self, username, password):
42 raise NotImplemented
43
44 def patch_headers(self, headers):
45 raise NotImplemented
46
47
48class CookieAuthenticator(ExchangeAuthenticator):
49
50 AUTH_DLL = "/exchweb/bin/auth/owaauth.dll"
51
52 def _do_authentication(self, username, password):
53 """
54 Does a post to the authentication DLL to fetch a cookie for the session
55 this can then be passed back to the exchange API for servers that don't
56 support basicc HTTP auth.
57 """
58 params = urllib.urlencode({ "destination": "https://%s/exchange" % (self.web_server),
59 "flags": "0",
60 "username": username,
61 "password": password,
62 "SubmitCreds": "Log On",
63 "trusted": "4"
64 })
65
66 conn = HTTPSConnection(self.web_server)
67 conn.request("POST", self.AUTH_DLL, params)
68 response = conn.getresponse()
69
70 cookie = SimpleCookie(response.getheader("set-cookie"))
71 cookie = ("sessionid=%s" % cookie["sessionid"].value, "cadata=%s" % cookie["cadata"].value)
72
73 self.authenticated = True
74 return "; ".join(cookie)
75
76 def patch_headers(self, headers):
77 out_headers = copy(headers)
78 out_headers["Cookie"] = self._auth_cookie
79 return out_headers
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"""
3Exchange Commands
4
5@author: Mike Crute (mcrute@gmail.com)
6@date: November 10, 2008
7@version: $Revision$
8
9This is a set of classes that starts to define a set of classes for
10fetching data using Exchange's WebDAV API. This is still pretty
11development code but it does the trick. Watch out, it doesn't consider
12many corner cases.
13
14$Id$
15"""
16import xml.etree.cElementTree as etree
17
18from httplib import HTTPSConnection
19from string import Template
20from datetime import datetime, timedelta
21
22import dateutil.parser
23from icalendar import Calendar, Event, Alarm
24
25from exchange import ExchangeException
26
27
28class 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
93class 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
diff --git a/exchange/timezones.py b/exchange/timezones.py
new file mode 100644
index 0000000..88e84b5
--- /dev/null
+++ b/exchange/timezones.py
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2"""
3Timezone Definitions
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: April 26, 2009
8@version: $Rev$
9
10$Id$
11"""
12from datetime import tzinfo, timedelta
13
14
15class EST(tzinfo):
16
17 def tzname(self, dt):
18 return "EST"
19
20 def utcoffset(self, dt):
21 return timedelta(0)
22
23 dst = utcoffset
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..f4f671c
--- /dev/null
+++ b/server.py
@@ -0,0 +1,69 @@
1# -*- coding: utf-8 -*-
2"""
3Exchange Calendar Proxy Server
4
5@author: Mike Crute (mcrute@gmail.com)
6@organization: SoftGroup Interactive, Inc.
7@date: April 26, 2009
8@version: $Rev$
9
10$Id$
11"""
12from getpass import getpass
13from ConfigParser import ConfigParser
14from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
15
16from exchange.commands import FetchCalendar
17from exchange.authenticators import CookieAuthenticator
18
19
20class CalendarHandler(BaseHTTPRequestHandler):
21 def do_GET(self):
22 print('> GET CALENDARS')
23
24 fetcher = FetchCalendar(self.server.exchange_server)
25 authenticator = CookieAuthenticator(self.server.exchange_server)
26
27 authenticator.authenticate(self.server.user, self.server.password)
28 fetcher.authenticator = authenticator
29
30 calendar = fetcher.execute()
31 self.wfile.write(calendar.as_string()).close()
32
33
34def get_un_pass(config):
35 username = config.get('exchange', 'user')
36
37 if config.has_option('exchange', 'password'):
38 password = config.get('exchange', 'password')
39 else:
40 password = getpass('Exchange Password: ')
41
42 return username, password
43
44
45def get_host_port(config):
46 bind_address = config.get('local_server', 'address')
47 bind_port = int(config.get('local_server', 'port'))
48
49 return bind_address, bind_port
50
51
52def main(config_file='exchange.cfg'):
53 config = ConfigParser().read(config_file)
54 server_cfg = get_host_port(config)
55
56 print('Exchange iCal Proxy Running on port {0:d}'.format(server_cfg[1]))
57
58 server = HTTPServer(server_cfg, CalendarHandler)
59 server.exchange_server = config.get('exchange', 'server')
60 server.user, server.password = get_un_pass(config)
61
62 try:
63 server.serve_forever()
64 except KeyboardInterrupt:
65 print '\n All done, shutting down.'
66
67
68if __name__ == '__main__':
69 main()