rews 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Trampoline Systems Ltd
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,52 @@
1
+ = rews
2
+
3
+ Rews is a simple Ruby client for Exchange Web Services.
4
+
5
+ * Searching, fetching and deletion are supported.
6
+ * Filtering, ordering, and arbitrary property retrieval are supported.
7
+ * Bulk operations are supported
8
+
9
+ Method names generally follow the Exchange Web Services API
10
+ http://msdn.microsoft.com/en-us/library/bb409286(EXCHG.140).aspx
11
+
12
+ to use :
13
+
14
+ # create a client
15
+ c = Rews::Client.new("https://exchange.bar.com/EWS/Exchange.asmx", :ntlm, 'EXCHDOM\foo', 'password')
16
+
17
+ # find a distinguished folder from any one of the mailboxes the user has permissions for
18
+ inbox=c.distinguished_folder_id('inbox', 'foo@bar.com')
19
+
20
+ # find some message_ids,
21
+ mids = inbox.find_item_id(:restriction=>[:<=, "item:DateTimeReceived",DateTime.now],
22
+ :sort_order=>[["item:DateTimeReceived", "Ascending"]],
23
+ :indexed_page_item_view=>{:max_entries_returned=>10, :offset=>0})
24
+
25
+ # get some properties for a bunch of messages in one hit
26
+ messages = inbox.get_item(mids, :item_shape=>{
27
+ :base_shape=>:IdOnly,
28
+ :additional_properties=>[
29
+ [:field_uri, "item:Subject"],
30
+ [:field_uri, "item:DateTimeReceived"],
31
+ [:field_uri, "message:InternetMessageId"]]})
32
+
33
+ # delete the items to the DeletedItems folder
34
+ inbox.delete_item(mids, :delete_type=>:MoveToDeletedItems)
35
+
36
+ == Install
37
+
38
+ gem install rews
39
+
40
+ == Note on Patches/Pull Requests
41
+
42
+ * Fork the project.
43
+ * Make your feature addition or bug fix.
44
+ * Add tests for it. This is important so I don't break it in a
45
+ future version unintentionally.
46
+ * Commit, do not mess with rakefile, version, or history.
47
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
48
+ * Send me a pull request. Bonus points for topic branches.
49
+
50
+ == Copyright
51
+
52
+ Copyright (c) 2011 Trampoline Systems Ltd. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rews"
8
+ gem.summary = %Q{a Ruby client for Exchange Web Services}
9
+ gem.description = %Q{an email focussed Ruby client for Exchange Web Services atop Savon}
10
+ gem.email = "craig@trampolinesystems.com"
11
+ gem.homepage = "http://github.com/trampoline/rews"
12
+ gem.authors = ["Trampoline Systems Ltd"]
13
+ gem.add_dependency "savon", ">= 0.8.6"
14
+ gem.add_dependency "ntlm-http", ">= 0.1.1"
15
+ gem.add_dependency "httpclient", ">= 2.1.6.1.1"
16
+ gem.add_dependency "fetch_in", ">= 0.2.0"
17
+ gem.add_development_dependency "rspec", ">= 1.2.9"
18
+ gem.add_development_dependency "rr", ">= 0.10.5"
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'spec/rake/spectask'
27
+ Spec::Rake::SpecTask.new(:spec) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.spec_files = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
33
+ spec.libs << 'lib' << 'spec'
34
+ spec.pattern = 'spec/**/*_spec.rb'
35
+ spec.rcov = true
36
+ end
37
+
38
+ task :spec => :check_dependencies
39
+
40
+ task :default => :spec
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "rews #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,274 @@
1
+ module Rews
2
+ module Folder
3
+ class Folder
4
+ attr_reader :client
5
+ attr_reader :folder_id
6
+ attr_reader :attributes
7
+
8
+ def initialize(client, folder)
9
+ @folder_id = VanillaFolderId.new(client, folder[:folder_id])
10
+ @attributes = folder
11
+ end
12
+
13
+ def [](key)
14
+ @attributes[key]
15
+ end
16
+
17
+ def keys
18
+ @attributes.keys
19
+ end
20
+ end
21
+
22
+ class FindResult
23
+ VIEW_ATTRS = [:includes_last_item_in_range,
24
+ :indexed_paging_offset,
25
+ :total_items_in_view]
26
+
27
+ VIEW_ATTRS.each do |attr|
28
+ attr_reader attr
29
+ end
30
+ attr_reader :result
31
+
32
+ def initialize(view, &proc)
33
+ VIEW_ATTRS.each do |attr|
34
+ self.instance_variable_set("@#{attr}", view[attr])
35
+ end
36
+ @result = proc.call(view) if proc
37
+ end
38
+
39
+ def length
40
+ result.length
41
+ end
42
+
43
+ def size
44
+ result.size
45
+ end
46
+
47
+ def [](key)
48
+ result[key]
49
+ end
50
+ end
51
+
52
+ class BaseFolderId
53
+ include Util
54
+
55
+ attr_reader :client
56
+
57
+ def initialize(client)
58
+ @client=client
59
+ end
60
+
61
+ FIND_FOLDER_OPTS = {
62
+ :restriction=>nil,
63
+ :indexed_page_folder_view=>View::INDEXED_PAGE_VIEW_OPTS,
64
+ :folder_shape=>Shape::FOLDER_SHAPE_OPTS}
65
+
66
+ def find_folder(opts={})
67
+ opts = check_opts(FIND_FOLDER_OPTS, opts)
68
+
69
+ r = with_error_check(client, :find_folder_response, :response_messages, :find_folder_response_message) do
70
+ client.request(:wsdl, "FindFolder", "Traversal"=>"Shallow") do
71
+ soap.namespaces["xmlns:t"]=SCHEMA_TYPES
72
+ xml = Builder::XmlMarkup.new
73
+
74
+ xml << Shape::FolderShape.new(opts[:folder_shape]||{}).to_xml
75
+ xml << View::IndexedPageFolderView.new(opts[:indexed_page_folder_view]).to_xml if opts[:indexed_page_folder_view]
76
+ xml << Restriction.new(opts[:restriction]).to_xml if opts[:restriction]
77
+
78
+ xml.wsdl :ParentFolderIds do
79
+ xml << Gyoku.xml(self.to_xml_hash)
80
+ end
81
+ soap.body = xml.target!
82
+ end
83
+ end
84
+
85
+ FindResult.new(r.fetch_in(:root_folder)) do |view|
86
+ results = view.fetch_in(:folders, :folder)
87
+ results = [results] if !results.is_a?(Array)
88
+ results.compact.map do |folder|
89
+ Folder.new(client, folder)
90
+ end
91
+ end
92
+ end
93
+
94
+ def find_folder_id(opts={})
95
+ opts = check_opts(FIND_FOLDER_OPTS, opts)
96
+
97
+ shape = opts[:folder_shape] ||={}
98
+ shape[:base_shape]||=:IdOnly
99
+
100
+ r = find_folder(opts)
101
+ r.result.map!(&:folder_id)
102
+ r
103
+ end
104
+
105
+ FIND_ITEM_OPTS = {
106
+ :restriction=>nil,
107
+ :sort_order=>nil,
108
+ :indexed_page_item_view=>View::INDEXED_PAGE_VIEW_OPTS,
109
+ :item_shape=>Shape::ITEM_SHAPE_OPTS}
110
+
111
+ # find message-ids in a folder
112
+ def find_item(opts={})
113
+ opts = check_opts(FIND_ITEM_OPTS, opts)
114
+
115
+ r = with_error_check(client, :find_item_response, :response_messages, :find_item_response_message) do
116
+ client.request(:wsdl, "FindItem", "Traversal"=>"Shallow") do
117
+ soap.namespaces["xmlns:t"]=SCHEMA_TYPES
118
+
119
+ xml = Builder::XmlMarkup.new
120
+
121
+ xml << Shape::ItemShape.new(opts[:item_shape]||{}).to_xml
122
+ xml << View::IndexedPageItemView.new(opts[:indexed_page_item_view]).to_xml if opts[:indexed_page_item_view]
123
+ xml << Restriction.new(opts[:restriction]).to_xml if opts[:restriction]
124
+ xml << SortOrder.new(opts[:sort_order]).to_xml if opts[:sort_order]
125
+
126
+ xml.wsdl :ParentFolderIds do
127
+ xml << Gyoku.xml(self.to_xml_hash)
128
+ end
129
+
130
+ soap.body = xml.target!
131
+ end
132
+ end
133
+
134
+ FindResult.new(r.to_hash.fetch_in(:root_folder)) do |view|
135
+ results = Item.read_items(client, view[:items])
136
+ end
137
+ end
138
+
139
+ def find_item_id(opts={})
140
+ opts = check_opts(FIND_ITEM_OPTS, opts)
141
+
142
+ shape = opts[:item_shape] ||= {}
143
+ shape[:base_shape]||=:IdOnly
144
+
145
+ r = find_item(opts)
146
+ r.result.map!(&:item_id)
147
+ r
148
+ end
149
+
150
+ GET_ITEM_OPTS = {
151
+ :item_shape=>Shape::ITEM_SHAPE_OPTS,
152
+ :ignore_change_keys=>nil
153
+ }
154
+
155
+ # get a bunch of messages in one api hit
156
+ def get_item(message_ids, opts={})
157
+ opts = check_opts(GET_ITEM_OPTS, opts)
158
+ message_ids = message_ids.result if message_ids.is_a?(FindResult)
159
+
160
+ r = with_error_check(client, :get_item_response,:response_messages,:get_item_response_message) do
161
+ client.request(:wsdl, "GetItem") do
162
+ soap.namespaces["xmlns:t"]=SCHEMA_TYPES
163
+
164
+ xml = Builder::XmlMarkup.new
165
+
166
+ xml << Shape::ItemShape.new(opts[:item_shape]||{}).to_xml
167
+ xml.wsdl :ItemIds do
168
+ message_ids.each do |mid|
169
+ xml << Gyoku.xml(mid.to_xml_hash(opts[:ignore_change_keys]))
170
+ end
171
+ end
172
+
173
+ soap.body = xml.target!
174
+ end
175
+ end
176
+ Item.read_get_item_response_messages(client, r)
177
+ end
178
+
179
+ DELETE_ITEM_OPTS = {
180
+ :delete_type! =>nil,
181
+ :ignore_change_keys=>false
182
+ }
183
+
184
+ def delete_item(message_ids, opts={})
185
+ opts = check_opts(DELETE_ITEM_OPTS, opts)
186
+ message_ids = message_ids.result if message_ids.is_a?(FindResult)
187
+
188
+ r = with_error_check(client, :delete_item_response, :response_messages, :delete_item_response_message) do
189
+ client.request(:wsdl, "DeleteItem", :DeleteType=>opts[:delete_type]) do
190
+ soap.namespaces["xmlns:t"]=SCHEMA_TYPES
191
+
192
+ xml = Builder::XmlMarkup.new
193
+
194
+ xml.wsdl :ItemIds do
195
+ message_ids.each do |mid|
196
+ xml << Gyoku.xml(mid.to_xml_hash(opts[:ignore_change_keys]))
197
+ end
198
+ end
199
+
200
+ soap.body = xml.target!
201
+ end
202
+ end
203
+ true
204
+ end
205
+ end
206
+
207
+ class VanillaFolderId < BaseFolderId
208
+ attr_reader :id
209
+ attr_reader :change_key
210
+
211
+ def initialize(client, folder_id)
212
+ super(client)
213
+ @id=folder_id[:id]
214
+ @change_key=folder_id[:change_key]
215
+ raise "no id" if !@id
216
+ end
217
+
218
+ def to_xml_hash
219
+ if change_key
220
+ {
221
+ "t:FolderId"=>"",
222
+ :attributes! => {
223
+ "t:FolderId" => {
224
+ "Id" => id.to_s,
225
+ "ChangeKey" => change_key.to_s}}}
226
+ else
227
+ {
228
+ "t:FolderId"=>"",
229
+ :attributes! => {
230
+ "t:FolderId" => {
231
+ "Id" => id.to_s}}}
232
+ end
233
+ end
234
+
235
+ def inspect
236
+ "#{self.class}(id: #{id}, change_key: #{change_key})"
237
+ end
238
+ end
239
+
240
+ class DistinguishedFolderId < BaseFolderId
241
+ attr_reader :id
242
+ attr_reader :mailbox_email
243
+
244
+ def initialize(client, id, mailbox_email=nil)
245
+ super(client)
246
+ @id = id
247
+ @mailbox_email = mailbox_email
248
+ raise "no id" if !@id
249
+ end
250
+
251
+ def to_xml_hash
252
+ {
253
+ "t:DistinguishedFolderId"=>mailbox_xml_hash,
254
+ :attributes! => {"t:DistinguishedFolderId"=>{"Id"=>id}}}
255
+ end
256
+
257
+ def inspect
258
+ "#{self.class}(id: #{id}, mailbox_email: #{mailbox_email})"
259
+ end
260
+
261
+ private
262
+
263
+ def mailbox_xml_hash
264
+ if mailbox_email
265
+ {
266
+ "t:Mailbox"=>{
267
+ "t:EmailAddress"=>mailbox_email}}
268
+ else
269
+ ""
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
data/lib/rews/item.rb ADDED
@@ -0,0 +1,127 @@
1
+ module Rews
2
+ module Item
3
+ module_function
4
+
5
+ # return a list of Item objects given a hash formed from an Items element
6
+ def read_items(client, items)
7
+ items.map do |item_class,items_of_class|
8
+ items_of_class = [items_of_class] if !items_of_class.is_a?(Array)
9
+ items_of_class.map do |item|
10
+ Item.new(client, item_class, item)
11
+ end
12
+ end.flatten
13
+ end
14
+
15
+ # return a list of Item objects from a list of GetItemResponseMessages
16
+ def read_get_item_response_messages(client, get_item_response_messages)
17
+ get_item_response_messages = [get_item_response_messages] if !get_item_response_messages.is_a?(Array)
18
+ items = get_item_response_messages.map do |girm|
19
+ read_items(client, girm[:items])
20
+ end.flatten
21
+ end
22
+
23
+ class Item
24
+ attr_reader :client
25
+ attr_reader :item_id
26
+ attr_reader :item_class
27
+ attr_reader :attributes
28
+
29
+ def initialize(client, item_class, attributes)
30
+ @item_id = ItemId.new(client, attributes[:item_id])
31
+ @item_class = item_class
32
+ @attributes = attributes
33
+ end
34
+
35
+ def [](key)
36
+ @attributes[key]
37
+ end
38
+
39
+ def keys
40
+ @attributes.keys
41
+ end
42
+ end
43
+
44
+ class ItemId
45
+ include Util
46
+
47
+ attr_reader :client
48
+ attr_reader :id
49
+ attr_reader :change_key
50
+
51
+ def initialize(client, item_id)
52
+ @client=client
53
+ @id = item_id[:id]
54
+ @change_key=item_id[:change_key]
55
+ raise "no id" if !@id
56
+ end
57
+
58
+ GET_ITEM_OPTS = {
59
+ :item_shape=>Shape::ITEM_SHAPE_OPTS,
60
+ :ignore_change_keys=>nil
61
+ }
62
+
63
+ def get_item(opts={})
64
+ opts = check_opts(GET_ITEM_OPTS, opts)
65
+ r = with_error_check(client, :get_item_response,:response_messages,:get_item_response_message) do
66
+ client.request(:wsdl, "GetItem") do
67
+ soap.namespaces["xmlns:t"]=SCHEMA_TYPES
68
+
69
+ xml = Builder::XmlMarkup.new
70
+
71
+ xml << Shape::ItemShape.new(opts[:item_shape]||{}).to_xml
72
+ xml.wsdl :ItemIds do
73
+ xml << Gyoku.xml(self.to_xml_hash(opts[:ignore_change_keys]))
74
+ end
75
+
76
+ soap.body = xml.target!
77
+ end
78
+ end
79
+ ::Rews::Item.read_get_item_response_messages(client, r).first
80
+ end
81
+
82
+ DELETE_ITEM_OPTS = {
83
+ :delete_type! =>nil,
84
+ :ignore_change_keys=>false
85
+ }
86
+
87
+ def delete_item(opts={})
88
+ opts = check_opts(DELETE_ITEM_OPTS, opts)
89
+ r = with_error_check(client, :delete_item_response, :response_messages, :delete_item_response_message) do
90
+ client.request(:wsdl, "DeleteItem", :DeleteType=>opts[:delete_type]) do
91
+ soap.namespaces["xmlns:t"]=SCHEMA_TYPES
92
+
93
+ xml = Builder::XmlMarkup.new
94
+
95
+ xml.wsdl :ItemIds do
96
+ xml << Gyoku.xml(self.to_xml_hash(opts[:ignore_change_keys]))
97
+ end
98
+
99
+ soap.body = xml.target!
100
+ end
101
+ end
102
+ true
103
+ end
104
+
105
+ def to_xml_hash(ignore_change_key=false)
106
+ if change_key && !ignore_change_key
107
+ {
108
+ "t:ItemId"=>"",
109
+ :attributes! => {
110
+ "t:ItemId" => {
111
+ "Id" => id.to_s,
112
+ "ChangeKey" => change_key.to_s}}}
113
+ else
114
+ {
115
+ "t:ItemId"=>"",
116
+ :attributes! => {
117
+ "t:ItemId" => {
118
+ "Id" => id.to_s}}}
119
+ end
120
+ end
121
+
122
+ def inspect
123
+ "#{self.class}(id: #{id}, change_key: #{change_key})"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,89 @@
1
+ # takes restrictions written in Ruby s-expressions and
2
+ # outputs Exchange Web Services Restriction XML
3
+ #
4
+ #
5
+ module Rews
6
+ class Restriction
7
+
8
+ attr_reader :expr
9
+
10
+ def initialize(expr)
11
+ @expr = expr
12
+ end
13
+
14
+ def inspect
15
+ "#{self.class}: #{@expr.inspect}"
16
+ end
17
+
18
+ def to_xml
19
+ Xml::write_restriction(expr)
20
+ end
21
+
22
+ module Xml
23
+ module_function
24
+
25
+ def write_restriction(expr)
26
+ xml = Builder::XmlMarkup.new
27
+ xml.wsdl :Restriction do
28
+ write_expr(xml, expr)
29
+ end
30
+ xml.target!
31
+ end
32
+
33
+ def write_expr(xml, expr)
34
+ case expr[0]
35
+ when :<, :<=, :==, :>=, :>, :"!=" then
36
+ write_comparison(xml, expr)
37
+ when :and, :or, :not then
38
+ write_logical(xml, expr)
39
+ else
40
+ raise "unknown operator: #{expr[0]}"
41
+ end
42
+ end
43
+
44
+ COMPARISON_OPS = {
45
+ :< => :IsLessThan,
46
+ :<= => :IsLessThanOrEqualTo,
47
+ :== => :IsEqualTo,
48
+ :>= => :IsGreaterThanOrEqualTo,
49
+ :> => :IsGreaterThan,
50
+ :"!=" => :IsNotEqualTo}
51
+
52
+ def write_comparison(xml, expr)
53
+ xml.t COMPARISON_OPS[expr[0]] do
54
+ write_field_uri(xml, expr[1])
55
+ write_field_uri_or_constant(xml, expr[2])
56
+ end
57
+ end
58
+
59
+ def write_field_uri_or_constant(xml, expr)
60
+ xml.t :FieldURIOrConstant do
61
+ if expr.is_a?(Array) && expr[0] == :field_uri
62
+ write_field_uri(xml, expr[1])
63
+ else
64
+ write_constant(xml, expr)
65
+ end
66
+ end
67
+ end
68
+
69
+ def write_field_uri(xml, expr)
70
+ xml.t :FieldURI, :FieldURI=>expr
71
+ end
72
+
73
+ def write_constant(xml, expr)
74
+ xml.t :Constant, :Value=>expr
75
+ end
76
+
77
+ LOGICAL_OPS = {
78
+ :and => :And,
79
+ :or => :Or,
80
+ :not => :Not
81
+ }
82
+ def write_logical(xml, expr)
83
+ xml.t LOGICAL_OPS[expr[0]] do
84
+ write_xml(xml ,expr[1..-1])
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
data/lib/rews/shape.rb ADDED
@@ -0,0 +1,70 @@
1
+ module Rews
2
+ module Shape
3
+
4
+ module Xml
5
+ module_function
6
+
7
+ def write_shape(shape_type, &proc)
8
+ xml = Builder::XmlMarkup.new
9
+ xml.wsdl shape_type do
10
+ proc.call(xml)
11
+ end
12
+ xml.target!
13
+ end
14
+
15
+ def write_additional_properties(xml, additional_properties)
16
+ return if !additional_properties
17
+ xml.t :AdditionalProperties do
18
+ additional_properties.each do |additional_property|
19
+ if additional_property[0] == :field_uri
20
+ xml.t :FieldURI, :FieldURI=>additional_property[1]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ class Base
28
+ include Util
29
+ attr_reader :shape
30
+ end
31
+
32
+ ITEM_SHAPE_OPTS = {
33
+ :base_shape=>:Default,
34
+ :include_mime_content=>nil,
35
+ :additional_properties=>nil
36
+ }
37
+
38
+ class ItemShape < Base
39
+ def initialize(shape)
40
+ @shape = check_opts(ITEM_SHAPE_OPTS, shape)
41
+ end
42
+
43
+ def to_xml
44
+ Xml::write_shape(:ItemShape) do |xml|
45
+ xml.t :BaseShape, shape[:base_shape]
46
+ xml.t :IncludeMimeContent, shape[:include_mime_content] if shape[:include_mime_content]
47
+ Xml::write_additional_properties(xml, shape[:additional_properties])
48
+ end
49
+ end
50
+ end
51
+
52
+ FOLDER_SHAPE_OPTS = {
53
+ :base_shape=>:Default,
54
+ :additional_properties=>nil
55
+ }
56
+
57
+ class FolderShape < Base
58
+ def initialize(shape)
59
+ @shape = check_opts(FOLDER_SHAPE_OPTS, shape)
60
+ end
61
+
62
+ def to_xml
63
+ Xml::write_shape(:FolderShape) do |xml|
64
+ xml.t :BaseShape, shape[:base_shape]
65
+ Xml::write_additional_properties(xml, shape[:additional_properties])
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,53 @@
1
+ # takes sort_orders written in Ruby s-expressions and
2
+ # outputs EWS SortOrder XML
3
+ module Rews
4
+ class SortOrder
5
+ attr_reader :expr
6
+
7
+ def initialize(expr)
8
+ @expr=expr
9
+ end
10
+
11
+ def inspect
12
+ "#{self.class}: #{@expr.inspect}"
13
+ end
14
+
15
+ def to_xml
16
+ Xml::write_sort_order(expr)
17
+ end
18
+
19
+ module Xml
20
+ module_function
21
+
22
+ def write_sort_order(expr)
23
+ xml = Builder::XmlMarkup.new
24
+ xml.wsdl :SortOrder do
25
+ write_expr(xml, expr)
26
+ end
27
+ xml.target!
28
+ end
29
+
30
+ def write_expr(xml, expr)
31
+ expr.each do |field_order|
32
+ write_field_order(xml, field_order)
33
+ end
34
+ end
35
+
36
+ def write_field_order(xml, field_order)
37
+ if field_order.is_a?(Array)
38
+ xml.t :FieldOrder, :Order=>field_order[1] do
39
+ write_field_uri(xml, field_order[0])
40
+ end
41
+ else
42
+ xml.t :FieldOrder, field_order do
43
+ write_field_uri(xml, field_order)
44
+ end
45
+ end
46
+ end
47
+
48
+ def write_field_uri(xml, field_uri)
49
+ xml.t :FieldURI, :FieldURI=>field_uri
50
+ end
51
+ end
52
+ end
53
+ end
data/lib/rews/util.rb ADDED
@@ -0,0 +1,86 @@
1
+ require 'set'
2
+
3
+ module Rews
4
+ module Util
5
+ module_function
6
+
7
+ # validates an options hash against a constraints hash
8
+ # in the constraints hash :
9
+ # - keys ending in ! indicate option is required
10
+ # - keys not ending in ! indicate option is not required
11
+ # - non-nil values provide defaults
12
+ # - hash values provide constraints for sub-hashes
13
+ def check_opts(constraints, opts={}, prefix=nil)
14
+ required_keys = Hash[constraints.keys.select{|k| k.to_s[-1..-1] == '!'}.map{|k| [strip_bang(k),k]}]
15
+ optional_keys = constraints.keys.select{|k| k.to_s[-1..-1] != '!'}
16
+
17
+ # check each of the provided options
18
+ opts.keys.each do |key|
19
+ raise "unknown option: #{[prefix,key].compact.join(".")}" if !required_keys.include?(key) && !optional_keys.include?(key)
20
+
21
+ ckey = required_keys[key] || key
22
+ if constraints[ckey].is_a? Hash
23
+ check_opts(constraints[ckey], opts[key] || {}, [prefix,key].compact.join("."))
24
+ end
25
+
26
+ required_keys.delete(key)
27
+ optional_keys.delete(key)
28
+ end
29
+
30
+ raise "required options not given: #{required_keys.keys.map{|k| [prefix,k].compact.join('.')}.join(", ")}" if required_keys.size>0
31
+
32
+ # defaults
33
+ optional_keys.each{|k| opts[k] = constraints[k] if !constraints[k].is_a?(Hash)}
34
+ opts
35
+ end
36
+
37
+ def strip_bang(k)
38
+ if k.is_a? Symbol
39
+ k.to_s[0...-1].to_sym
40
+ else
41
+ k.to_s[0...-1]
42
+ end
43
+ end
44
+
45
+ def camelize(s)
46
+ s.split('_').map(&:capitalize).join
47
+ end
48
+
49
+ def camel_keys(h)
50
+ Hash[h.map{|k,v| [camelize(k.to_s), v]}]
51
+ end
52
+
53
+ def with_error_check(client, *response_msg_keys)
54
+ raise "no block" if !block_given?
55
+
56
+ response = yield
57
+ hash_response = response.to_hash
58
+ statuses = hash_response.fetch_in(*response_msg_keys)
59
+
60
+ if statuses.is_a?(Array)
61
+ all_statuses = statuses
62
+ else
63
+ all_statuses = [statuses]
64
+ end
65
+
66
+ errors = all_statuses.map{|s| single_error_check(client, s)}.compact
67
+ raise errors.join("\n") if !errors.empty?
68
+
69
+ statuses
70
+ end
71
+
72
+ def single_error_check(client, status)
73
+ begin
74
+ response_class = status[:response_class]
75
+ rescue
76
+ raise "no response_class found: #{status.inspect}" if !response_class
77
+ end
78
+
79
+ if status[:response_class] == "Error"
80
+ return "Rews: #{status[:response_code]} - #{status[:message_text]}"
81
+ elsif status[:response_class] == "Warning"
82
+ client.log{|logger| logger.warn("#{status[:message_text]}")}
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/rews/view.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Rews
2
+ module View
3
+ module Xml
4
+ module_function
5
+
6
+ def write_item_view(view_type, attrs, &proc)
7
+ xml = Builder::XmlMarkup.new
8
+ xml.wsdl view_type, Util::camel_keys(attrs) do
9
+ proc.call(xml) if proc
10
+ end
11
+ xml.target!
12
+ end
13
+ end
14
+
15
+ class Base
16
+ include Util
17
+
18
+ attr_reader :view
19
+ end
20
+
21
+ INDEXED_PAGE_VIEW_OPTS = {:max_entries_returned=>nil, :offset=>0, :base_point=>:Beginning}
22
+
23
+ class IndexedPageItemView < Base
24
+ def initialize(view)
25
+ @view = check_opts(INDEXED_PAGE_VIEW_OPTS, view)
26
+ end
27
+
28
+ def to_xml
29
+ Xml::write_item_view(:IndexedPageItemView, view)
30
+ end
31
+ end
32
+
33
+ class IndexedPageFolderView < Base
34
+ def initialize(view)
35
+ @view = check_opts(INDEXED_PAGE_VIEW_OPTS, view)
36
+ end
37
+
38
+ def to_xml
39
+ Xml::write_item_view(:IndexedPageFolderView, view)
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/rews.rb ADDED
@@ -0,0 +1,55 @@
1
+ $: << File.expand_path("..", __FILE__)
2
+
3
+ require 'net/ntlm'
4
+ require 'httpclient'
5
+ require 'savon'
6
+ require 'fetch_in'
7
+
8
+ # Ruby Exchange Web Services
9
+ module Rews
10
+ WSDL = File.expand_path("../../Services.wsdl", __FILE__)
11
+ SCHEMA_MESSAGES = "http://schemas.microsoft.com/exchange/services/2006/messages"
12
+ SCHEMA_TYPES = "http://schemas.microsoft.com/exchange/services/2006/types"
13
+ end
14
+
15
+ require 'rews/util'
16
+ require 'rews/restriction'
17
+ require 'rews/shape'
18
+ require 'rews/sort_order'
19
+ require 'rews/view'
20
+ require 'rews/folder'
21
+ require 'rews/item'
22
+
23
+ module Rews
24
+ class Client
25
+ attr_reader :client
26
+ attr_accessor :logdev
27
+
28
+ # Rews::Client.new('https://exchange.foo.com/EWS/Exchange.asmx', :ntlm, 'DOMAIN\\user', 'password')
29
+ # Rews::Client.new('https://exchange.foo.com/EWS/Exchange.asmx', :basic, 'DOMAIN\\user', 'password')
30
+ def initialize(endpoint, auth_type, user, password)
31
+ @client = Savon::Client.new do
32
+ wsdl.endpoint = endpoint
33
+ wsdl.namespace = SCHEMA_MESSAGES
34
+
35
+ http.auth.ssl.verify_mode = :none
36
+ http.auth.send(auth_type, user, password)
37
+ end
38
+ end
39
+
40
+ # client.distinguished_folder_id('inbox')
41
+ # client.distinguished_folder_id('inbox', 'foo@bar.com') # to get a folder from another mailbox
42
+ def distinguished_folder_id(id, mailbox_email=nil)
43
+ Folder::DistinguishedFolderId.new(client, id, mailbox_email)
44
+ end
45
+
46
+ def log
47
+ yield logger if @logdev
48
+ end
49
+
50
+ def logger
51
+ return @logger if @logger
52
+ @logger = Logger.new(@logdev) if @logdev
53
+ end
54
+ end
55
+ end
data/spec/rews_spec.rb ADDED
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Rews do
4
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,11 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'rews'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+ require 'rr'
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.mock_with RR::Adapters::Rspec
11
+ end
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rews
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Trampoline Systems Ltd
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-14 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: savon
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 51
30
+ segments:
31
+ - 0
32
+ - 8
33
+ - 6
34
+ version: 0.8.6
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: ntlm-http
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 25
46
+ segments:
47
+ - 0
48
+ - 1
49
+ - 1
50
+ version: 0.1.1
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: httpclient
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 217
62
+ segments:
63
+ - 2
64
+ - 1
65
+ - 6
66
+ - 1
67
+ - 1
68
+ version: 2.1.6.1.1
69
+ type: :runtime
70
+ version_requirements: *id003
71
+ - !ruby/object:Gem::Dependency
72
+ name: fetch_in
73
+ prerelease: false
74
+ requirement: &id004 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 23
80
+ segments:
81
+ - 0
82
+ - 2
83
+ - 0
84
+ version: 0.2.0
85
+ type: :runtime
86
+ version_requirements: *id004
87
+ - !ruby/object:Gem::Dependency
88
+ name: rspec
89
+ prerelease: false
90
+ requirement: &id005 !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 13
96
+ segments:
97
+ - 1
98
+ - 2
99
+ - 9
100
+ version: 1.2.9
101
+ type: :development
102
+ version_requirements: *id005
103
+ - !ruby/object:Gem::Dependency
104
+ name: rr
105
+ prerelease: false
106
+ requirement: &id006 !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ hash: 61
112
+ segments:
113
+ - 0
114
+ - 10
115
+ - 5
116
+ version: 0.10.5
117
+ type: :development
118
+ version_requirements: *id006
119
+ description: an email focussed Ruby client for Exchange Web Services atop Savon
120
+ email: craig@trampolinesystems.com
121
+ executables: []
122
+
123
+ extensions: []
124
+
125
+ extra_rdoc_files:
126
+ - LICENSE
127
+ - README.rdoc
128
+ files:
129
+ - .document
130
+ - LICENSE
131
+ - README.rdoc
132
+ - Rakefile
133
+ - VERSION
134
+ - lib/rews.rb
135
+ - lib/rews/folder.rb
136
+ - lib/rews/item.rb
137
+ - lib/rews/restriction.rb
138
+ - lib/rews/shape.rb
139
+ - lib/rews/sort_order.rb
140
+ - lib/rews/util.rb
141
+ - lib/rews/view.rb
142
+ - spec/rews_spec.rb
143
+ - spec/spec.opts
144
+ - spec/spec_helper.rb
145
+ has_rdoc: true
146
+ homepage: http://github.com/trampoline/rews
147
+ licenses: []
148
+
149
+ post_install_message:
150
+ rdoc_options: []
151
+
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ hash: 3
160
+ segments:
161
+ - 0
162
+ version: "0"
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ hash: 3
169
+ segments:
170
+ - 0
171
+ version: "0"
172
+ requirements: []
173
+
174
+ rubyforge_project:
175
+ rubygems_version: 1.5.2
176
+ signing_key:
177
+ specification_version: 3
178
+ summary: a Ruby client for Exchange Web Services
179
+ test_files:
180
+ - spec/rews_spec.rb
181
+ - spec/spec_helper.rb