sonar_rexchange 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +22 -0
- data/LICENSE +20 -0
- data/RAKEFILE +50 -0
- data/README +89 -0
- data/REXCHANGE-CHANGELOG +52 -0
- data/REXCHANGE-MIT-LICENSE +22 -0
- data/VERSION +1 -0
- data/lib/r_exchange.rb +2 -0
- data/lib/rexchange.rb +33 -0
- data/lib/rexchange/appointment.rb +38 -0
- data/lib/rexchange/contact.rb +29 -0
- data/lib/rexchange/credentials.rb +38 -0
- data/lib/rexchange/dav_delete_request.rb +30 -0
- data/lib/rexchange/dav_get_request.rb +28 -0
- data/lib/rexchange/dav_mkcol_request.rb +27 -0
- data/lib/rexchange/dav_move_request.rb +30 -0
- data/lib/rexchange/dav_proppatch_request.rb +49 -0
- data/lib/rexchange/dav_search_request.rb +8 -0
- data/lib/rexchange/exchange_request.rb +86 -0
- data/lib/rexchange/folder.rb +108 -0
- data/lib/rexchange/generic_item.rb +124 -0
- data/lib/rexchange/message.rb +88 -0
- data/lib/rexchange/message_href.rb +107 -0
- data/lib/rexchange/note.rb +16 -0
- data/lib/rexchange/r_exception.rb +25 -0
- data/lib/rexchange/session.rb +28 -0
- data/lib/rexchange/task.rb +26 -0
- data/test/functional.rb +23 -0
- metadata +92 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used for arbitrary SEARCH requests
|
5
|
+
class DavGetRequest < ExchangeRequest
|
6
|
+
METHOD = 'GET'
|
7
|
+
REQUEST_HAS_BODY = false
|
8
|
+
RESPONSE_HAS_BODY = true
|
9
|
+
|
10
|
+
def self.execute(credentials, url, &b)
|
11
|
+
begin
|
12
|
+
options = {
|
13
|
+
:path => url,
|
14
|
+
:headers => {
|
15
|
+
'Translate' => 'f' # Microsoft IIS 5.0 "Translate: f" Source Disclosure Vulnerability (http://www.securityfocus.com/bid/1578)
|
16
|
+
}
|
17
|
+
}
|
18
|
+
response = super credentials, options
|
19
|
+
yield response if b
|
20
|
+
response
|
21
|
+
rescue RException => e
|
22
|
+
raise e
|
23
|
+
rescue Exception => e
|
24
|
+
raise RException.new(options[:request], response, e)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used to make a new collection (aka folder).
|
5
|
+
class DavMkcolRequest < ExchangeRequest
|
6
|
+
METHOD = 'MKCOL'
|
7
|
+
REQUEST_HAS_BODY = false
|
8
|
+
RESPONSE_HAS_BODY = false
|
9
|
+
|
10
|
+
def self.execute(credentials, folder_url, &b)
|
11
|
+
begin
|
12
|
+
options = {
|
13
|
+
:path => folder_url
|
14
|
+
}
|
15
|
+
|
16
|
+
response = super credentials, options
|
17
|
+
yield response if b
|
18
|
+
response
|
19
|
+
rescue RException => e
|
20
|
+
raise e
|
21
|
+
rescue Exception => e
|
22
|
+
raise RException.new(options[:request], response, e)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used to move entities to different locations in the accessable mailbox.
|
5
|
+
class DavMoveRequest < ExchangeRequest
|
6
|
+
METHOD = 'MOVE'
|
7
|
+
REQUEST_HAS_BODY = false
|
8
|
+
RESPONSE_HAS_BODY = false
|
9
|
+
|
10
|
+
def self.execute(credentials, source, destination, &b)
|
11
|
+
begin
|
12
|
+
options = {
|
13
|
+
:headers => {
|
14
|
+
'Destination' => destination
|
15
|
+
},
|
16
|
+
:path => source
|
17
|
+
}
|
18
|
+
|
19
|
+
response = super credentials, options
|
20
|
+
yield response if b
|
21
|
+
response
|
22
|
+
rescue RException => e
|
23
|
+
raise e
|
24
|
+
rescue Exception => e
|
25
|
+
raise RException.new(options[:request], response, e)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rexchange/exchange_request'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
# Used to move entities to different locations in the accessable mailbox.
|
5
|
+
class DavProppatchRequest < ExchangeRequest
|
6
|
+
METHOD = 'PROPPATCH'
|
7
|
+
|
8
|
+
|
9
|
+
def self.request_body(property, name_space, value)
|
10
|
+
<<-eos
|
11
|
+
<D:propertyupdate xmlns:D = "DAV:">
|
12
|
+
<D:set>
|
13
|
+
<D:prop>
|
14
|
+
<X:#{property} xmlns:X = "#{name_space}:">#{value}</X:read>
|
15
|
+
</D:prop>
|
16
|
+
</D:set>
|
17
|
+
</D:propertyupdate>
|
18
|
+
eos
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.execute(credentials, uri, property, name_space, value, &b)
|
22
|
+
begin
|
23
|
+
options = {}
|
24
|
+
options[:path] = uri
|
25
|
+
headers = options[:headers] ||= {}
|
26
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
27
|
+
options[:body] = request_body(property, name_space, value)
|
28
|
+
|
29
|
+
response = self.exchange_request(credentials, options)
|
30
|
+
case response
|
31
|
+
when Net::HTTPClientError then
|
32
|
+
self.authenticate(credentials)
|
33
|
+
#repeat exchange_request after authentication with the auth-cookies
|
34
|
+
headers = options[:headers] ||= {}
|
35
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
36
|
+
response = self.exchange_request(credentials, options)
|
37
|
+
end
|
38
|
+
yield response if b
|
39
|
+
response
|
40
|
+
rescue RException => e
|
41
|
+
raise e
|
42
|
+
rescue Exception => e
|
43
|
+
raise RException.new(options[:request], response, e)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
|
3
|
+
module RExchange
|
4
|
+
|
5
|
+
# Exchange Server's WebDAV interface is non-standard, so
|
6
|
+
# we create this simple wrapper to extend the 'net/http'
|
7
|
+
# library and add the request methods we need.
|
8
|
+
class ExchangeRequest < Net::HTTPRequest
|
9
|
+
REQUEST_HAS_BODY = true
|
10
|
+
RESPONSE_HAS_BODY = true
|
11
|
+
|
12
|
+
def self.authenticate(credentials)
|
13
|
+
owa_uri = credentials.owa_uri
|
14
|
+
if owa_uri
|
15
|
+
http = Net::HTTP.new(owa_uri.host, owa_uri.port)
|
16
|
+
http.set_debug_output(RExchange::DEBUG_STREAM) if RExchange::DEBUG_STREAM
|
17
|
+
req = Net::HTTP::Post.new(owa_uri.path)
|
18
|
+
destination = owa_uri.scheme+"://"+owa_uri.host+(owa_uri.port ? ':'+owa_uri.port.to_s : '')
|
19
|
+
req.body = "destination=#{destination}&username=#{credentials.user}&password=#{credentials.password}"
|
20
|
+
http.use_ssl = owa_uri.scheme ? (owa_uri.scheme.downcase == 'https') : false
|
21
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
22
|
+
res = http.request(req)
|
23
|
+
credentials.auth_cookie = res.header["set-cookie"].split(',').map(&:strip).map{|c| c.split(';')[0]}.reverse.join('; ') if res.header["set-cookie"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.exchange_request(credentials, options = {})
|
28
|
+
http = Net::HTTP.new(credentials.dav_uri.host, credentials.dav_uri.port)
|
29
|
+
http.set_debug_output(RExchange::DEBUG_STREAM) if RExchange::DEBUG_STREAM
|
30
|
+
request_path = options[:path] || credentials.dav_uri.path
|
31
|
+
req = self.new(request_path)
|
32
|
+
options[:request] = req
|
33
|
+
req.basic_auth credentials.user, credentials.password #if !credentials.is_owa?
|
34
|
+
req.content_type = 'text/xml'
|
35
|
+
req.add_field 'host', credentials.dav_uri.host
|
36
|
+
|
37
|
+
if options[:headers]
|
38
|
+
options[:headers].each_pair do |k, v|
|
39
|
+
req.add_field k, v
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
req.body = options[:body] if REQUEST_HAS_BODY
|
44
|
+
http.use_ssl = credentials.dav_use_ssl?
|
45
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
46
|
+
response = http.request(req) if RESPONSE_HAS_BODY
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.execute(credentials, options = {}, &b)
|
51
|
+
begin
|
52
|
+
headers = options[:headers] ||= {}
|
53
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
54
|
+
response = self.exchange_request(credentials, options)
|
55
|
+
case response
|
56
|
+
when Net::HTTPClientError then
|
57
|
+
self.authenticate(credentials)
|
58
|
+
#repeat exchange_request after authentication with the auth-cookies
|
59
|
+
headers = options[:headers] ||= {}
|
60
|
+
headers['Cookie'] = credentials.auth_cookie if credentials.auth_cookie
|
61
|
+
response = self.exchange_request(credentials, options)
|
62
|
+
end
|
63
|
+
|
64
|
+
raise 'NOT 2xx NOR 3xx: ' + response.inspect.to_s unless (Net::HTTPSuccess === response || Net::HTTPRedirection === response)
|
65
|
+
|
66
|
+
yield response if b
|
67
|
+
response
|
68
|
+
rescue RException => e
|
69
|
+
raise e
|
70
|
+
rescue Exception => e
|
71
|
+
#puts e.backtrace.map{|e| e.to_s}.join("\n")
|
72
|
+
raise RException.new(options[:request], response, e)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
# You can not instantiate an ExchangeRequest externally.
|
79
|
+
def initialize(*args)
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'net/https'
|
3
|
+
require 'rexchange/dav_search_request'
|
4
|
+
require 'rexchange/dav_mkcol_request'
|
5
|
+
require 'rexchange/message'
|
6
|
+
require 'rexchange/contact'
|
7
|
+
require 'rexchange/appointment'
|
8
|
+
require 'rexchange/note'
|
9
|
+
require 'rexchange/task'
|
10
|
+
require 'rexchange/message_href'
|
11
|
+
require 'uri'
|
12
|
+
|
13
|
+
module RExchange
|
14
|
+
|
15
|
+
class FolderNotFoundError < StandardError
|
16
|
+
end
|
17
|
+
|
18
|
+
class Folder
|
19
|
+
include REXML
|
20
|
+
|
21
|
+
attr_reader :credentials, :displayname
|
22
|
+
|
23
|
+
def initialize(credentials, parent, displayname, href, content_type)
|
24
|
+
@credentials, @parent, @href = credentials, parent, href
|
25
|
+
@content_type = CONTENT_TYPES[content_type] || Message
|
26
|
+
@displayname = displayname
|
27
|
+
end
|
28
|
+
|
29
|
+
# Used to access subfolders.
|
30
|
+
def method_missing(sym, *args)
|
31
|
+
|
32
|
+
if folders_hash.has_key?(sym.to_s)
|
33
|
+
folders_hash[sym.to_s]
|
34
|
+
else
|
35
|
+
puts folders_hash.keys.inspect
|
36
|
+
puts sym.to_s
|
37
|
+
raise FolderNotFoundError.new("#{sym} is not a subfolder of #{@displayname} - #{@href}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
include Enumerable
|
42
|
+
|
43
|
+
def message_hrefs(href_regex)
|
44
|
+
RExchange::MessageHref::find_message_hrefs(href_regex, @credentials, to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Iterate through each entry in this folder
|
48
|
+
def each
|
49
|
+
@content_type::find(@credentials, to_s).each do |item|
|
50
|
+
yield item
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Not Implemented!
|
55
|
+
def search(conditions = {})
|
56
|
+
raise NotImplementedError.new('Bad Touch!')
|
57
|
+
@content_type::find(@credentials, to_s, conditions)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return an Array of subfolders for this folder
|
61
|
+
def folders
|
62
|
+
@folders ||=
|
63
|
+
begin
|
64
|
+
request_body = <<-eos
|
65
|
+
<D:searchrequest xmlns:D = "DAV:">
|
66
|
+
<D:sql>
|
67
|
+
SELECT "DAV:displayname", "DAV:contentclass"
|
68
|
+
FROM SCOPE('shallow traversal of "#{@href}"')
|
69
|
+
WHERE "DAV:ishidden" = false
|
70
|
+
AND "DAV:isfolder" = true
|
71
|
+
</D:sql>
|
72
|
+
</D:searchrequest>
|
73
|
+
eos
|
74
|
+
folders = []
|
75
|
+
DavSearchRequest.execute(@credentials, :body => request_body) do |response|
|
76
|
+
|
77
|
+
|
78
|
+
# iterate through folders query and add a new Folder
|
79
|
+
# object for each, under a normalized name.
|
80
|
+
xpath_query = "/*/a:response[a:propstat/a:status/text() = 'HTTP/1.1 200 OK']"
|
81
|
+
Document.new(response.body).elements.each(xpath_query) do |m|
|
82
|
+
href = m.elements['a:href'].text
|
83
|
+
displayname = m.elements['a:propstat/a:prop/a:displayname'].text
|
84
|
+
contentclass = m.elements['a:propstat/a:prop/a:contentclass'].text
|
85
|
+
folders << Folder.new(@credentials, self, displayname, href, contentclass.split(':').last.sub(/folder$/, ''))
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
folders
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def folders_hash
|
95
|
+
@folders_hash ||= folders.inject({}){|memo, f| memo[f.displayname.normalize]=f; memo}
|
96
|
+
end
|
97
|
+
|
98
|
+
def make_subfolder(subfolder)
|
99
|
+
path = @href.ensure_ends_with("/") + subfolder.ensure_ends_with("/")
|
100
|
+
DavMkcolRequest.execute(@credentials, path)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return the absolute path to this folder (but not the full URI)
|
104
|
+
def to_s
|
105
|
+
@href
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'rexchange/dav_move_request'
|
4
|
+
require 'rexchange/dav_get_request'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module RExchange
|
8
|
+
|
9
|
+
class Folder
|
10
|
+
CONTENT_TYPES = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
class GenericItem
|
14
|
+
include REXML
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
attr_accessor :attributes
|
18
|
+
|
19
|
+
def initialize(session, dav_property_node)
|
20
|
+
@attributes = {}
|
21
|
+
@session = session
|
22
|
+
|
23
|
+
dav_property_node.elements.each do |element|
|
24
|
+
namespaced_name = element.namespace + element.name
|
25
|
+
|
26
|
+
if element.name =~ /date$/i || self.class::ATTRIBUTE_MAPPINGS.find { |k, v| v == namespaced_name && k.to_s =~ /\_(at|on)$/ }
|
27
|
+
@attributes[namespaced_name] = Time::parse(element.text) rescue element.text
|
28
|
+
else
|
29
|
+
@attributes[namespaced_name] = element.text
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set the default CONTENT_CLASS to the class name, and define a
|
35
|
+
# dynamic query method for the derived class.
|
36
|
+
def self.inherited(base)
|
37
|
+
base.const_set('CONTENT_CLASS', base.to_s.split('::').last.downcase)
|
38
|
+
|
39
|
+
def base.query(path)
|
40
|
+
<<-QBODY
|
41
|
+
SELECT
|
42
|
+
#{self::ATTRIBUTE_MAPPINGS.values.map { |f| '"' + f + '"' }.join(',')}
|
43
|
+
FROM SCOPE('shallow traversal of "#{path}"')
|
44
|
+
WHERE "DAV:ishidden" = false
|
45
|
+
AND "DAV:isfolder" = false
|
46
|
+
AND "DAV:contentclass" = 'urn:content-classes:#{self::CONTENT_CLASS}'
|
47
|
+
QBODY
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# This handy method is meant to be called from any inheriting
|
52
|
+
# classes. It is used to bind types of folders to particular
|
53
|
+
# Entity classes so that the folder knows what type it's
|
54
|
+
# enumerating. So for a "calendarfolder" you'd call:
|
55
|
+
# set_folder_type 'calendarfolder' # or just 'calendar'
|
56
|
+
def self.set_folder_type(dav_name)
|
57
|
+
Folder::CONTENT_TYPES[dav_name.sub(/folder$/, '')] = self
|
58
|
+
end
|
59
|
+
|
60
|
+
# --Normally Not Used--
|
61
|
+
# By default the CONTENT_CLASS is determined by the name
|
62
|
+
# of your class. So for the Appointment class the
|
63
|
+
# CONTENT_CLASS would be 'appointment'.
|
64
|
+
# If for some reason this convention doesn't suit you,
|
65
|
+
# you can use this method to set the appropriate value
|
66
|
+
# (which is used in queries).
|
67
|
+
# For example, the DAV:content-class for contacts is:
|
68
|
+
# 'urn:content-classes:person'
|
69
|
+
# Person doesn't strike me as the best name for our class though.
|
70
|
+
# Most people would refer to an entry in a Contacts folder as
|
71
|
+
# a Contact. So that's what we call our class, and we use this method
|
72
|
+
# to make sure everything still works as it should.
|
73
|
+
def self.set_content_class(dav_name)
|
74
|
+
verbosity, $VERBOSE = $VERBOSE, nil # disable warnings for the next operation
|
75
|
+
const_set('CONTENT_CLASS', dav_name)
|
76
|
+
$VERBOSE = verbosity # revert to the original verbosity
|
77
|
+
end
|
78
|
+
|
79
|
+
# Defines what attributes are used in queries, and
|
80
|
+
# what methods they map to in instances. You should
|
81
|
+
# pass a Hash of method_name symbols and namespaced-attribute-name pairs.
|
82
|
+
def self.attribute_mappings(mappings)
|
83
|
+
|
84
|
+
mappings.merge! :uid => 'DAV:uid',
|
85
|
+
:modified_at => 'DAV:getlastmodified',
|
86
|
+
:href => 'DAV:href'
|
87
|
+
|
88
|
+
const_set('ATTRIBUTE_MAPPINGS', mappings)
|
89
|
+
|
90
|
+
mappings.each_pair do |k, v|
|
91
|
+
|
92
|
+
define_method(k) do
|
93
|
+
@attributes[v]
|
94
|
+
end
|
95
|
+
|
96
|
+
define_method("#{k.to_s.sub(/\?$/, '')}=") do |value|
|
97
|
+
@attributes[v] = value
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Retrieve an Array of items (such as Contact, Message, etc)
|
104
|
+
def self.find(credentials, path, conditions = nil)
|
105
|
+
qbody = <<-QBODY
|
106
|
+
<D:searchrequest xmlns:D = "DAV:">
|
107
|
+
<D:sql>
|
108
|
+
#{conditions.nil? ? query(path) : search(path, conditions)}
|
109
|
+
</D:sql>
|
110
|
+
</D:searchrequest>
|
111
|
+
QBODY
|
112
|
+
items = []
|
113
|
+
DavSearchRequest.execute(credentials, :body => qbody) do |response|
|
114
|
+
|
115
|
+
xpath_query = "//a:propstat[a:status/text() = 'HTTP/1.1 200 OK']/a:prop"
|
116
|
+
|
117
|
+
Document.new(response.body).elements.each(xpath_query) do |m|
|
118
|
+
items << self.new(credentials, m)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
items
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|