rexchange 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ -- 1.1
2
+ * Fleshed out the README
3
+ * Copied the RAKEFILE from the Rools project
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+ require 'rake/rdoctask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/contrib/rubyforgepublisher'
8
+ # require 'pscp'
9
+
10
+ PACKAGE_VERSION = '0.1.1'
11
+
12
+ PACKAGE_FILES = FileList[
13
+ 'README',
14
+ 'CHANGELOG',
15
+ 'RAKEFILE',
16
+ 'lib/**/*.rb',
17
+ 'test/*.rb'
18
+ ].to_a
19
+
20
+ PROJECT = 'rexchange'
21
+
22
+ ENV['RUBYFORGE_USER'] = "ssmoot@rubyforge.org"
23
+ ENV['RUBYFORGE_PROJECT'] = "/var/www/gforge-projects/#{PROJECT}"
24
+
25
+ task :default => [:rdoc]
26
+
27
+ desc 'Generate Documentation'
28
+ rd = Rake::RDocTask.new do |rdoc|
29
+ rdoc.rdoc_dir = 'doc'
30
+ rdoc.title = "RExchange -- A simple wrapper around Microsoft Exchange Server's WebDAV API"
31
+ rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README'
32
+ rdoc.rdoc_files.include(PACKAGE_FILES)
33
+ end
34
+
35
+ gem_spec = Gem::Specification.new do |s|
36
+ s.platform = Gem::Platform::RUBY
37
+ s.name = PROJECT
38
+ s.summary = "A simple wrapper around Microsoft Exchange Server's WebDAV API"
39
+ s.description = "Connect, browse, and iterate through folders and messages on an Exchange Server"
40
+ s.version = PACKAGE_VERSION
41
+
42
+ s.authors = 'Sam Smoot', 'Scott Bauer'
43
+ s.email = 'ssmoot@gmail.com; bauer.mail@gmail.com'
44
+ s.rubyforge_project = PROJECT
45
+ s.homepage = 'http://substantiality.net'
46
+
47
+ s.files = PACKAGE_FILES
48
+
49
+ s.require_path = 'lib'
50
+ s.requirements << 'none'
51
+ s.autorequire = 'rexchange'
52
+
53
+ s.has_rdoc = true
54
+ s.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README'
55
+ s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a
56
+ end
57
+
58
+ Rake::GemPackageTask.new(gem_spec) do |p|
59
+ p.gem_spec = gem_spec
60
+ p.need_tar = true
61
+ p.need_zip = true
62
+ end
63
+
64
+ desc "Publish RDOC to RubyForge"
65
+ task :rubyforge => [:rdoc, :gem] do
66
+ Rake::SshDirPublisher.new(ENV['RUBYFORGE_USER'], ENV['RUBYFORGE_PROJECT'], 'doc').upload
67
+ end
data/README ADDED
@@ -0,0 +1,76 @@
1
+ = RExchange -- A pure ruby wrapper for the Microsoft Exchange Server WebDAV API
2
+
3
+ == Things you should know
4
+
5
+ * Requires Ruby 1.8.4 (for the extended WebDAV support in the net/http library)
6
+ * RExchange is cross-platform compatible, being written in pure Ruby
7
+ * Kiwi fruits are packed with vitamins
8
+
9
+ == Why should you use RExchange
10
+
11
+ * It makes interacting with Exchange simple
12
+ * It was written for a real application, and does real work reliably day in and day out
13
+
14
+ == Example
15
+
16
+ uri = 'https://example.com/exchange/admin/'
17
+ options = { :user => 'mydomain\admin', :password => 'random' }
18
+
19
+ # We pass our uri (pointing directly to a mailbox), and options hash to RExchange::open
20
+ # to create a RExchange::Session.
21
+ RExchange::open(uri, options) do |mailbox|
22
+
23
+ # The block parameter ("mailbox" in this case) is actually the Session itself.
24
+ # You can refer to folders by chaining them as method calls. "inbox" in this case
25
+ # isn't a defined method for Session, but part of the DSL to refer to folder names.
26
+ # Each folder name returns a RExchange::Folder. The folder is Enumerable, allowing
27
+ # iteration over the messages in 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, hasattachment 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 '/inbox/archive'
38
+ end
39
+
40
+ # You can also call the RExchange::Folder#messages method if you find calling "each"
41
+ # on a folder directly a little obscure.
42
+ mailbox.inbox.archive.messages.each do |m|
43
+
44
+ # Our previous message should show up in here now.
45
+ p m.subject
46
+ end
47
+
48
+ # The RExchange::Folder#message_in method is less expensive than the DSL folder-chaining
49
+ # methods. Since the folder-chaining is cached in the Session it's a small hit, and some
50
+ # may prefer the readability of them, but if you're looking for absolute performance
51
+ # or have really deep folder structures then this may be the method for you.
52
+ mailbox.messages_in '/inbox/' do |m|
53
+
54
+ # Since we moved all our messages to the archive earlier, this shouldn't display
55
+ # anything.
56
+ p m.from
57
+ end
58
+
59
+ # Folder names are "normalized", replacing dashes and spaces with underscores,
60
+ # squeezing out multiple underscores in a row, and downcasing the whole thing.
61
+ # So a folder name such as "My Very-long Folder Name" would look like:
62
+ mailbox.my_very_long_folder_name
63
+ end
64
+
65
+ == Caveats
66
+
67
+ There are several features missing (simply because we didn't need them yet). Among them:
68
+
69
+ * The ability to delete messages or folders
70
+ * The ability to create folders
71
+ * There's no mechanism for sending new messages, or replying or forwarding existing ones
72
+ * There are a lot more message attributes we're not retrieving since they weren't useful to us, but they might be to you
73
+ * Exporting an email or entire folder tree to offline storage
74
+ * And much much more!
75
+
76
+ 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!
@@ -0,0 +1,11 @@
1
+ require 'rexchange/session'
2
+
3
+ module RExchange
4
+ # Use STDOUT or another stream if you'd like to capture the HTTP debug output
5
+ DEBUG_STREAM = nil
6
+
7
+ # A shortcut to RExchange::Session#new's block syntax
8
+ def self.open(uri, options = {})
9
+ yield RExchange::Session.new(uri, options)
10
+ end
11
+ end
@@ -0,0 +1,27 @@
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, :uri, :use_ssl
9
+
10
+ # You must pass a uri, and an options hash containing :user and :password
11
+ def initialize(uri, options = {})
12
+ @uri = URI.parse(uri)
13
+ @use_ssl = (@uri.scheme.downcase == 'https')
14
+ @user = @uri.userinfo ? @user.userinfo.split(':')[0] : options.delete(:user)
15
+ @password = @uri.userinfo ? @user.userinfo.split(':')[1] : options.delete(:password)
16
+ @port = @uri.port || @uri.default_port
17
+
18
+ if block_given?
19
+ yield self
20
+ else
21
+ return self
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,22 @@
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)
11
+ options = {
12
+ :headers => {
13
+ 'Destination' => destination
14
+ },
15
+ :path => source
16
+ }
17
+
18
+ super credentials, options
19
+ end
20
+ end
21
+
22
+ 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,40 @@
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.execute(credentials, options = {})
13
+ http = Net::HTTP.new(credentials.uri.host, credentials.uri.port)
14
+ http.set_debug_output(RExchange::DEBUG_STREAM) if RExchange::DEBUG_STREAM
15
+ request_path = options[:path] || credentials.uri.path
16
+ req = self.new(request_path)
17
+ req.basic_auth credentials.user, credentials.password
18
+ req.content_type = 'text/xml'
19
+ req.add_field 'host', credentials.uri.host
20
+
21
+ if options[:headers]
22
+ options[:headers].each_pair do |k, v|
23
+ req.add_field k, v
24
+ end
25
+ end
26
+
27
+ req.body = options[:body] if REQUEST_HAS_BODY
28
+ http.use_ssl = true
29
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
30
+ return http.request(req) if RESPONSE_HAS_BODY
31
+ return true
32
+ end
33
+
34
+ private
35
+ # You can not instantiate an ExchangeRequest externally.
36
+ def initialize(*args)
37
+ super
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+ def ends_with?(partial)
3
+ self[self.size - partial.size..self.size] == partial
4
+ end
5
+
6
+ def ensure_ends_with(partial)
7
+ self.ends_with?(partial) ? self : self + partial
8
+ end
9
+ end
@@ -0,0 +1,119 @@
1
+ require 'rexml/document'
2
+ require 'net/https'
3
+ require 'rexchange/extensions'
4
+ require 'rexchange/dav_search_request'
5
+ require 'rexchange/message'
6
+
7
+ module RExchange
8
+
9
+ class Folder
10
+ include REXML
11
+
12
+ include Enumerable
13
+
14
+ attr_reader :credentails
15
+
16
+ def initialize(credentials, parent, folder)
17
+ @credentials, @parent, @folder = credentials, parent, folder
18
+ end
19
+
20
+ alias :old_method_missing :method_missing
21
+
22
+ def method_missing(sym, *args)
23
+ if subfolder_exist?(sym.to_s)
24
+ Folder.new(@credentials, self, sym )
25
+ else
26
+ old_method_missing(sym, args)
27
+ end
28
+ end
29
+
30
+ def each
31
+ messages.each do |msg|
32
+ yield msg
33
+ end
34
+ end
35
+
36
+ def messages_in(folder)
37
+ folder.split('/').inject(@credentials.uri.path) do |final_path, current_path|
38
+ Folder.new(@credentials, final_path, current_path)
39
+ end.messages
40
+ end
41
+
42
+ def messages
43
+
44
+ body = <<DABODY
45
+ <?xml version="1.0"?>
46
+ <D:searchrequest xmlns:D = "DAV:">
47
+ <D:sql>
48
+ SELECT "DAV:href",
49
+ "urn:schemas:httpmail:from", "urn:schemas:httpmail:to", "urn:schemas:mailheader:message-id",
50
+ "urn:schemas:httpmail:subject", "DAV:href", "urn:schemas:httpmail:date",
51
+ "urn:schemas:httpmail:importance", "urn:schemas:httpmail:hasattachment",
52
+ "urn:schemas:httpmail:textdescription", "urn:schemas:httpmail:htmldescription"
53
+ FROM SCOPE('shallow traversal of "#{to_s}"')
54
+ WHERE "DAV:ishidden" = false
55
+ AND "DAV:isfolder" = false
56
+ AND "DAV:contentclass" = 'urn:content-classes:message'
57
+ </D:sql>
58
+ </D:searchrequest>
59
+ DABODY
60
+
61
+ response = DavSearchRequest.execute(@credentials, :body => body)
62
+
63
+ mail_messages = []
64
+ xpath_query = "//a:propstat[a:status/text() = 'HTTP/1.1 200 OK']/a:prop"
65
+ Document.new(response.body).elements.each(xpath_query) do |m|
66
+ mail_messages << Message.new(@credentials, m)
67
+ end
68
+
69
+ return mail_messages
70
+ end
71
+
72
+ def self.join(*args)
73
+ args.collect { |f| f.to_s.ensure_ends_with('/') }.to_s.squeeze('/')
74
+ end
75
+
76
+ def folders
77
+ @folders ||= get_folders
78
+ end
79
+
80
+ def subfolder_exist?(folder)
81
+ folders.include? normalize_folder_name(folder)
82
+ end
83
+
84
+ def to_s
85
+ Folder.join(@parent, @folder)
86
+ end
87
+
88
+ private
89
+ def normalize_folder_name(folder)
90
+ folder.to_s.tr('- ', '_').squeeze('_').downcase
91
+ end
92
+
93
+ def get_folders
94
+ request_body = <<DA_QUERY
95
+ <?xml version="1.0"?>
96
+ <D:searchrequest xmlns:D = "DAV:">
97
+ <D:sql>
98
+ SELECT "DAV:displayname"
99
+ FROM SCOPE('shallow traversal of "#{to_s}"')
100
+ WHERE "DAV:ishidden" = false
101
+ AND "DAV:isfolder" = true
102
+ </D:sql>
103
+ </D:searchrequest>
104
+ DA_QUERY
105
+
106
+ response = DavSearchRequest.execute(@credentials, :body => request_body)
107
+
108
+ folders = []
109
+
110
+ # iterate through folders query and add each normalized name to the folders array.
111
+ xpath_query = "//a:propstat[a:status/text() = 'HTTP/1.1 200 OK']/a:prop"
112
+ Document.new(response.body).elements.each(xpath_query) do |m|
113
+ folders << normalize_folder_name(m.elements['a:displayname'].text)
114
+ end
115
+
116
+ return folders
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,43 @@
1
+ require 'rexml/document'
2
+ require 'ostruct'
3
+ require 'rexchange/dav_move_request'
4
+
5
+ module RExchange
6
+ class Message
7
+ include REXML
8
+ include Enumerable
9
+
10
+ attr_accessor :attributes
11
+
12
+ def [](key)
13
+ return @attributes[key]
14
+ end
15
+
16
+ def method_missing(sym, *args)
17
+ return @attributes[sym.to_s] if @attributes.include?(sym.to_s)
18
+ end
19
+
20
+ def body
21
+ @attributes['textdescription'] || @attributes['htmldescription']
22
+ end
23
+
24
+ def initialize(session, dav_property_node)
25
+ @attributes = {}
26
+ @session = session
27
+
28
+ dav_property_node.elements.each do |element|
29
+ @attributes[element.name.tr('-', '_')] = element.text
30
+ end
31
+
32
+ return self
33
+ end
34
+
35
+ def move_to(folder)
36
+ source = URI.parse(self.href).path
37
+ destination = @session.uri.path.ensure_ends_with('/') + folder.ensure_ends_with('/') + source.split('/').last
38
+
39
+ DavMoveRequest.execute(@session, source, destination)
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,34 @@
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
+ # uri = 'https://mydomain.com/exchange/demo'
12
+ # options = { :user => 'test', :password => 'random' }
13
+ #
14
+ # RExchange::Session.new(uri, options) do |mailbox|
15
+ # mailbox.test.each do |message|
16
+ # puts message.subject
17
+ # end
18
+ # end
19
+ def initialize(uri, options = {})
20
+
21
+ @credentials = Credentials.new(uri, options)
22
+ @parent = @credentials.uri.path
23
+ @folder = ''
24
+
25
+ if block_given?
26
+ yield self
27
+ else
28
+ return self
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,28 @@
1
+ require 'test/unit'
2
+ require 'rexchange'
3
+
4
+ class FunctionalTests < Test::Unit::TestCase
5
+
6
+ def setup
7
+ end
8
+
9
+ def teardown
10
+ end
11
+
12
+ # Ok, so it's not a real test, but I needed to get started,
13
+ # and get rid of console scripts.
14
+ def test_no_exceptions
15
+
16
+ uri = "https://#{ENV['rexchange_test_server']}/exchange/#{ENV['rexchange_test_mailbox']}/"
17
+ options = { :user => ENV['rexchange_test_user'], :password => ENV['rexchange_test_password'] }
18
+
19
+ RExchange::open(uri, options) do |mailbox|
20
+ mailbox.inbox.each do |message|
21
+ puts message.body
22
+ end
23
+
24
+ mailbox.folders.each { |folder| puts folder }
25
+ end
26
+
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.10
3
+ specification_version: 1
4
+ name: rexchange
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.1
7
+ date: 2006-01-27
8
+ summary: "A simple wrapper around Microsoft Exchange Server's WebDAV API"
9
+ require_paths:
10
+ - lib
11
+ email: ssmoot@gmail.com; bauer.mail@gmail.com
12
+ homepage: http://substantiality.net
13
+ rubyforge_project: rexchange
14
+ description: "Connect, browse, and iterate through folders and messages on an Exchange Server"
15
+ autorequire: rexchange
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ -
22
+ - ">"
23
+ - !ruby/object:Gem::Version
24
+ version: 0.0.0
25
+ version:
26
+ platform: ruby
27
+ authors:
28
+ - Sam Smoot
29
+ - Scott Bauer
30
+ files:
31
+ - README
32
+ - CHANGELOG
33
+ - RAKEFILE
34
+ - lib/rexchange.rb
35
+ - lib/rexchange/credentials.rb
36
+ - lib/rexchange/dav_move_request.rb
37
+ - lib/rexchange/dav_search_request.rb
38
+ - lib/rexchange/exchange_request.rb
39
+ - lib/rexchange/extensions.rb
40
+ - lib/rexchange/folder.rb
41
+ - lib/rexchange/message.rb
42
+ - lib/rexchange/session.rb
43
+ - test/functional.rb
44
+ test_files: []
45
+ rdoc_options:
46
+ - "--line-numbers"
47
+ - "--inline-source"
48
+ - "--main"
49
+ - README
50
+ extra_rdoc_files:
51
+ - README
52
+ - CHANGELOG
53
+ - RAKEFILE
54
+ executables: []
55
+ extensions: []
56
+ requirements:
57
+ - none
58
+ dependencies: []