sonar_ews_pull_connector 0.2.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,17 @@
1
+ = sonar-ews-pull-connector
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (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)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Trampoline Systems Ltd. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sonar_ews_pull_connector"
8
+ gem.summary = %Q{Exchange Web Services connector for Exchange 2007/2010}
9
+ gem.description = %Q{A sonar-connector for extracting emails from Exchange 2007/2010 through Exchange Web Services}
10
+ gem.email = "craig@trampolinesystems.com"
11
+ gem.homepage = "http://github.com/trampoline/sonar-ews-pull-connector"
12
+ gem.authors = ["mccraigmccraig"]
13
+ gem.add_dependency "sonar_connector", ">= 0.7.2"
14
+ gem.add_dependency "savon", ">= 0.8.6"
15
+ gem.add_dependency "ntlm-http", ">= 0.1.1"
16
+ gem.add_dependency "httpclient", ">= 2.1.6.1.1"
17
+ gem.add_dependency "fetch_in", ">= 0.2.0"
18
+ gem.add_dependency "rfc822_util", ">= 0.1.1"
19
+ gem.add_development_dependency "rspec", ">= 1.2.9"
20
+ gem.add_development_dependency "rr", ">= 0.10.5"
21
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
26
+ end
27
+
28
+ require 'spec/rake/spectask'
29
+ Spec::Rake::SpecTask.new(:spec) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.spec_files = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
35
+ spec.libs << 'lib' << 'spec'
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :spec => :check_dependencies
41
+
42
+ task :default => :spec
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "sonar-ews-pull-connector #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,208 @@
1
+ require 'date'
2
+ require 'json'
3
+ require 'sonar_connector'
4
+ require 'rfc822_util'
5
+ require 'base64'
6
+ require 'md5'
7
+ require 'rews'
8
+
9
+ module Sonar
10
+ module Connector
11
+ class EwsPullConnector < Sonar::Connector::Base
12
+
13
+ MIN_BATCH_SIZE = 2
14
+ DEFAULT_BATCH_SIZE = 100
15
+
16
+ attr_accessor :url
17
+ attr_accessor :auth
18
+ attr_accessor :user
19
+ attr_accessor :password
20
+ attr_accessor :distinguished_folders
21
+ attr_accessor :batch_size
22
+ attr_accessor :delete
23
+ attr_accessor :is_journal
24
+
25
+ def parse(settings)
26
+ ["name", "repeat_delay", "url", "auth", "user", "password", "distinguished_folders", "batch_size"].each do |param|
27
+ raise Sonar::Connector::InvalidConfig.new("#{self.class}: param '#{param}' is blank") if settings[param].blank?
28
+ end
29
+
30
+ @url = settings["url"]
31
+ @auth = settings["auth"]
32
+ @user = settings["user"]
33
+ @password = settings["password"]
34
+ @mailbox_email = settings["mailbox_email"]
35
+ @distinguished_folders = settings["distinguished_folders"]
36
+ @batch_size = [settings["batch_size"] || DEFAULT_BATCH_SIZE, MIN_BATCH_SIZE].max
37
+ @delete = !!settings["delete"]
38
+ @is_journal = !!settings["is_journal"]
39
+ end
40
+
41
+ def inspect
42
+ "#<#{self.class} @url=#{url}, @auth=#{auth}, @user=#{user}, @password=#{password}, @distinguished_folders=#{distinguished_folders}, @batch_size=#{batch_size}, @delete=#{delete}, @is_journal=#{is_journal}>"
43
+ end
44
+
45
+ def distinguished_folder_ids
46
+ return @distinguished_folder_ids if @distinguished_folder_ids
47
+ client ||= Rews::Client.new(url, auth, user, password)
48
+
49
+ @distinguished_folder_ids = @distinguished_folders.inject([]) do |ids, (name, mailbox_email)|
50
+ ids << client.distinguished_folder_id(name, mailbox_email)
51
+ end
52
+ end
53
+
54
+ # find message ids from a folder
55
+ def find(folder_id, offset)
56
+ find_opts = {
57
+ :sort_order=>[["item:DateTimeReceived", "Ascending"]],
58
+ :indexed_page_item_view=>{
59
+ :max_entries_returned=>batch_size,
60
+ :offset=>offset},
61
+ :item_shape=>{
62
+ :base_shape=>:IdOnly}}
63
+
64
+ restriction = [:==, "item:ItemClass", "IPM.Note"]
65
+ if state[folder_id.key]
66
+ restriction = [:and,
67
+ restriction,
68
+ [:>= , "item:DateTimeReceived", state[folder_id.key]]]
69
+ end
70
+ find_opts[:restriction] = restriction
71
+
72
+ folder_id.find_item(find_opts)
73
+ end
74
+
75
+ def get(folder_id, msg_ids)
76
+ get_opts = {
77
+ :item_shape=>{
78
+ :base_shape=>:IdOnly,
79
+ :additional_properties=>[[:field_uri, "item:ItemClass"],
80
+ [:field_uri, "item:DateTimeSent"],
81
+ [:field_uri, "item:DateTimeReceived"],
82
+ [:field_uri, "item:InReplyTo"],
83
+ [:field_uri, "message:InternetMessageId"],
84
+ [:field_uri, "message:References"],
85
+ [:field_uri, "message:From"],
86
+ [:field_uri, "message:Sender"],
87
+ [:field_uri, "message:ToRecipients"],
88
+ [:field_uri, "message:CcRecipients"],
89
+ [:field_uri, "message:BccRecipients"]]}}
90
+
91
+ # we have to retrieve the journal message content and unwrap the
92
+ # original message if this
93
+ # is an exchange journal mailbox
94
+ if is_journal
95
+ get_opts[:item_shape][:additional_properties] << [:field_uri, "item:MimeContent"]
96
+ end
97
+
98
+ folder_id.get_item(msg_ids, get_opts)
99
+ end
100
+
101
+ def action
102
+ distinguished_folder_ids.each do |fid|
103
+ log.info "processing: #{fid.inspect}"
104
+
105
+ offset = 0
106
+
107
+ begin
108
+ msg_ids = find(fid, offset)
109
+
110
+ if msg_ids && msg_ids.length>0
111
+ msgs = get(fid, msg_ids)
112
+
113
+ # if there is no state, then state is set to the first message timestamp
114
+ state[fid.key] ||= msgs.first[:date_time_received].to_s if msgs.first[:date_time_received]
115
+
116
+ save_messages(msgs)
117
+
118
+ if msgs.last[:date_time_received] != state[fid.key]
119
+ finished=true
120
+ state[fid.key] = msgs.last[:date_time_received].to_s
121
+ end
122
+
123
+ delete_messages(fid, msgs) if delete
124
+
125
+ offset += msg_ids.length
126
+ end
127
+ end while msg_ids.length>0 && !finished
128
+
129
+ save_state
130
+
131
+ log.info "finished processing: #{fid.inspect}"
132
+ end
133
+ log.info "finished action"
134
+ end
135
+
136
+ def save_messages(messages)
137
+ messages.each do |msg|
138
+ h = if is_journal
139
+ extract_journalled_message(msg)
140
+ else
141
+ message_to_hash(msg)
142
+ end
143
+
144
+ if !h
145
+ log.warn("no data extracted from message. could be a decoding eror")
146
+ return
147
+ end
148
+
149
+ h[:type] = "email"
150
+ h[:connector] = name
151
+ h[:source] = url
152
+ h[:source_id]=msg[:item_id][:id]
153
+ h[:received_at] = msg[:date_time_received]
154
+
155
+
156
+ fname = MD5.hexdigest(msg[:item_id][:id])
157
+ filestore.write(:complete, "#{fname}.json", h.to_json)
158
+ end
159
+ end
160
+
161
+ def mailbox_to_hash(mailbox)
162
+ [:name, :email_address].inject({}) do |h, k|
163
+ h[k] = mailbox[k]
164
+ h
165
+ end
166
+ end
167
+
168
+ def mailbox_recipients_to_hashes(recipients)
169
+ mailboxes = recipients[:mailbox] if recipients
170
+ mailboxes = [mailboxes] if !mailboxes.is_a?(Array)
171
+ mailboxes.compact.map{|a| mailbox_to_hash(a)}
172
+ end
173
+
174
+ def message_to_hash(msg)
175
+ message_id = Rfc822Util.strip_header(msg[:internet_message_id]) if msg[:internet_message_id]
176
+ in_reply_to = Rfc822Util.strip_headers(msg[:in_reply_to]).first if msg[:in_reply_to]
177
+ references = Rfc822Util.strip_headers(msg[:references]) if msg[:references]
178
+
179
+ json_hash = {
180
+ :message_id=>message_id,
181
+ :sent_at=>msg[:date_time_sent].to_s,
182
+ :in_reply_to=>in_reply_to,
183
+ :references=>references,
184
+ :from=>mailbox_recipients_to_hashes(msg[:from]).first,
185
+ :sender=>mailbox_recipients_to_hashes(msg[:sender]).first,
186
+ :to=>mailbox_recipients_to_hashes(msg[:to_recipients]),
187
+ :cc=>mailbox_recipients_to_hashes(msg[:cc_recipients]),
188
+ :bcc=>mailbox_recipients_to_hashes(msg[:bcc_recipients])
189
+ }
190
+
191
+ end
192
+
193
+ def extract_journalled_message(message)
194
+ mime_msg = Base64::decode64(message[:mime_content])
195
+ journal_msg = Rfc822Util.extract_journalled_mail(mime_msg)
196
+ Rfc822Util.mail_to_hash(journal_msg)
197
+ rescue Exception=>e
198
+ log.warn("problem extracting journalled message from wrapper message")
199
+ log.warn(e)
200
+ end
201
+
202
+ def delete_messages(folder_id, messages)
203
+ log.info "deleting #{messages.length} messages from #{folder_id.inspect}"
204
+ folder_id.delete_item(messages, :delete_type=>:HardDelete)
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path("../ews_pull_connector/ews_pull_connector", __FILE__)
@@ -0,0 +1,490 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ module Sonar
4
+ module Connector
5
+ describe EwsPullConnector do
6
+
7
+ before do
8
+ setup_valid_config_file
9
+ @base_config = Sonar::Connector::Config.load(valid_config_filename)
10
+ end
11
+
12
+ def one_folder_config
13
+ {
14
+ 'name'=>'foobarcom-exchange',
15
+ 'repeat_delay'=>60,
16
+ 'url'=>"https://foo.com/EWS/Exchange.asmx",
17
+ 'auth'=>'ntlm',
18
+ 'user'=>"foo",
19
+ 'password'=>"foopass",
20
+ 'distinguished_folders'=>[["inbox", "foo@foo.com"]],
21
+ 'batch_size'=>100,
22
+ 'delete'=>true
23
+ }
24
+ end
25
+
26
+ def two_folder_config
27
+ {
28
+ 'name'=>'foobarcom-exchange',
29
+ 'repeat_delay'=>60,
30
+ 'url'=>"https://foo.com/EWS/Exchange.asmx",
31
+ 'auth'=>'ntlm',
32
+ 'user'=>"foo",
33
+ 'password'=>"foopass",
34
+ 'distinguished_folders'=>[["inbox", "foo@foo.com"], "inbox"],
35
+ 'batch_size'=>100,
36
+ 'delete'=>true
37
+ }
38
+ end
39
+
40
+ it "should parse config" do
41
+ Sonar::Connector::EwsPullConnector.new(two_folder_config, @base_config)
42
+ end
43
+
44
+ describe "distinguished_folder_ids" do
45
+ it "should create Rews::Clients for each configured distinguished folder" do
46
+ c=Sonar::Connector::EwsPullConnector.new(two_folder_config, @base_config)
47
+ fids = c.distinguished_folder_ids
48
+ fids.size.should == 2
49
+
50
+ fids[0].client.should be(fids[1].client)
51
+ client = fids[0].client
52
+ client.endpoint.should == "https://foo.com/EWS/Exchange.asmx"
53
+ client.auth_type.should == "ntlm"
54
+ client.user.should == 'foo'
55
+ client.password.should == 'foopass'
56
+
57
+ fid0 = fids[0]
58
+ fid0.id.should == 'inbox'
59
+ fid0.mailbox_email.should == 'foo@foo.com'
60
+ fid0.key.should == ['distinguished_folder', 'inbox', 'foo@foo.com']
61
+
62
+ fid1 = fids[1]
63
+ fid1.id.should == 'inbox'
64
+ fid1.mailbox_email.should == nil
65
+ fid1.key.should == ['distinguished_folder', 'inbox']
66
+ end
67
+
68
+ it "should cache the Rews::Clients" do
69
+ c=Sonar::Connector::EwsPullConnector.new(two_folder_config, @base_config)
70
+ fids = c.distinguished_folder_ids
71
+ fid0 = fids[0]
72
+ fid1 = fids[1]
73
+
74
+ fids = c.distinguished_folder_ids
75
+ fid0.should be(fids[0])
76
+ fid1.should be(fids[1])
77
+ end
78
+ end
79
+
80
+ describe "find" do
81
+ it "should include batch_size and offset but not item:DateTimeReceived restriction if there is no folder state" do
82
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
83
+ state={}
84
+ stub(c.state){state}
85
+ stub(c).batch_size{17}
86
+
87
+ folder_id = Object.new
88
+ folder_key = Object.new
89
+ stub(folder_id).key{folder_key}
90
+
91
+ mock(folder_id).find_item.with_any_args do |find_opts|
92
+ find_opts.should == {
93
+ :sort_order=>[["item:DateTimeReceived", "Ascending"]],
94
+ :indexed_page_item_view=>{
95
+ :max_entries_returned=>17,
96
+ :offset=>123},
97
+ :item_shape=>{
98
+ :base_shape=>:IdOnly},
99
+ :restriction=>[:==, "item:ItemClass", "IPM.Note"]}
100
+ end
101
+
102
+ c.find(folder_id, 123)
103
+ end
104
+
105
+ it "should include a item:DateTimeReceived restriction if there is folder state" do
106
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
107
+ stub(c).batch_size{17}
108
+
109
+ folder_id = Object.new
110
+ folder_key = Object.new
111
+ stub(folder_id).key{folder_key}
112
+
113
+ state_time = DateTime.now.to_s
114
+ stub(c).state.stub!.[](folder_key){state_time}
115
+
116
+ mock(folder_id).find_item.with_any_args do |find_opts|
117
+ find_opts.should == {
118
+ :sort_order=>[["item:DateTimeReceived", "Ascending"]],
119
+ :indexed_page_item_view=>{
120
+ :max_entries_returned=>17,
121
+ :offset=>123},
122
+ :item_shape=>{
123
+ :base_shape=>:IdOnly},
124
+ :restriction=>[:and,
125
+ [:==, "item:ItemClass", "IPM.Note"],
126
+ [:>=, "item:DateTimeReceived", state_time]]}
127
+ end
128
+
129
+ c.find(folder_id, 123)
130
+ end
131
+ end
132
+
133
+ describe "get" do
134
+ it "should not fetch message content if !is_journal" do
135
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
136
+
137
+ folder_id = Object.new
138
+ msg_ids = Object.new
139
+
140
+ mock(folder_id).get_item.with_any_args do |mids, get_opts|
141
+ mids.should be(msg_ids)
142
+ get_opts.should == {
143
+ :item_shape=>{
144
+ :base_shape=>:IdOnly,
145
+ :additional_properties=>[[:field_uri, "item:ItemClass"],
146
+ [:field_uri, "item:DateTimeSent"],
147
+ [:field_uri, "item:DateTimeReceived"],
148
+ [:field_uri, "item:InReplyTo"],
149
+ [:field_uri, "message:InternetMessageId"],
150
+ [:field_uri, "message:References"],
151
+ [:field_uri, "message:From"],
152
+ [:field_uri, "message:Sender"],
153
+ [:field_uri, "message:ToRecipients"],
154
+ [:field_uri, "message:CcRecipients"],
155
+ [:field_uri, "message:BccRecipients"]]}}
156
+ end
157
+
158
+ c.get(folder_id, msg_ids)
159
+ end
160
+
161
+ it "should fetch message content if is_journal" do
162
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
163
+ stub(c).is_journal{true}
164
+
165
+ folder_id = Object.new
166
+ msg_ids = Object.new
167
+
168
+ mock(folder_id).get_item.with_any_args do |mids, get_opts|
169
+ mids.should be(msg_ids)
170
+ get_opts.should == {
171
+ :item_shape=>{
172
+ :base_shape=>:IdOnly,
173
+ :additional_properties=>[[:field_uri, "item:ItemClass"],
174
+ [:field_uri, "item:DateTimeSent"],
175
+ [:field_uri, "item:DateTimeReceived"],
176
+ [:field_uri, "item:InReplyTo"],
177
+ [:field_uri, "message:InternetMessageId"],
178
+ [:field_uri, "message:References"],
179
+ [:field_uri, "message:From"],
180
+ [:field_uri, "message:Sender"],
181
+ [:field_uri, "message:ToRecipients"],
182
+ [:field_uri, "message:CcRecipients"],
183
+ [:field_uri, "message:BccRecipients"],
184
+ [:field_uri, "item:MimeContent"]]}}
185
+ end
186
+
187
+ c.get(folder_id, msg_ids)
188
+ end
189
+ end
190
+
191
+ describe "action" do
192
+ it "should make a Rews find_item request, save, update state, delete" do
193
+ c=Sonar::Connector::EwsPullConnector.new(two_folder_config, @base_config)
194
+ state = {}
195
+ stub(c).state{state}
196
+
197
+ c.distinguished_folder_ids.each do |fid|
198
+ msg_ids = Object.new
199
+ stub(msg_ids).length{1}
200
+
201
+ msgs = Object.new
202
+ stub(msgs).first.stub!.[](:date_time_received){ DateTime.now-1 }
203
+ stub(msgs).last.stub!.[](:date_time_received){ DateTime.now }
204
+
205
+ mock(fid).find_item(anything){msg_ids}
206
+ mock(fid).get_item(msg_ids, anything){msgs}
207
+
208
+ mock(c).save_messages(msgs)
209
+ mock(c).delete_messages(fid, msgs)
210
+ end
211
+ c.action
212
+ end
213
+
214
+ it "should have a single item:ItemClass Restriction clause if fstate is nil" do
215
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
216
+ fid = c.distinguished_folder_ids.first
217
+
218
+ msg_ids = Object.new
219
+ stub(msg_ids).length{1}
220
+ mock(fid).find_item(anything) do |query_opts|
221
+ r = query_opts[:restriction]
222
+ r.should == [:==, "item:ItemClass", "IPM.Note"]
223
+ msg_ids
224
+ end
225
+
226
+ msgs=Object.new
227
+ stub(msgs).first.stub!.[](:date_time_received){ DateTime.now-1 }
228
+ stub(msgs).last.stub!.[](:date_time_received){DateTime.now}
229
+
230
+ mock(fid).get_item(msg_ids, anything){msgs}
231
+ mock(c).save_messages(msgs)
232
+ mock(c).delete_messages(fid, msgs)
233
+
234
+ c.action
235
+ end
236
+
237
+ it "should have a second item:DateTimeReceived Restriction clause if fstate is non-nil" do
238
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
239
+
240
+ fid = c.distinguished_folder_ids.first
241
+
242
+ state_time = DateTime.now - 1
243
+ state = {fid.key => state_time.to_s}
244
+ stub(c).state{state}
245
+
246
+ msg_ids = Object.new
247
+ stub(msg_ids).length{1}
248
+ mock(fid).find_item(anything) do |query_opts|
249
+ r = query_opts[:restriction]
250
+ r[0].should == :and
251
+ r[1].should == [:==, "item:ItemClass", "IPM.Note"]
252
+ r[2].should == [:>=, "item:DateTimeReceived", state_time.to_s]
253
+ msg_ids
254
+ end
255
+
256
+ msgs = Object.new
257
+ stub(msgs).first.stub!.[](:date_time_received){state_time}
258
+ stub(msgs).last.stub!.[](:date_time_received){DateTime.now}
259
+
260
+ mock(fid).get_item(msg_ids, anything){msgs}
261
+ mock(c).save_messages(msgs)
262
+ mock(c).delete_messages(fid, msgs)
263
+
264
+ c.action
265
+ end
266
+
267
+ it "should cycle through messages with identical item:DateTimeReceived, so state is always updated even if 'delete' option is false" do
268
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
269
+
270
+ fid = c.distinguished_folder_ids.first
271
+
272
+ state_time = (DateTime.now - 2).to_s
273
+ state = {fid.key => state_time}
274
+ stub(c).state{state}
275
+
276
+ msg_ids = Object.new
277
+ stub(msg_ids).length{10}
278
+ msgs = Object.new
279
+ stub(msgs).first.stub!.[](:date_time_received){state_time}
280
+ stub(msgs).last.stub!.[](:date_time_received){state_time}
281
+
282
+ more_msg_ids = Object.new
283
+ stub(more_msg_ids).length{1}
284
+ more_msgs = Object.new
285
+ stub(more_msgs).first.stub!.[](:date_time_received){state_time}
286
+ stub(more_msgs).last.stub!.[](:date_time_received){DateTime.now}
287
+
288
+ mock(fid).find_item.with_any_args.twice do |opts|
289
+ if opts[:indexed_page_item_view][:offset]==0
290
+ msg_ids
291
+ elsif opts[:indexed_page_item_view][:offset]==10
292
+ more_msg_ids
293
+ else
294
+ raise "oops"
295
+ end
296
+ end
297
+
298
+ mock(fid).get_item(msg_ids, anything){msgs}
299
+ mock(c).save_messages(msgs)
300
+ mock(c).delete_messages(fid, msgs)
301
+
302
+ mock(fid).get_item(more_msg_ids, anything){more_msgs}
303
+ mock(c).save_messages(more_msgs)
304
+ mock(c).delete_messages(fid, more_msgs)
305
+
306
+ c.action
307
+ end
308
+
309
+ it "should terminate the fetch loop if no messages are returned" do
310
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
311
+
312
+ fid = c.distinguished_folder_ids.first
313
+
314
+ state_time = (DateTime.now - 1).to_s
315
+ state = {fid.key => state_time}
316
+ stub(c).state{state}
317
+
318
+ msg_ids = Object.new
319
+ stub(msg_ids).length{0}
320
+ msgs = Object.new
321
+
322
+ mock(fid).find_item.with_any_args do |opts|
323
+ opts[:indexed_page_item_view][:offset].should == 0
324
+ msg_ids
325
+ end
326
+
327
+ c.action
328
+ end
329
+
330
+
331
+ end
332
+
333
+ describe "mailbox_to_hash" do
334
+ it "should keep only :name and :email_address keys of a Rews address hash" do
335
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
336
+ mb = {:name=>"foo bar", :email_address=>"foo@bar.com", :blah=>"blah"}
337
+ c.mailbox_to_hash(mb).should == {:name=>"foo bar", :email_address=>"foo@bar.com"}
338
+ end
339
+ end
340
+
341
+ describe "mailbox_recipients_to_hashes" do
342
+ it "should convert a nil recipient to an empty list" do
343
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
344
+ mbr = {:mailbox=>nil}
345
+ c.mailbox_recipients_to_hashes(mbr).should == []
346
+ end
347
+
348
+ it "should convert a single recipient to a list of one hash" do
349
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
350
+ mbr = {:mailbox=>{:name=>"foo bar", :email_address=>"foo@bar.com", :blah=>"blah"}}
351
+ c.mailbox_recipients_to_hashes(mbr).should == [{:name=>"foo bar", :email_address=>"foo@bar.com"}]
352
+ end
353
+
354
+ it "should convert multiple recipients to a list of hashes" do
355
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
356
+ mbr = {:mailbox=>[{:name=>"foo bar", :email_address=>"foo@bar.com", :blah=>"blah"},
357
+ {:name=>"baz mcbaz", :email_address=>"baz.mcbaz@baz.com"}]}
358
+ c.mailbox_recipients_to_hashes(mbr).should == [{:name=>"foo bar", :email_address=>"foo@bar.com"},
359
+ {:name=>"baz mcbaz", :email_address=>"baz.mcbaz@baz.com"}]
360
+ end
361
+ end
362
+
363
+ describe "message_to_hash" do
364
+ it "should convert a message Rews::Item::Item to a hash" do
365
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
366
+ sent_at = DateTime.now-1
367
+ m = Rews::Item::Item.new(c,
368
+ :message,
369
+ :item_id=>{:id=>"abc", :change_key=>"def"},
370
+ :internet_message_id=>"<abc123>",
371
+ :date_time_sent=>sent_at,
372
+ :date_time_received=>DateTime.now,
373
+ :in_reply_to=>"<foo>",
374
+ :references=>["<foo>", "<bar>"],
375
+ :from=>{:mailbox=>{:name=>"foo mcfoo", :email_address=>"foo.mcfoo@foo.com"}},
376
+ :sender=>{:mailbox=>{:name=>"mrs mcmrs", :email_address=>"mrs.mcmrs@foo.com"}},
377
+ :to_recipients=>{:mailbox=>[{:name=>"bar mcbar", :email_address=>"bar.mcbar@bar.com"}, {:name=>"baz mcbaz", :email_address=>"baz.mcbaz@baz.com"}]},
378
+ :cc_recipients=>{:mailbox=>{:name=>"woo wuwoo", :email_address=>"woo.wuwoo@woo.com"}},
379
+ :bcc_recipients=>{:mailbox=>{:name=>"fee mcfee", :email_address=>"fee.mcfee@fee.com"}})
380
+ h=c.message_to_hash(m)
381
+ h.should == {
382
+ :message_id=>"abc123",
383
+ :sent_at=>sent_at.to_s,
384
+ :in_reply_to=>"foo",
385
+ :references=>["foo", "bar"],
386
+ :from=>{:name=>"foo mcfoo", :email_address=>"foo.mcfoo@foo.com"},
387
+ :sender=>{:name=>"mrs mcmrs", :email_address=>"mrs.mcmrs@foo.com"},
388
+ :to=>[{:name=>"bar mcbar", :email_address=>"bar.mcbar@bar.com"},
389
+ {:name=>"baz mcbaz", :email_address=>"baz.mcbaz@baz.com"}],
390
+ :cc=>[{:name=>"woo wuwoo", :email_address=>"woo.wuwoo@woo.com"}],
391
+ :bcc=>[{:name=>"fee mcfee", :email_address=>"fee.mcfee@fee.com"}]
392
+ }
393
+ end
394
+ end
395
+
396
+ describe "save_messages" do
397
+ def check_saved_msg(c, msg, json_msg)
398
+ h = JSON.parse(json_msg)
399
+ h["type"].should == "email"
400
+ h["connector"].should == c.name
401
+ h["source"].should == c.url
402
+ h["source_id"].should == msg[:item_id][:id]
403
+
404
+ end
405
+
406
+ it "should save a file for each message result" do
407
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
408
+
409
+
410
+ msg1 = {
411
+ :item_id=>{:id=>"abc", :change_key=>"def"},
412
+ "item:DateTimeSent"=>DateTime.now-1,
413
+ "item:DateTimeReceived"=>DateTime.now,
414
+ "item:InReplyTo"=>"foo",
415
+ "message:InternetMessageId"=>"foobar",
416
+ "message:References"=>"barbar",
417
+ "messsage:From"=>"foo@bar.com",
418
+ "message:Sender"=>"foo@bar.com",
419
+ "message:ToRecipients"=>"baz@bar.com",
420
+ "message:CcRecipients"=>"abc@def.com",
421
+ "message:BccRecipients"=>"boss@foo.com"
422
+ }
423
+ msg2 = {
424
+ :item_id=>{:id=>"ghi", :change_key=>"jkl"}}
425
+
426
+ msgs = [msg1, msg2]
427
+
428
+ mock(c.filestore).write(:complete, "#{MD5.hexdigest('abc')}.json", anything) do |*args|
429
+ check_saved_msg(c, msg1, args.last)
430
+ end
431
+ mock(c.filestore).write(:complete, "#{MD5.hexdigest('ghi')}.json", anything) do |*args|
432
+ check_saved_msg(c, msg2, args.last)
433
+ end
434
+
435
+ c.save_messages(msgs)
436
+ end
437
+
438
+ it "should log and continue if the extracted message is nil" do
439
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
440
+ stub(c).is_journal{true}
441
+
442
+ msg = Object.new
443
+ msgs=[msg]
444
+ stub(c).extract_journalled_message(msg){nil}
445
+
446
+ stub(c.log).warn(/no data extracted/)
447
+ dont_allow(c.filestore).write
448
+
449
+ c.save_messages(msgs)
450
+ end
451
+ end
452
+
453
+ describe "extract_journalled_message" do
454
+ it "should catch any exception extracting the message, log a warning and return nil" do
455
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
456
+
457
+ msg = Object.new
458
+ mime_content = Object.new
459
+ stub(msg).[](:mime_content){mime_content}
460
+ mime_msg = Object.new
461
+ stub(Base64).decode64(mime_content){mime_msg}
462
+ e = begin ; raise "bang" ; rescue Exception=>e ; e ; end
463
+ stub(Rfc822Util).extract_journalled_mail(mime_msg){raise e}
464
+
465
+ stub(c.log).warn(/problem/)
466
+ stub(c.log).warn(e)
467
+
468
+ lambda{
469
+ c.extract_journalled_message(msg)
470
+ }.should_not raise_error
471
+ end
472
+ end
473
+
474
+ describe "delete_messages" do
475
+ it "should HardDelete all messages from a result" do
476
+ c=Sonar::Connector::EwsPullConnector.new(one_folder_config, @base_config)
477
+ fid = c.distinguished_folder_ids.first
478
+
479
+ msgs = Object.new
480
+ mock(msgs).length{1}
481
+
482
+ mock(fid).delete_item(msgs, :delete_type=>:HardDelete)
483
+
484
+ c.delete_messages(fid, msgs)
485
+ end
486
+ end
487
+
488
+ end
489
+ end
490
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+ require 'rr'
7
+ require 'ews_pull_connector/ews_pull_connector'
8
+ require 'sonar_connector/rspec/spec_helper'
9
+
10
+ Spec::Runner.configure do |config|
11
+ config.mock_with RR::Adapters::Rspec
12
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sonar_ews_pull_connector
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - mccraigmccraig
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-03-25 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: sonar_connector
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 0
32
+ - 7
33
+ - 2
34
+ version: 0.7.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: savon
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 51
46
+ segments:
47
+ - 0
48
+ - 8
49
+ - 6
50
+ version: 0.8.6
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: ntlm-http
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 25
62
+ segments:
63
+ - 0
64
+ - 1
65
+ - 1
66
+ version: 0.1.1
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: httpclient
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 217
78
+ segments:
79
+ - 2
80
+ - 1
81
+ - 6
82
+ - 1
83
+ - 1
84
+ version: 2.1.6.1.1
85
+ type: :runtime
86
+ version_requirements: *id004
87
+ - !ruby/object:Gem::Dependency
88
+ name: fetch_in
89
+ prerelease: false
90
+ requirement: &id005 !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 23
96
+ segments:
97
+ - 0
98
+ - 2
99
+ - 0
100
+ version: 0.2.0
101
+ type: :runtime
102
+ version_requirements: *id005
103
+ - !ruby/object:Gem::Dependency
104
+ name: rfc822_util
105
+ prerelease: false
106
+ requirement: &id006 !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ hash: 25
112
+ segments:
113
+ - 0
114
+ - 1
115
+ - 1
116
+ version: 0.1.1
117
+ type: :runtime
118
+ version_requirements: *id006
119
+ - !ruby/object:Gem::Dependency
120
+ name: rspec
121
+ prerelease: false
122
+ requirement: &id007 !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ hash: 13
128
+ segments:
129
+ - 1
130
+ - 2
131
+ - 9
132
+ version: 1.2.9
133
+ type: :development
134
+ version_requirements: *id007
135
+ - !ruby/object:Gem::Dependency
136
+ name: rr
137
+ prerelease: false
138
+ requirement: &id008 !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ hash: 61
144
+ segments:
145
+ - 0
146
+ - 10
147
+ - 5
148
+ version: 0.10.5
149
+ type: :development
150
+ version_requirements: *id008
151
+ description: A sonar-connector for extracting emails from Exchange 2007/2010 through Exchange Web Services
152
+ email: craig@trampolinesystems.com
153
+ executables: []
154
+
155
+ extensions: []
156
+
157
+ extra_rdoc_files:
158
+ - LICENSE
159
+ - README.rdoc
160
+ files:
161
+ - .document
162
+ - LICENSE
163
+ - README.rdoc
164
+ - Rakefile
165
+ - VERSION
166
+ - lib/ews_pull_connector/ews_pull_connector.rb
167
+ - lib/sonar_ews_pull_connector.rb
168
+ - spec/ews_pull_connector/ews_pull_connector_spec.rb
169
+ - spec/spec.opts
170
+ - spec/spec_helper.rb
171
+ has_rdoc: true
172
+ homepage: http://github.com/trampoline/sonar-ews-pull-connector
173
+ licenses: []
174
+
175
+ post_install_message:
176
+ rdoc_options: []
177
+
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ none: false
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ hash: 3
186
+ segments:
187
+ - 0
188
+ version: "0"
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ none: false
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ hash: 3
195
+ segments:
196
+ - 0
197
+ version: "0"
198
+ requirements: []
199
+
200
+ rubyforge_project:
201
+ rubygems_version: 1.6.2
202
+ signing_key:
203
+ specification_version: 3
204
+ summary: Exchange Web Services connector for Exchange 2007/2010
205
+ test_files:
206
+ - spec/ews_pull_connector/ews_pull_connector_spec.rb
207
+ - spec/spec_helper.rb