diff options
author | Mike Crute <mcrute@gmail.com> | 2009-04-26 13:37:43 -0400 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2009-04-26 13:37:43 -0400 |
commit | 0e23789c050b1b763736a7b3df9f8536a139e63d (patch) | |
tree | c79c8423905b7a6f35458249253e8e0b0360c714 /exchange | |
parent | 1d7b31d19562823dda93b045b786606191fd3e5b (diff) | |
download | calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.tar.bz2 calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.tar.xz calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.zip |
Moved code around into smaller packages.
Diffstat (limited to 'exchange')
-rw-r--r-- | exchange/__init__.py | 15 | ||||
-rw-r--r-- | exchange/authenticators.py | 79 | ||||
-rw-r--r-- | exchange/commands.py | 164 | ||||
-rw-r--r-- | exchange/timezones.py | 23 |
4 files changed, 281 insertions, 0 deletions
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 | """ | ||
3 | Exchange 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 | |||
14 | class 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 | """ | ||
3 | Exchange 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 | """ | ||
12 | import urllib | ||
13 | |||
14 | from copy import copy | ||
15 | from httplib import HTTPSConnection | ||
16 | from Cookie import SimpleCookie | ||
17 | |||
18 | |||
19 | class 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 | |||
48 | class 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 | """ | ||
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 | ||
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 | """ | ||
3 | Timezone 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 | """ | ||
12 | from datetime import tzinfo, timedelta | ||
13 | |||
14 | |||
15 | class 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 | ||