lambder-rexchange 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,52 @@
1
+ -- 0.3.4:
2
+ * Updated the README so it jives with the current version
3
+ * Removed Folder::join since it's only used by Folder#to_s, and just used a simpler
4
+ solution in Folder#to_s instead
5
+
6
+ -- 0.3.3:
7
+ * Folder objects now default to using the Message class for enumeration
8
+ if no class is associated with the content_type of the Folder.
9
+
10
+ -- 0.3.2:
11
+ * Quick hack to make Message#move_to work
12
+
13
+ -- 0.3.1:
14
+ * Disabled a warning about overwriting GenericItem::CONTENT_CLASS
15
+ * Added a "rescue" for Time::parse failures that reverts to the original element text
16
+
17
+ -- 0.3.0:
18
+ * Added support for Appointments
19
+ * Options hash for Session creation is gone. The new form is: "RExchange::Session.new(url, username, password)"
20
+ * Removed GenericItem#[], GenericItem#method_missing (attributes are explicit now)
21
+ * Removed Folder#get_messages, #get_contacts and #messages_in. Folders are now Enumerables using Enumerable#entries, #each, etc
22
+ to retrieve entries according to the folder's content-class. So a 'mailfolder' will retrieve Message instances,
23
+ a 'calendarfolder' will retrieve Appointment instances, and so on.
24
+
25
+ -- 0.2.0:
26
+ * Fixed a bug in Message#move_to when passing folders
27
+ * Added support for Contact browsing
28
+ * Added GenericItem to abstract Contacts and Messages
29
+ * Renamed Folder#messages to Folder#get_messages to avoid naming collisions
30
+ * Modified attributes in GenericItem that end with "date" to be Time::parse'd
31
+ * Added "r_exchange.rb" for Rails auto-require compatibility
32
+ * Now calling String#normalize on attribute names instead of just String#tr('-', '_')
33
+
34
+ -- 0.1.4:
35
+ * Moved Folder::normalize_folder_name to String#normalize, and added support for MixedCase sources.
36
+ * Fixed a bug where the normalized folder name was being used for the Folder#to_s result.
37
+ * Moved the meat of Folder#messages to Message::find
38
+ * Added Message#to_s
39
+ * Modified Folder#get_folders to return a hash instead of an array, providing both the original, and normalized names.
40
+
41
+ -- 0.1.3: Bugfixes
42
+ * Fixed several nasty bugs.
43
+
44
+ -- 0.1.2: Minor documentation/cleanup
45
+ * Removed a double-dash in the README that was apparently causing some viewing errors
46
+ * Changed Session initialize to a ternary condition
47
+ * Added RExchange::Message#has_attachments?
48
+ * Added documentation for most methods in RExchange::Message and RExchange::Folder
49
+
50
+ -- 0.1.1
51
+ * Fleshed out the README
52
+ * Copied the RAKEFILE from the Rools project
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2006 Samuel Smoot
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/RAKEFILE ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'rake/rdoctask'
7
+ require 'rake/gempackagetask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'pscp'
10
+
11
+ PACKAGE_VERSION = '0.3.4'
12
+
13
+ PACKAGE_FILES = FileList[
14
+ 'README',
15
+ 'CHANGELOG',
16
+ 'RAKEFILE',
17
+ 'MIT-LICENSE',
18
+ 'lib/**/*.rb',
19
+ 'test/*.rb'
20
+ ].to_a
21
+
22
+ PROJECT = 'rexchange'
23
+
24
+ ENV['RUBYFORGE_USER'] = "ssmoot@rubyforge.org"
25
+ ENV['RUBYFORGE_PROJECT'] = "/var/www/gforge-projects/#{PROJECT}"
26
+
27
+ task :default => [:rdoc]
28
+
29
+ desc 'Generate Documentation'
30
+ rd = Rake::RDocTask.new do |rdoc|
31
+ rdoc.rdoc_dir = 'doc'
32
+ rdoc.title = "RExchange -- A simple wrapper around Microsoft Exchange Server's WebDAV API"
33
+ rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
34
+ rdoc.rdoc_files.include(PACKAGE_FILES)
35
+ end
36
+
37
+ gem_spec = Gem::Specification.new do |s|
38
+ s.platform = Gem::Platform::RUBY
39
+ s.name = PROJECT
40
+ s.summary = "A simple wrapper around Microsoft Exchange Server's WebDAV API"
41
+ s.description = "Connect, browse, and iterate through folders and messages on an Exchange Server"
42
+ s.version = PACKAGE_VERSION
43
+
44
+ s.authors = 'Sam Smoot', 'Scott Bauer'
45
+ s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com'
46
+ s.rubyforge_project = PROJECT
47
+ s.homepage = 'http://substantiality.net'
48
+
49
+ s.files = PACKAGE_FILES
50
+
51
+ s.require_path = 'lib'
52
+ s.requirements << 'none'
53
+ s.autorequire = 'rexchange'
54
+
55
+ s.has_rdoc = true
56
+ s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
57
+ s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
58
+ end
59
+
60
+ Rake::GemPackageTask.new(gem_spec) do |p|
61
+ p.gem_spec = gem_spec
62
+ p.need_tar = true
63
+ p.need_zip = true
64
+ end
65
+
66
+ desc "Publish RDOC to RubyForge"
67
+ task :rubyforge => [:rdoc, :gem] do
68
+ Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'doc').upload
69
+ end
70
+
71
+ Rake::TestTask.new do |t|
72
+ t.libs << "test"
73
+ t.test_files = FileList['test/*.rb']
74
+ t.verbose = true
75
+ end
data/README ADDED
@@ -0,0 +1,86 @@
1
+ = RExchange
2
+
3
+ RExchange is a pure ruby wrapper for the Microsoft Exchange Server WebDAV API
4
+
5
+ == Things you should know
6
+
7
+ * Requires Ruby 1.8.4 (or later, for the extended WebDAV support in the net/http library)
8
+ * RExchange is cross-platform compatible, being written in pure Ruby
9
+ * Kiwi fruits are packed with vitamins
10
+
11
+ == Why should you use RExchange
12
+
13
+ * It makes interacting with Exchange simple
14
+ * It was written for a real application, and does real work reliably day in and day out
15
+
16
+ == Example
17
+
18
+ # We pass our uri (pointing directly to a mailbox), a username, and a password to RExchange::open
19
+ # to create a RExchange::Session. Notice that we escape the "\" in our username.
20
+ RExchange::open('https://example.com/exchange/admin/', 'mydomain\\admin', 'secret') do |mailbox|
21
+
22
+ # The block parameter ("mailbox" in this case) is actually the Session itself.
23
+ # You can refer to folders by chaining them as method calls. "inbox" in this case
24
+ # isn't a defined method for Session, but part of the DSL to refer to folder names.
25
+ # Each folder name returns a RExchange::Folder. The folder is Enumerable, allowing
26
+ # iteration over the items in the folder, depending on the DAV:content-class of
27
+ # the folder.
28
+ mailbox.inbox.each do |message|
29
+
30
+ # The "message" block parameter is a RExchange::Message object. You have access to
31
+ # several attributes of the message, including: href, from, to, message_id, date,
32
+ # importance, has_attachment? and body.
33
+ p message.subject
34
+
35
+ # The RExchange::Message#move_to method moves the message to another folder, in this
36
+ # case, an "archive" folder off of the inbox.
37
+ message.move_to mailbox.inbox.archive
38
+ end
39
+
40
+ # Folder names are "normalized", replacing dashes and spaces with underscores,
41
+ # squeezing out multiple underscores in a row, and downcasing the whole thing.
42
+ # So a folder name such as "My Very-long Folder Name" would look like:
43
+ mailbox.my_very_long_folder_name
44
+
45
+ # The Enumerable mixin gives us an Folder#entries method in case we just want to
46
+ # return an array.
47
+ messages = mailbox.inbox.entries
48
+
49
+ # Other methods, like Enumerable#map and Enumerable#sort_by let us manipulate
50
+ # the Folder in fun ways:
51
+ puts mailbox.contacts.sort_by { |contact| contact.first_name }.map do |contact|
52
+ <<-CONTACT
53
+ Name: #{contact.first_name} #{contact.last_name}
54
+ Email: #{contact.email}
55
+ Phone: #{contact.phone}
56
+ CONTACT
57
+ end
58
+
59
+ end
60
+
61
+ # We can access the Global Address Book by accessing the "/public" folder:
62
+ RExchange::open('https://example.com/public', 'mydomain\\somebody', 'secret') do |mailbox|
63
+
64
+ puts mailbox.company_contacts.each do |contact|
65
+ <<-CONTACT
66
+ Company: #{contact.company}
67
+ Name: #{contact.first_name} #{contact.last_name}
68
+ Email: #{contact.email}
69
+ Phone: #{contact.phone}
70
+ CONTACT
71
+ end
72
+
73
+ end
74
+
75
+ == Caveats
76
+
77
+ There are several features missing (simply because we didn't need them yet). Among them:
78
+
79
+ * The ability to delete messages or folders
80
+ * The ability to create folders
81
+ * There's no mechanism for sending new messages, or replying or forwarding existing ones
82
+ * There are a lot more message attributes we're not retrieving since they weren't useful to us, but they might be to you
83
+ * Exporting an email or entire folder tree to offline storage
84
+ * And much much more!
85
+
86
+ If you'd like to see any of these features, or have some ideas of your own you'd like to see implemented don't hesitate to let us know, and if it strikes our fancy maybe you'll get some free programming!
data/lib/r_exchange.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Provided for compatibility with RubyOnRails auto-require functionality.
2
+ require 'rexchange'
data/lib/rexchange.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'rexchange/session'
2
+ require 'rexchange/r_exception'
3
+
4
+ class String
5
+ def ends_with?(partial)
6
+ self[self.size - partial.size..self.size] == partial
7
+ end
8
+
9
+ def ensure_ends_with(partial)
10
+ self.ends_with?(partial) ? self : self + partial
11
+ end
12
+
13
+ def normalize
14
+ self.split(/(?=[A-Z][a-z]*)/).join('_').tr('- ', '_').squeeze('_').downcase
15
+ end
16
+ end
17
+
18
+ module RExchange
19
+ # Use STDOUT or another stream if you'd like to capture the HTTP debug output
20
+ DEBUG_STREAM = $log
21
+
22
+ PR_HTTPMAIL_READ = 'read'
23
+ NS_HTTPMAIL = 'urn:schemas:httpmail'
24
+
25
+ # A shortcut to RExchange::Session#new
26
+ def self.open(dav_uri, owa_uri, username = nil, password = nil)
27
+ session = RExchange::Session.new(dav_uri, owa_uri, username, password)
28
+
29
+ yield session if block_given?
30
+ return session
31
+ end
32
+
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'rexchange/generic_item'
2
+
3
+ module RExchange
4
+ class Appointment < GenericItem
5
+
6
+ set_folder_type 'calendar'
7
+
8
+ attribute_mappings :all_day_event => 'urn:schemas:calendar:alldayevent',
9
+ :busy_status => 'urn:schemas:calendar:busystatus',
10
+ :contact => 'urn:schemas:calendar:contact',
11
+ :contact_url => 'urn:schemas:calendar:contacturl',
12
+ :created_on => 'urn:schemas:calendar:created',
13
+ :description_url => 'urn:schemas:calendar:descriptionurl',
14
+ :end_at => 'urn:schemas:calendar:dtend',
15
+ :created_at => 'urn:schemas:calendar:dtstamp',
16
+ :start_at => 'urn:schemas:calendar:dtstart',
17
+ :duration => 'urn:schemas:calendar:duration',
18
+ :expires_on => 'urn:schemas:calendar:exdate',
19
+ :expiry_rule => 'urn:schemas:calendar:exrule',
20
+ :has_attachment? => 'urn:schemas:httpmail:hasattachment',
21
+ :html => 'urn:schemas:httpmail:htmldescription',
22
+ :modified_on => 'urn:schemas:calendar:lastmodified',
23
+ :location => 'urn:schemas:calendar:location',
24
+ :location_url => 'urn:schemas:calendar:locationurl',
25
+ :meeting_status => 'urn:schemas:calendar:meetingstatus',
26
+ :normalized_subject => 'urn:schemas:httpmail:normalizedsubject',
27
+ :priority => 'urn:schemas:httpmail:priority',
28
+ :recurres_on => 'urn:schemas:calendar:rdate',
29
+ :reminder_offset => 'urn:schemas:calendar:reminderoffset',
30
+ :reply_time => 'urn:schemas:calendar:replytime',
31
+ :sequence => 'urn:schemas:calendar:sequence',
32
+ :subject => 'urn:schemas:httpmail:subject',
33
+ :body => 'urn:schemas:httpmail:textdescription',
34
+ :timezone => 'urn:schemas:calendar:timezone',
35
+ :uid => 'urn:schemas:calendar:uid'
36
+
37
+ end
38
+ end
@@ -0,0 +1,29 @@
1
+ require 'rexchange/generic_item'
2
+
3
+ module RExchange
4
+ class Contact < GenericItem
5
+
6
+ set_folder_type 'contact'
7
+ set_content_class 'person'
8
+
9
+ attribute_mappings :first_name => 'urn:schemas:contacts:givenName',
10
+ :middle_name => 'urn:schemas:contacts:middlename',
11
+ :last_name => 'urn:schemas:contacts:sn',
12
+ :title => 'urn:schemas:contacts:title',
13
+ :created_at => 'DAV:creationdate',
14
+ :address => 'urn:schemas:contacts:mailingstreet',
15
+ :city => 'urn:schemas:contacts:mailingcity',
16
+ :state => 'urn:schemas:contacts:st',
17
+ :zip_code => 'urn:schemas:contacts:mailingpostalcode',
18
+ :country => 'urn:schemas:contacts:co',
19
+ :phone => 'urn:schemas:contacts:homePhone',
20
+ :business_phone => 'urn:schemas:contacts:telephoneNumber',
21
+ :fax => 'urn:schemas:contacts:facsimiletelephonenumber',
22
+ :mobile => 'urn:schemas:contacts:mobile',
23
+ :email => 'urn:schemas:contacts:email1',
24
+ :website => 'urn:schemas:contacts:businesshomepage',
25
+ :company => 'urn:schemas:contacts:o',
26
+ :notes => 'urn:schemas:httpmail:textdescription'
27
+
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ require 'uri'
2
+
3
+ module RExchange
4
+
5
+ # Credentials are passed around between Folders to emulate a stateful
6
+ # connection with the RExchange::Session
7
+ class Credentials
8
+ attr_reader :user, :password, :dav_uri, :owa_uri
9
+ attr_accessor :auth_cookie
10
+
11
+ # You must pass a dav_uri, owa_uri, a username and a password
12
+ def initialize(dav_uri = nil, owa_uri = nil, username = nil, password = nil)
13
+ owa_uri = nil if owa_uri && owa_uri.blank?
14
+ @dav_uri = URI.parse(dav_uri) if dav_uri
15
+ @owa_uri = URI.parse(owa_uri) if owa_uri
16
+ @dav_use_ssl = @dav_uri.scheme ? (@dav_uri.scheme.downcase == 'https') : false
17
+ @user = username || (@dav_uri && @dav_uri.userinfo ? @dav_uri.userinfo.split(':')[0] : nil) || (@owa_uri && @owa_uri.userinfo ? @owa_uri.userinfo.split(':')[0] : nil)
18
+ @password = password || (@dav_uri && @dav_uri.userinfo ? @dav_uri.userinfo.split(':')[1] : nil) || (@owa_uri && @owa_uri.userinfo ? @owa_uri.userinfo.split(':')[1] : nil)
19
+ @dav_port = (@dav_uri.port || @dav_uri.default_port) if @dav_uri
20
+ @owa_port = (@owa_uri.port || @owa_uri.default_port) if @owa_uri
21
+ if block_given?
22
+ yield self
23
+ else
24
+ return self
25
+ end
26
+ end
27
+
28
+ def dav_use_ssl?
29
+ @dav_use_ssl
30
+ end
31
+
32
+ def is_owa?
33
+ @owa_uri && @owa_uri.scheme && @owa_uri.host
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -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,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,8 @@
1
+ require 'rexchange/exchange_request'
2
+
3
+ module RExchange
4
+ # Used for arbitrary SEARCH requests
5
+ class DavSearchRequest < ExchangeRequest
6
+ METHOD = 'SEARCH'
7
+ end
8
+ 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,102 @@
1
+ require 'rexml/document'
2
+ require 'net/https'
3
+ require 'rexchange/dav_search_request'
4
+ require 'rexchange/message'
5
+ require 'rexchange/contact'
6
+ require 'rexchange/appointment'
7
+ require 'rexchange/note'
8
+ require 'rexchange/task'
9
+ require 'rexchange/message_href'
10
+ require 'uri'
11
+
12
+ module RExchange
13
+
14
+ class FolderNotFoundError < StandardError
15
+ end
16
+
17
+ class Folder
18
+ include REXML
19
+
20
+ attr_reader :credentials, :displayname
21
+
22
+ def initialize(credentials, parent, displayname, href, content_type)
23
+ @credentials, @parent, @href = credentials, parent, href
24
+ @content_type = CONTENT_TYPES[content_type] || Message
25
+ @displayname = displayname
26
+ end
27
+
28
+ # Used to access subfolders.
29
+ def method_missing(sym, *args)
30
+
31
+ if folders_hash.has_key?(sym.to_s)
32
+ folders_hash[sym.to_s]
33
+ else
34
+ puts folders_hash.keys.inspect
35
+ puts sym.to_s
36
+ raise FolderNotFoundError.new("#{sym} is not a subfolder of #{@displayname} - #{@href}")
37
+ end
38
+ end
39
+
40
+ include Enumerable
41
+
42
+ def message_hrefs(href_regex)
43
+ RExchange::MessageHref::find_message_hrefs(href_regex, @credentials, to_s)
44
+ end
45
+
46
+ # Iterate through each entry in this folder
47
+ def each
48
+ @content_type::find(@credentials, to_s).each do |item|
49
+ yield item
50
+ end
51
+ end
52
+
53
+ # Not Implemented!
54
+ def search(conditions = {})
55
+ raise NotImplementedError.new('Bad Touch!')
56
+ @content_type::find(@credentials, to_s, conditions)
57
+ end
58
+
59
+ # Return an Array of subfolders for this folder
60
+ def folders
61
+ @folders ||=
62
+ begin
63
+ request_body = <<-eos
64
+ <D:searchrequest xmlns:D = "DAV:">
65
+ <D:sql>
66
+ SELECT "DAV:displayname", "DAV:contentclass"
67
+ FROM SCOPE('shallow traversal of "#{@href}"')
68
+ WHERE "DAV:ishidden" = false
69
+ AND "DAV:isfolder" = true
70
+ </D:sql>
71
+ </D:searchrequest>
72
+ eos
73
+ folders = []
74
+ DavSearchRequest.execute(@credentials, :body => request_body) do |response|
75
+
76
+
77
+ # iterate through folders query and add a new Folder
78
+ # object for each, under a normalized name.
79
+ xpath_query = "/*/a:response[a:propstat/a:status/text() = 'HTTP/1.1 200 OK']"
80
+ Document.new(response.body).elements.each(xpath_query) do |m|
81
+ href = m.elements['a:href'].text
82
+ displayname = m.elements['a:propstat/a:prop/a:displayname'].text
83
+ contentclass = m.elements['a:propstat/a:prop/a:contentclass'].text
84
+ folders << Folder.new(@credentials, self, displayname, href, contentclass.split(':').last.sub(/folder$/, ''))
85
+ end
86
+
87
+ end
88
+ folders
89
+ end
90
+
91
+ end
92
+
93
+ def folders_hash
94
+ @folders_hash ||= folders.inject({}){|memo, f| memo[f.displayname.normalize]=f; memo}
95
+ end
96
+
97
+ # Return the absolute path to this folder (but not the full URI)
98
+ def to_s
99
+ @href
100
+ end
101
+ end
102
+ 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
@@ -0,0 +1,88 @@
1
+ require 'rexchange/generic_item'
2
+ require 'rexchange/dav_proppatch_request'
3
+
4
+ module RExchange
5
+ class Message < GenericItem
6
+
7
+ set_folder_type 'mail'
8
+
9
+ attribute_mappings :from => 'urn:schemas:httpmail:from',
10
+ :to => 'urn:schemas:httpmail:to',
11
+ :message_id => 'urn:schemas:mailheader:message-id',
12
+ :subject => 'urn:schemas:httpmail:subject',
13
+ :recieved_on => 'urn:schemas:httpmail:date',
14
+ :importance => 'urn:schemas:httpmail:importance',
15
+ :has_attachments? => 'urn:schemas:httpmail:hasattachment',
16
+ :body => 'urn:schemas:httpmail:textdescription',
17
+ :html => 'urn:schemas:httpmail:htmldescription'
18
+
19
+
20
+ # Move this message to the specified folder.
21
+ # The folder can be a string such as 'inbox/archive' or a RExchange::Folder.
22
+ # === Example
23
+ # mailbox.inbox.each do |message|
24
+ # message.move_to mailbox.inbox.archive
25
+ # end
26
+ def move_to(folder)
27
+ destination =
28
+ if folder.is_a?(RExchange::Folder)
29
+ folder.to_s.ensure_ends_with('/') + self.href.split('/').last
30
+ else
31
+ @session.uri.path.ensure_ends_with('/') + folder.to_s.ensure_ends_with('/') + self.href.split('/').last
32
+ end
33
+
34
+ DavMoveRequest.execute(@session, self.href, destination)
35
+ end
36
+
37
+ # Delete this message.
38
+ # === Example
39
+ # mailbox.inbox.each do |message|
40
+ # message.delete!
41
+ # end
42
+ def delete!
43
+ response = DavDeleteRequest.execute(@session, self.href)
44
+ case response.code
45
+ when 204 then
46
+ # Standard success response. ( http://msdn.microsoft.com/en-us/library/aa142839(EXCHG.65).aspx )
47
+ true
48
+ when 423 then
49
+ # The destination resource is locked.
50
+ false
51
+ end
52
+ end
53
+
54
+
55
+ def to_s
56
+ "To: #{to}, From: #{from}, Subject: #{subject}"
57
+ end
58
+
59
+ def raw
60
+ fetch(href, limit = 10)
61
+ end
62
+
63
+ def fetch(uri_str, limit = 10)
64
+ # You should choose better exception.
65
+ raise ArgumentError, 'HTTP redirect too deep' if limit == 0
66
+ response = DavGetRequest.execute(@session, uri_str)
67
+ case response
68
+ when Net::HTTPSuccess then
69
+ response.body
70
+ when Net::HTTPRedirection then
71
+ fetch(response['location'], limit - 1)
72
+ else
73
+ response.error!
74
+ end
75
+ end
76
+
77
+ def mark_as_read
78
+ response = DavProppatchRequest.execute(@session, self.href, RExchange::PR_HTTPMAIL_READ, RExchange::NS_HTTPMAIL, 1)
79
+ case response
80
+ when Net::HTTPSuccess then
81
+ true
82
+ else
83
+ false
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,16 @@
1
+ require 'rexchange/generic_item'
2
+
3
+ module RExchange
4
+ class Note < GenericItem
5
+
6
+ set_folder_type 'note'
7
+
8
+ attribute_mappings :displayname => 'DAV:displayname',
9
+ :created_at => 'DAV:creationdate',
10
+ :subject =>'urn:schemas:httpmail:subject',
11
+ :body => 'urn:schemas:httpmail:textdescription'
12
+
13
+ end
14
+
15
+
16
+ end
@@ -0,0 +1,28 @@
1
+ require 'uri'
2
+ require 'rexchange/folder'
3
+ require 'rexchange/credentials'
4
+
5
+ module RExchange
6
+
7
+ class Session < Folder
8
+
9
+ # Creates a Credentials instance to pass to subfolders
10
+ # === Example
11
+ # RExchange::Session.new('https://mydomain.com/exchange/demo', 'https://mydomain.com/owa/auth/owaauth.dll', 'mydomain\\bob', 'secret') do |mailbox|
12
+ # mailbox.test.each do |message|
13
+ # puts message.subject
14
+ # end
15
+ # end
16
+ def initialize(dav_uri, owa_uri, username = nil, password = nil)
17
+
18
+ @credentials = Credentials.new(dav_uri, owa_uri, username, password)
19
+ @parent = @credentials.dav_uri.path
20
+ @folder = ''
21
+ @href = @credentials.dav_uri.to_s
22
+
23
+ yield(self) if block_given?
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,26 @@
1
+ require 'rexchange/generic_item'
2
+
3
+ module RExchange
4
+ class Task < GenericItem
5
+
6
+ set_folder_type 'task'
7
+
8
+ attribute_mappings :displayname => 'DAV:displayname',
9
+ :created_at => 'DAV:creationdate',
10
+ :subject =>'urn:schemas:httpmail:subject',
11
+ :body => 'urn:schemas:httpmail:textdescription',
12
+ :percent_complete =>'http://schemas.microsoft.com/exchange/tasks/percentcomplete',
13
+ :owner =>'http://schemas.microsoft.com/exchange/tasks/owner',
14
+ :is_complete =>'http://schemas.microsoft.com/exchange/tasks/is_complete',
15
+ :date_start =>'http://schemas.microsoft.com/exchange/tasks/dtstart',
16
+ :date_due =>'http://schemas.microsoft.com/exchange/tasks/dtdue',
17
+ :actual_effort =>'http://schemas.microsoft.com/exchange/tasks/actualeffort',
18
+ :estimated_effort =>'http://schemas.microsoft.com/exchange/tasks/estimatedeffort',
19
+ :priority =>'http://schemas.microsoft.com/mapi/priority',
20
+ :status =>'http://schemas.microsoft.com/exchange/tasks/status',
21
+ :state =>'http://schemas.microsoft.com/exchange/tasks/state'
22
+
23
+ end
24
+
25
+
26
+ end
@@ -0,0 +1,23 @@
1
+ require 'test/unit'
2
+ require 'rexchange'
3
+
4
+ class FunctionalTests < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @mailbox = RExchange::Session.new 'url', 'username', 'password'
8
+ end
9
+
10
+ def teardown
11
+ @mailbox = nil
12
+ end
13
+
14
+ # Ok, so it's not a real test, but I needed to get started,
15
+ # and get rid of console scripts.
16
+ def test_no_exceptions
17
+
18
+ @mailbox.inbox.search(:from => 'scott').each do |m|
19
+ puts m.subject
20
+ end
21
+
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lambder-rexchange
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Sam Smoot
8
+ - Scott Bauer
9
+ - Daniel Kwiecinski
10
+ autorequire: rexchange
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-08-27 00:00:00 -07:00
15
+ default_executable:
16
+ dependencies: []
17
+
18
+ description: Connect, browse, and iterate through folders and messages on an Exchange Server
19
+ email: ssmoot@gmail.com; bauer.mail@gmail.com; daniel@lambder.com
20
+ executables: []
21
+
22
+ extensions: []
23
+
24
+ extra_rdoc_files:
25
+ - README
26
+ - CHANGELOG
27
+ - RAKEFILE
28
+ - MIT-LICENSE
29
+ files:
30
+ - README
31
+ - CHANGELOG
32
+ - RAKEFILE
33
+ - MIT-LICENSE
34
+ - lib/r_exchange.rb
35
+ - lib/rexchange/appointment.rb
36
+ - lib/rexchange/contact.rb
37
+ - lib/rexchange/credentials.rb
38
+ - lib/rexchange/dav_get_request.rb
39
+ - lib/rexchange/dav_move_request.rb
40
+ - lib/rexchange/dav_search_request.rb
41
+ - lib/rexchange/exchange_request.rb
42
+ - lib/rexchange/folder.rb
43
+ - lib/rexchange/generic_item.rb
44
+ - lib/rexchange/message.rb
45
+ - lib/rexchange/note.rb
46
+ - lib/rexchange/session.rb
47
+ - lib/rexchange/task.rb
48
+ - lib/rexchange.rb
49
+ - test/functional.rb
50
+ has_rdoc: true
51
+ homepage: http://substantiality.net
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --line-numbers
55
+ - --inline-source
56
+ - --main
57
+ - README
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements:
73
+ - none
74
+ rubyforge_project: rexchange
75
+ rubygems_version: 1.2.0
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: A simple wrapper around Microsoft Exchange Server's WebDAV API (that works with Exchange 2007 using basic authentication and the form-based one.
79
+ test_files: []
80
+