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 | |
parent | 1d7b31d19562823dda93b045b786606191fd3e5b (diff) | |
download | calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.tar.bz2 calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.tar.xz calendar_proxy-0e23789c050b1b763736a7b3df9f8536a139e63d.zip |
Moved code around into smaller packages.
-rw-r--r-- | exchange.py | 350 | ||||
-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 | ||||
-rw-r--r-- | server.py | 69 |
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 | """ | ||
2 | Exchange API -> iCal Fie Proxy | ||
3 | |||
4 | @author: Mike Crute (mcrute@gmail.com) | ||
5 | @date: November 10, 2008 | ||
6 | @version: $Revision$ | ||
7 | |||
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 | ||
10 | development code but it does the trick. Watch out, it doesn't consider | ||
11 | many corner cases. | ||
12 | |||
13 | == Config File Format == | ||
14 | [exchange] | ||
15 | server = <your_mail_server> | ||
16 | |||
17 | [authentication] | ||
18 | user = <your_username> | ||
19 | password = <your_password> | ||
20 | |||
21 | [server] | ||
22 | bind_address = 0.0.0.0 | ||
23 | bind_port = 8000 | ||
24 | """ | ||
25 | |||
26 | ###################################################################### | ||
27 | # Standard Library Imports | ||
28 | ###################################################################### | ||
29 | import urllib | ||
30 | import xml.etree.cElementTree as etree | ||
31 | |||
32 | from getpass import getpass | ||
33 | from copy import copy | ||
34 | from httplib import HTTPSConnection | ||
35 | from string import Template | ||
36 | from Cookie import SimpleCookie | ||
37 | from datetime import datetime, timedelta, tzinfo | ||
38 | |||
39 | ###################################################################### | ||
40 | # Third-Party Library Imports | ||
41 | ###################################################################### | ||
42 | #: These can all be found on pypi | ||
43 | import dateutil.parser | ||
44 | from icalendar import Calendar, Event, Alarm | ||
45 | |||
46 | |||
47 | ###################################################################### | ||
48 | # Exceptions | ||
49 | ###################################################################### | ||
50 | class ExchangeException(Exception): | ||
51 | """ | ||
52 | Exception that is thrown by all Exchange handling code. | ||
53 | """ | ||
54 | pass | ||
55 | |||
56 | |||
57 | ###################################################################### | ||
58 | # Exchange Enumerations | ||
59 | ###################################################################### | ||
60 | class 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 | ###################################################################### | ||
84 | class 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 | ###################################################################### | ||
97 | class 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 | |||
127 | class 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 | ###################################################################### | ||
168 | class 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 | ###################################################################### | ||
236 | class 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 | ###################################################################### | ||
313 | if __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 | """ | ||
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 | ||
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 | """ | ||
3 | Exchange 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 | """ | ||
12 | from getpass import getpass | ||
13 | from ConfigParser import ConfigParser | ||
14 | from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler | ||
15 | |||
16 | from exchange.commands import FetchCalendar | ||
17 | from exchange.authenticators import CookieAuthenticator | ||
18 | |||
19 | |||
20 | class 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 | |||
34 | def 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 | |||
45 | def 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 | |||
52 | def 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 | |||
68 | if __name__ == '__main__': | ||
69 | main() | ||