sonar_ews_pull_connector 0.2.0

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