ews-api 0.1.0.a

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/.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