ews-api 0.1.0.a

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ doc
21
+ .yardoc
22
+
23
+ ## PROJECT::SPECIFIC
24
+ spec/test-config.yml
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jeremy Burks
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,38 @@
1
+ = ews-api
2
+
3
+ Exchange Web Services API.
4
+
5
+ == Configuration
6
+
7
+ Set the endpoint
8
+
9
+ EWS::Service.endpoint :uri => 'https://example.com/ews/exchange.asmx',
10
+ :version => 1
11
+
12
+ Set the credentials if the service requires autentication. NTLM is known to work.
13
+
14
+ EWS::Service.set_auth 'testuser', 'xxxxxx'
15
+
16
+ == Testing
17
+
18
+ Typically it isn't a good idea for tests to depend on external resources.
19
+ This project is in its early days and I am new to EWS. So as to make it
20
+ easier to implement the service the tests depend on connecting to EWS.
21
+
22
+ If +spec/test-config+ exists it will be loaded and the +EWS::Service+ will
23
+ be configured.
24
+
25
+ The config file is ignored via +.gitignore+.
26
+
27
+ === Example +spec/test-config.yml+
28
+
29
+ # Example spec/test-config.yml
30
+ endpoint:
31
+ :uri: 'https://localhost/ews/exchange.asmx'
32
+ :version: 1
33
+ username: testuser
34
+ password: xxxxxx
35
+
36
+ == Copyright
37
+
38
+ Copyright (c) 2009 Jeremy Burks. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,88 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "ews-api"
8
+ gem.summary = 'Exchange Web Services API'
9
+ gem.description = "Exchange Web Services API. It doesn't use soap4r."
10
+ gem.email = "jeremy.burks@gmail.com"
11
+ gem.homepage = "http://github.com/jrun/ews-api"
12
+ gem.authors = ["jrun"]
13
+ gem.add_dependency 'httpclient'
14
+ gem.add_dependency 'rubyntlm'
15
+ gem.add_dependency 'handsoap', '1.1.4'
16
+ gem.add_development_dependency "rspec", ">= 1.2.9"
17
+ gem.add_development_dependency "yard", ">= 0"
18
+
19
+ desc "Install development dependencies."
20
+ task :setup do
21
+ gems = ::Gem::SourceIndex.from_installed_gems
22
+ gem.dependencies.each do |dep|
23
+ if gems.find_name(dep.name, dep.version_requirements).empty?
24
+ puts "Installing dependency: #{dep}"
25
+ system %Q|gem install #{dep.name} -v "#{dep.version_requirements}" --development|
26
+ end
27
+ end
28
+ end
29
+
30
+ desc "Build and reinstall the gem locally."
31
+ task :reinstall => :build do
32
+ version = File.read('VERSION')
33
+ if (system("gem list #{gem.name} -l") || "") =~ /#{gem.name}-#{version}/
34
+ system "gem uninstall #{gem.name}"
35
+ end
36
+ system "gem install --no-rdoc --no-ri -l pkg/#{gem.name}-#{version}"
37
+ end
38
+ end
39
+
40
+ Jeweler::GemcutterTasks.new
41
+ rescue LoadError
42
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
43
+ end
44
+
45
+ require 'spec/rake/spectask'
46
+ Spec::Rake::SpecTask.new(:spec) do |spec|
47
+ spec.libs << 'lib' << 'spec'
48
+ spec.spec_files = FileList['spec/**/*_spec.rb']
49
+ end
50
+
51
+ desc 'Run tests against a real EWS server'
52
+ Spec::Rake::SpecTask.new(:integration) do |spec|
53
+ spec.libs << 'lib' << 'spec'
54
+ spec.spec_files = FileList['spec/integration.rb']
55
+ end
56
+
57
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
58
+ spec.libs << 'lib' << 'spec'
59
+ spec.pattern = 'spec/**/*_spec.rb'
60
+ spec.rcov = true
61
+ end
62
+
63
+ task :spec => :check_dependencies
64
+ task :default => :spec
65
+ task :build => [:spec, :yard]
66
+
67
+ begin
68
+ require 'yard'
69
+ YARD::Rake::YardocTask.new
70
+ rescue LoadError
71
+ task :yardoc do
72
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
73
+ end
74
+ end
75
+
76
+ begin
77
+ require 'grancher/task'
78
+ Grancher::Task.new do |g|
79
+ g.branch = 'gh-pages'
80
+ g.push_to = 'origin'
81
+ g.directory 'doc'
82
+ end
83
+ rescue LoadError
84
+ task :publish do
85
+ abort "grancher is not available. Run 'rake setup' to install all development dependencies."
86
+ end
87
+ end
88
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0.a
data/ews-api.gemspec ADDED
@@ -0,0 +1,96 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{ews-api}
8
+ s.version = "0.1.0.a"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["jrun"]
12
+ s.date = %q{2009-12-17}
13
+ s.description = %q{Exchange Web Services API. It doesn't use soap4r.}
14
+ s.email = %q{jeremy.burks@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "ews-api.gemspec",
27
+ "lib/ews-api.rb",
28
+ "lib/ews/attachment.rb",
29
+ "lib/ews/error.rb",
30
+ "lib/ews/folder.rb",
31
+ "lib/ews/message.rb",
32
+ "lib/ews/model.rb",
33
+ "lib/ews/parser.rb",
34
+ "lib/ews/service.rb",
35
+ "spec/ews/attachment_spec.rb",
36
+ "spec/ews/folder_spec.rb",
37
+ "spec/ews/message_spec.rb",
38
+ "spec/ews/model_spec.rb",
39
+ "spec/ews/parser_spec.rb",
40
+ "spec/ews/service_spec.rb",
41
+ "spec/fixtures/find_folder.xml",
42
+ "spec/fixtures/find_item.xml",
43
+ "spec/fixtures/find_item_all_properties.xml",
44
+ "spec/fixtures/get_attachment.xml",
45
+ "spec/fixtures/get_folder.xml",
46
+ "spec/fixtures/get_item_all_properties.xml",
47
+ "spec/fixtures/get_item_default.xml",
48
+ "spec/fixtures/get_item_id_only.xml",
49
+ "spec/fixtures/get_item_no_attachments.xml",
50
+ "spec/fixtures/get_item_with_error.xml",
51
+ "spec/integration.rb",
52
+ "spec/spec.opts",
53
+ "spec/spec_helper.rb"
54
+ ]
55
+ s.homepage = %q{http://github.com/jrun/ews-api}
56
+ s.rdoc_options = ["--charset=UTF-8"]
57
+ s.require_paths = ["lib"]
58
+ s.rubygems_version = %q{1.3.5}
59
+ s.summary = %q{Exchange Web Services API}
60
+ s.test_files = [
61
+ "spec/spec_helper.rb",
62
+ "spec/integration.rb",
63
+ "spec/ews/parser_spec.rb",
64
+ "spec/ews/message_spec.rb",
65
+ "spec/ews/attachment_spec.rb",
66
+ "spec/ews/folder_spec.rb",
67
+ "spec/ews/service_spec.rb",
68
+ "spec/ews/model_spec.rb"
69
+ ]
70
+
71
+ if s.respond_to? :specification_version then
72
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
73
+ s.specification_version = 3
74
+
75
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
76
+ s.add_runtime_dependency(%q<httpclient>, [">= 0"])
77
+ s.add_runtime_dependency(%q<rubyntlm>, [">= 0"])
78
+ s.add_runtime_dependency(%q<handsoap>, ["= 1.1.4"])
79
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
80
+ s.add_development_dependency(%q<yard>, [">= 0"])
81
+ else
82
+ s.add_dependency(%q<httpclient>, [">= 0"])
83
+ s.add_dependency(%q<rubyntlm>, [">= 0"])
84
+ s.add_dependency(%q<handsoap>, ["= 1.1.4"])
85
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
86
+ s.add_dependency(%q<yard>, [">= 0"])
87
+ end
88
+ else
89
+ s.add_dependency(%q<httpclient>, [">= 0"])
90
+ s.add_dependency(%q<rubyntlm>, [">= 0"])
91
+ s.add_dependency(%q<handsoap>, ["= 1.1.4"])
92
+ s.add_dependency(%q<rspec>, [">= 1.2.9"])
93
+ s.add_dependency(%q<yard>, [">= 0"])
94
+ end
95
+ end
96
+
data/lib/ews-api.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'net/ntlm'
2
+ require 'handsoap'
3
+
4
+ require 'ews/error'
5
+ require 'ews/model'
6
+ require 'ews/attachment'
7
+ require 'ews/message'
8
+ require 'ews/folder'
9
+ require 'ews/parser'
10
+ require 'ews/service'
11
+
12
+ module EWS
13
+ def self.inbox
14
+ Service.get_folder(:inbox, :base_shape => :AllProperties)
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ module EWS
2
+ class Attachment < Model
3
+ def id
4
+ attrs[:attachment_id]
5
+ end
6
+ end
7
+ end
data/lib/ews/error.rb ADDED
@@ -0,0 +1,14 @@
1
+ module EWS
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class ResponseError < Error
7
+ attr_reader :response_code
8
+
9
+ def initialize(message, response_code)
10
+ super message
11
+ @response_code = response_code
12
+ end
13
+ end
14
+ end
data/lib/ews/folder.rb ADDED
@@ -0,0 +1,44 @@
1
+ module EWS
2
+
3
+ class Folder < Model
4
+ def id
5
+ attrs[:folder_id][:id]
6
+ end
7
+
8
+ def change_key
9
+ attrs[:folder_id][:change_key]
10
+ end
11
+
12
+ def name
13
+ attrs[:display_name]
14
+ end
15
+
16
+ def each_message
17
+ items.each {|message| yield message }
18
+ end
19
+
20
+ def folders
21
+ @folders ||= find_folders.inject({}) do |folders, folder|
22
+ folders[folder.name] = folder
23
+ folders
24
+ end
25
+ end
26
+
27
+ def items
28
+ @items ||= find_folder_items
29
+ end
30
+
31
+ private
32
+ def find_folder_items
33
+ # NOTE: This assumes Service#find_item only returns
34
+ # Messages. That is true now but will change as more
35
+ # of the parser is implemented.
36
+ service.find_item(self.name, :base_shape => :AllProperties)
37
+ end
38
+
39
+ def find_folders
40
+ service.find_folder(self.name)
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,22 @@
1
+ module EWS
2
+
3
+ class Message < Model
4
+ def id
5
+ @id ||= attrs[:item_id][:id]
6
+ end
7
+
8
+ def change_key
9
+ @change_key ||= attrs[:item_id][:change_key]
10
+ end
11
+
12
+ def shallow?
13
+ self.body.nil? || self.header.nil?
14
+ end
15
+
16
+ def move_to!(folder_id)
17
+ # TODO: support DistinguishedFolderId?
18
+ service.move_item! folder_id, [self.id]
19
+ end
20
+ end
21
+
22
+ end
data/lib/ews/model.rb ADDED
@@ -0,0 +1,37 @@
1
+ module EWS
2
+
3
+ class Model
4
+ def initialize(attrs = {})
5
+ @attrs = attrs.dup
6
+ end
7
+
8
+ def shallow?
9
+ raise NotImplementedError, "Each model must determine when it is shallow."
10
+ end
11
+
12
+ protected
13
+ attr_reader :attrs
14
+
15
+ def service
16
+ EWS::Service
17
+ end
18
+
19
+ public
20
+ def method_missing(meth, *args)
21
+ method_name = meth.to_s
22
+ if method_name.end_with?('=')
23
+ attr = method_name.chomp('=').to_sym
24
+ @attrs[attr] = args.first
25
+ elsif method_name.end_with?('?')
26
+ attr = method_name.chomp('?').to_sym
27
+ @attrs[attr] == true
28
+ elsif @attrs.has_key?(meth)
29
+ @attrs[meth]
30
+ else
31
+ super meth, *args
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
data/lib/ews/parser.rb ADDED
@@ -0,0 +1,166 @@
1
+ module EWS
2
+
3
+ class Parser
4
+ def parse_find_folder(doc)
5
+ doc.xpath('//t:Folders/child::*').map do |node|
6
+ parse_exchange_folder node.xpath('.') # force NodeSelection
7
+ end.compact
8
+ end
9
+
10
+ def parse_get_folder(doc)
11
+ parse_exchange_folder doc.xpath('//m:Folders/child::*[1]')
12
+ end
13
+
14
+ def parse_find_item(doc)
15
+ doc.xpath('//t:Items/child::*').map do |node|
16
+ parse_exchange_item node.xpath('.') # force NodeSelection
17
+ end.compact
18
+ end
19
+
20
+ def parse_get_item(doc)
21
+ parse_exchange_item doc.xpath('//m:Items/child::*[1]')
22
+ end
23
+
24
+ def parse_get_attachment(doc)
25
+ parse_attachment doc.xpath('//m:Attachments/child::*[1]')
26
+ end
27
+
28
+ # Checks the ResponseMessage for errors.
29
+ #
30
+ # @see http://msdn.microsoft.com/en-us/library/aa494164%28EXCHG.80%29.aspx
31
+ # Exhange 2007 Valid Response Messages
32
+ def parse_response_message(doc)
33
+ error_node = doc.xpath('//m:ResponseMessages/child::*[@ResponseClass="Error"]')
34
+ unless error_node.empty?
35
+ error_msg = error_node.xpath('m:MessageText/text()').to_s
36
+ response_code = error_node.xpath('m:ResponseCode/text()').to_s
37
+ raise EWS::ResponseError.new(error_msg, response_code)
38
+ end
39
+ end
40
+
41
+ private
42
+ def parse_exchange_folder(folder_node)
43
+ case folder_node.node_name
44
+ when 'Folder'
45
+ parse_folder folder_node
46
+ when 'CalendarFolder'
47
+ when 'ContactsFolder'
48
+ when 'SearchFolder'
49
+ when 'TasksFolder'
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
55
+ def parse_folder(folder_node)
56
+ attrs = {
57
+ :folder_id => parse_id(folder_node.xpath('t:FolderId')),
58
+ :display_name => folder_node.xpath('t:DisplayName/text()').to_s,
59
+ :total_count => folder_node.xpath('t:TotalCount/text()').to_i,
60
+ :child_folder_count => folder_node.xpath('t:ChildFolderCount/text()').to_i,
61
+ :unread_count => folder_node.xpath('t:UnreadCount/text()').to_i
62
+ }
63
+ Folder.new attrs
64
+ end
65
+
66
+ def parse_exchange_item(item_node)
67
+ case item_node.node_name
68
+ when 'Item'
69
+ when 'Message'
70
+ parse_message item_node
71
+ when 'CalendarItem'
72
+ when 'Contact'
73
+ when 'Task'
74
+ when 'MeetingMessage'
75
+ when 'MeetingRequest'
76
+ when 'MeetingResponse'
77
+ when 'MeetingCancellation'
78
+ else
79
+ nil
80
+ end
81
+ end
82
+
83
+ def parse_message(message_node)
84
+ attrs = {
85
+ :item_id => parse_id(message_node.xpath('t:ItemId')),
86
+ :parent_folder_id => parse_id(message_node.xpath('t:ParentFolderId')),
87
+ :subject => message_node.xpath('t:Subject/text()').to_s,
88
+ :body => message_node.xpath('t:Body/text()').to_s,
89
+ :body_type => message_node.xpath('t:Body/@BodyType').to_s
90
+ }
91
+
92
+ nodeset = message_node.xpath('t:HasAttachments')
93
+ attrs[:has_attachments] = if not nodeset.empty?
94
+ parse_bool(nodeset)
95
+ end
96
+
97
+ nodeset = message_node.xpath('t:Attachments')
98
+ attrs[:attachments] = if not nodeset.empty?
99
+ nodeset.xpath('t:ItemAttachment|t:FileAttachment').map do |node|
100
+ parse_attachment node
101
+ end
102
+ end
103
+
104
+ nodeset = message_node.xpath('t:InternetMessageHeaders')
105
+ attrs[:header] = if not nodeset.empty?
106
+ parse_header nodeset
107
+ end
108
+
109
+ Message.new attrs
110
+ end
111
+
112
+ EXCHANGE_ITEM_XPATH = ['t:Item',
113
+ 't:Message',
114
+ 't:CalendarItem',
115
+ 't:Contact',
116
+ 't:Task',
117
+ 't:MeetingMessage',
118
+ 't:MeetingRequest',
119
+ 't:MeetingResponse',
120
+ 't:MeetingCancellation'].join('|').freeze
121
+
122
+ def parse_attachment(attachment_node)
123
+ attrs = {
124
+ :attachment_id => attachment_node.xpath('t:AttachmentId/@Id').to_s,
125
+ :name => attachment_node.xpath('t:Name/text()').to_s,
126
+ :content_type => attachment_node.xpath('t:ContentType/text()').to_s,
127
+ :content_id => attachment_node.xpath('t:ContentId/text()').to_s,
128
+ :content_location => attachment_node.xpath('t:ContentLocation/text()').to_s
129
+ }
130
+
131
+ case attachment_node.node_name
132
+ when 'ItemAttachment'
133
+ attrs[:item] = parse_exchange_item attachment_node.xpath(EXCHANGE_ITEM_XPATH)
134
+ when 'FileAttachment'
135
+ end
136
+
137
+ Attachment.new attrs
138
+ end
139
+
140
+ def parse_header(header_node)
141
+ header_node.xpath('t:InternetMessageHeader').inject({}) do |header, node|
142
+ name = node.xpath('@HeaderName').to_s.downcase
143
+ header[name] = [] unless header.has_key?(name)
144
+ header[name] << node.xpath('text()').to_s
145
+ header
146
+ end
147
+ end
148
+
149
+ def parse_id(id_node)
150
+ return nil if id_node.empty?
151
+ { :id => id_node.xpath('@Id').to_s,
152
+ :change_key => id_node.xpath('@ChangeKey').to_s }
153
+ end
154
+
155
+ def parse_bool(val)
156
+ case val.to_s.downcase
157
+ when 'true'
158
+ true
159
+ when 'false'
160
+ false
161
+ else
162
+ nil
163
+ end
164
+ end
165
+ end
166
+ end