summaryrefslogtreecommitdiff
path: root/exchange
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 /exchange
parent1d7b31d19562823dda93b045b786606191fd3e5b (diff)
downloadcalendar_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__.py15
-rw-r--r--exchange/authenticators.py79
-rw-r--r--exchange/commands.py164
-rw-r--r--exchange/timezones.py23
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"""
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