stockboy 0.5.4 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4970df2dbcf052ef0b526d6eb4df39d0ed8027ff
4
- data.tar.gz: 01c8f34e325e371d7575cf77a81968ac83f44841
3
+ metadata.gz: f79e82814670601621d41e2bd236f209c0623d26
4
+ data.tar.gz: eb46c2caf2ff5932afbc8eaf41a32064f52c6504
5
5
  SHA512:
6
- metadata.gz: d6810492ff48e3e9774f318023de30e4ddec26dd8fe3b14a0674b9e7b4ae50bec5743538289f4b0fa1c84e115e786ead9eacf6d5da701d21e26667af92d4c523
7
- data.tar.gz: f629334d14fca2a21bcf5c05841744976605084607b38e1aa1cb63e7bd136436bbc9c5330534be51bc9f07be5dd01a90a535267fe140106d404bfc0985ede4c2
6
+ metadata.gz: 041d9ca85788641bb8ee2e834c0488901f7d68207b1af5fb6e10e494d94b1e6d8b507cf06044403d6a2c06b81055dbf96853567ad9c4dcc057bfdcf8856f2aaf
7
+ data.tar.gz: 01ffad83c3db76ea4e97845c970dac4013a557b36044a118a83f577564ebaf13f52ae56dbdd220b1bbc21df82edf91adb7d620f9e011f95b7b6b724b63e72fb9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.5 / 2013-12-12
4
+
5
+ * [BUGFIX] Ensure IMAP connections are reused and closed
6
+ * [ENHANCEMENT] Expose IMAP search options for reuse
7
+
3
8
  ## 0.5.4 / 2013-12-04
4
9
 
5
10
  * [BUGFIX] Fixed broken IMAP client
data/Gemfile CHANGED
@@ -4,7 +4,8 @@ gemspec
4
4
  unless ENV["CI"]
5
5
  group :debug do
6
6
  gem "pry"
7
- gem "pry-debugger"
7
+ gem "pry-debugger" if RUBY_VERSION.start_with? "1.9"
8
+ gem "pry-byebug" if RUBY_VERSION.start_with? "2."
8
9
  end
9
10
  end
10
11
 
@@ -147,7 +147,8 @@ module Stockboy
147
147
  # defined within the context of each job template.
148
148
  #
149
149
  # @param [Symbol] key Name of the trigger
150
- # @param [Trigger, Proc, #call] trigger_class
150
+ # @yieldparam [Stockboy::Job]
151
+ # @yieldparam [Array] Arguments passed to the action when called
151
152
  #
152
153
  # @example
153
154
  # trigger :cleanup do |job, *args|
@@ -1,4 +1,5 @@
1
1
  require 'stockboy/provider'
2
+ require 'stockboy/providers/imap/search_options'
2
3
  require 'net/imap'
3
4
  require 'mail'
4
5
 
@@ -20,9 +21,6 @@ module Stockboy::Providers
20
21
  #
21
22
  class IMAP < Stockboy::Provider
22
23
 
23
- # Corresponds to %v mode in +DateTime#strftime+
24
- VMS_DATE = /\A\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2}\z/i
25
-
26
24
  # @!group Options
27
25
 
28
26
  # Host name or IP address for IMAP server connection
@@ -128,17 +126,6 @@ module Stockboy::Providers
128
126
 
129
127
  # @!endgroup
130
128
 
131
- # Library for connection, defaults to +Net::IMAP+
132
- #
133
- # @!attribute [rw] imap_client
134
- #
135
- def self.imap_client
136
- @imap_client ||= Net::IMAP
137
- end
138
- class << self
139
- attr_writer :imap_client
140
- end
141
-
142
129
  # Initialize a new IMAP reader
143
130
  #
144
131
  def initialize(opts={}, &block)
@@ -158,23 +145,36 @@ module Stockboy::Providers
158
145
  DSL.new(self).instance_eval(&block) if block_given?
159
146
  end
160
147
 
148
+ # Direct access to the configured +Net::IMAP+ connection
149
+ #
150
+ # @example
151
+ # provider.client do |imap|
152
+ # imap.search("FLAGGED")
153
+ # end
154
+ #
161
155
  def client
162
156
  raise(ArgumentError, "no block given") unless block_given?
163
- return yield @open_client if @open_client
164
-
165
- @open_client = ::Net::IMAP.new(host).tap do |i|
166
- i.login(username, password)
167
- i.examine(mailbox)
157
+ first_connection = @open_client.nil?
158
+ if first_connection
159
+ @open_client = ::Net::IMAP.new(host)
160
+ @open_client.login(username, password)
161
+ @open_client.examine(mailbox)
168
162
  end
169
163
  yield @open_client
170
- @open_client.disconnect
171
- @open_client = nil
172
164
  rescue ::Net::IMAP::Error => e
173
165
  errors.add :response, "IMAP connection error"
174
- @open_client.disconnect
175
- @open_client = nil
166
+ ensure
167
+ if first_connection
168
+ @open_client.disconnect
169
+ @open_client = nil
170
+ end
176
171
  end
177
172
 
173
+ # Purge the email from the mailbox corresponding to the [#matching_file]
174
+ #
175
+ # This can only be called after selecting the matching file to confirm the
176
+ # selected item, or after fetching the data
177
+ #
178
178
  def delete_data
179
179
  raise Stockboy::OutOfSequence, "must confirm #matching_message or calling #data" unless picked_matching_message?
180
180
 
@@ -185,12 +185,16 @@ module Stockboy::Providers
185
185
  end
186
186
  end
187
187
 
188
+ # IMAP message id for the email that contains the selected data to process
189
+ #
188
190
  def matching_message
189
191
  return @matching_message if @matching_message
190
- keys = fetch_imap_message_keys
191
- @matching_message = pick_from(keys) unless keys.empty?
192
+ message_ids = search(default_search_options)
193
+ @matching_message = pick_from(message_ids) unless message_ids.empty?
192
194
  end
193
195
 
196
+ # Clear received data and allow for selecting a new item from the server
197
+ #
194
198
  def clear
195
199
  super
196
200
  @matching_message = nil
@@ -198,8 +202,39 @@ module Stockboy::Providers
198
202
  @data_size = nil
199
203
  end
200
204
 
205
+ # Search the selected mailbox for matching messages
206
+ #
207
+ # By default, the configured options are used,
208
+ # @param [Hash, Array, String] options
209
+ # Override default configured search options
210
+ #
211
+ # @example
212
+ # provider.search(subject: "Daily Report", before: Date.today)
213
+ # provider.search(["SUBJECT", "Daily Report", "BEFORE", "21-DEC-12"])
214
+ # provider.search("FLAGGED BEFORE 21-DEC-12")
215
+ #
216
+ def search(options=nil)
217
+ client { |imap| imap.sort(['DATE'], search_keys(options), 'UTF-8') }
218
+ end
219
+
220
+ # Normalize a hash of search options into an array of IMAP search keys
221
+ #
222
+ # @param [Hash] options If none are given, the configured options are used
223
+ # @return [Array]
224
+ #
225
+ def search_keys(options=nil)
226
+ case options
227
+ when Array, String then options
228
+ else SearchOptions.new(options || default_search_options).to_imap
229
+ end
230
+ end
231
+
201
232
  private
202
233
 
234
+ def default_search_options
235
+ {subject: subject, from: from, since: since}
236
+ end
237
+
203
238
  def fetch_data
204
239
  client do |imap|
205
240
  return false unless matching_message
@@ -222,10 +257,6 @@ module Stockboy::Providers
222
257
  errors.empty?
223
258
  end
224
259
 
225
- def fetch_imap_message_keys
226
- client { |imap| imap.sort(['DATE'], search_keys, 'UTF-8') }
227
- end
228
-
229
260
  def picked_matching_message?
230
261
  !!@matching_message
231
262
  end
@@ -241,27 +272,6 @@ module Stockboy::Providers
241
272
  end
242
273
  end
243
274
 
244
- def search_keys
245
- keys = []
246
- keys.concat ['SUBJECT', subject] if subject
247
- keys.concat ['FROM', from] if from
248
- keys.concat ['SINCE', date_format(since)] if since
249
- keys.concat search if search
250
- keys
251
- end
252
-
253
- def date_format(value)
254
- case value
255
- when Date, Time, DateTime
256
- value.strftime('%v')
257
- when Numeric
258
- Time.at(value).strftime('%v')
259
- when String
260
- return value if value =~ VMS_DATE
261
- Date.parse(value).strftime('%v')
262
- end
263
- end
264
-
265
275
  # If activesupport is loaded, it mucks with DateTime#to_time to return
266
276
  # self when it has a utc_offset. Handle both to always return a Time.utc.
267
277
  #
@@ -0,0 +1,121 @@
1
+ require 'stockboy/providers/imap'
2
+
3
+ module Stockboy::Providers
4
+
5
+ # Helper for building standard IMAP options passed to [::Net::IMAP#search]
6
+ #
7
+ class IMAP::SearchOptions
8
+
9
+ # Corresponds to %v mode in +DateTime#strftime+
10
+ VMS_DATE = /\A\d{2}-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2}\z/i
11
+
12
+ OPTION_FORMATS = {
13
+ 'BEFORE' => :date_format,
14
+ 'ON' => :date_format,
15
+ 'SINCE' => :date_format,
16
+ 'SENTBEFORE' => :date_format,
17
+ 'SENTON' => :date_format,
18
+ 'SENTSINCE' => :date_format,
19
+ 'FLAGGED' => :boolean_format,
20
+ 'UNFLAGGED' => :boolean_format,
21
+ 'SEEN' => :boolean_format,
22
+ 'UNSEEN' => :boolean_format,
23
+ 'ANSWERED' => :boolean_format,
24
+ 'UNANSWERED' => :boolean_format,
25
+ 'DELETED' => :boolean_format,
26
+ 'UNDELETED' => :boolean_format,
27
+ 'DRAFT' => :boolean_format,
28
+ 'UNDRAFT' => :boolean_format,
29
+ 'NEW' => :boolean_format,
30
+ 'RECENT' => :boolean_format,
31
+ 'OLD' => :boolean_format
32
+ }
33
+
34
+ # Read options from a hash
35
+ #
36
+ # @param [Hash] options
37
+ #
38
+ def initialize(options={})
39
+ @options = options.each_with_object(Hash.new) do |(k,v), h|
40
+ h[imap_key(k)] = v
41
+ end
42
+ end
43
+
44
+ # Return a hash with merged and normalized key strings
45
+ #
46
+ # @example
47
+ # opt = Stockboy::Providers::IMAP::SearchOptions.new(since: Date.new(2012, 12, 21))
48
+ # opt.to_hash #=> {"SINCE" => #<Date 2012, 12, 21>}
49
+ #
50
+ def to_hash
51
+ @options
52
+ end
53
+
54
+ # Return an array of IMAP search keys
55
+ #
56
+ # @example
57
+ # opt = Stockboy::Providers::IMAP::SearchOptions.new(since: Date.new(2012, 12, 21))
58
+ # opt.to_imap #=> ["SINCE", "21-DEC-12"]
59
+ #
60
+ def to_imap
61
+ @options.reduce([]) do |a, pair|
62
+ a.concat imap_pair(pair)
63
+ end
64
+ end
65
+
66
+ # Convert a rubyish key to IMAP string key format
67
+ #
68
+ # @param [String, Symbol] key
69
+ # @return [String]
70
+ #
71
+ def imap_key(key)
72
+ key.to_s.upcase.gsub(/[^A-Z]/,'').freeze
73
+ end
74
+
75
+ # Format a key-value pair for IMAP, according to the correct type
76
+ #
77
+ # @param [Array] pair
78
+ # @return [Array] pair
79
+ #
80
+ def imap_pair(pair)
81
+ if format = OPTION_FORMATS[pair[0]]
82
+ send(format, pair)
83
+ else
84
+ pair
85
+ end
86
+ end
87
+
88
+ # Format a key-value pair for IMAP date keys (e.g. SINCE, ON, BEFORE)
89
+ #
90
+ # @param [Array] pair
91
+ # @return [Array] pair
92
+ #
93
+ def date_format(pair)
94
+ pair[1] = case value = pair[1]
95
+ when Date, Time, DateTime
96
+ value.strftime('%v')
97
+ when Numeric
98
+ Time.at(value).strftime('%v')
99
+ when String
100
+ value =~ VMS_DATE ? value : Date.parse(value).strftime('%v')
101
+ end
102
+ pair
103
+ end
104
+
105
+ # Format a key-value pair for setting true/false on IMAP keys (e.g. DELETED)
106
+ #
107
+ # @param [Array] pair
108
+ # @return [Array] pair
109
+ #
110
+ def boolean_format(pair)
111
+ return [] unless pair[1] == true || pair[1] == false
112
+
113
+ if pair[1]
114
+ [pair[0]]
115
+ else
116
+ ['NOT', pair[0]]
117
+ end
118
+ end
119
+
120
+ end
121
+ end
@@ -1,4 +1,5 @@
1
1
  require 'stockboy/exceptions'
2
+ require 'stockboy/registry'
2
3
  require 'stockboy/translator'
3
4
 
4
5
  module Stockboy
@@ -7,8 +8,7 @@ module Stockboy
7
8
  # job template DSL.
8
9
  #
9
10
  module Translations
10
-
11
- @registry ||= {}
11
+ extend Stockboy::Registry
12
12
 
13
13
  # Register a translator under a convenient symbolic name
14
14
  #
@@ -54,17 +54,5 @@ module Stockboy
54
54
  end
55
55
  end
56
56
 
57
- # Look up a translation and return it by symbolic name
58
- #
59
- # @param [Symbol] func_name
60
- # @return [Translator]
61
- #
62
- def self.find(func_name)
63
- @registry[func_name]
64
- end
65
- class << self
66
- alias_method :[], :find
67
- end
68
-
69
57
  end
70
58
  end
@@ -1,3 +1,3 @@
1
1
  module Stockboy
2
- VERSION = "0.5.4"
2
+ VERSION = "0.5.5"
3
3
  end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'stockboy/providers/imap/search_options'
3
+
4
+ module Stockboy::Providers
5
+ describe IMAP::SearchOptions do
6
+
7
+ describe "to_hash" do
8
+ it "converts keys to uppercase" do
9
+ options(subject: "Improbability")
10
+ .to_hash.should == {"SUBJECT" => "Improbability"}
11
+ end
12
+ end
13
+
14
+
15
+ describe "to_imap" do
16
+
17
+ it "converts to a flat array of options" do
18
+ options(subject: "Improbability", from: "me@example.com")
19
+ .to_imap.should == ["SUBJECT", "Improbability", "FROM", "me@example.com"]
20
+ end
21
+
22
+ DATE_OPTIONS = { before: "BEFORE",
23
+ on: "ON",
24
+ since: "SINCE",
25
+ sent_before: "SENTBEFORE",
26
+ sent_on: "SENTON",
27
+ sent_since: "SENTSINCE" }
28
+
29
+ DATE_OPTIONS.each do |date_option_symbol, date_option_imap|
30
+ it "converts #{date_option_imap.inspect} to IMAP date option" do
31
+ options(date_option_symbol => Time.new(2012, 12, 12))
32
+ .to_imap.should == [date_option_imap, "12-DEC-2012"]
33
+ end
34
+ end
35
+
36
+ BOOLEAN_OPTIONS = { seen: "SEEN",
37
+ unseen: "UNSEEN",
38
+ flagged: "FLAGGED",
39
+ unflagged: "UNFLAGGED" }
40
+
41
+ BOOLEAN_OPTIONS.each do |bool_option_symbol, bool_option_imap|
42
+ it "converts #{bool_option_imap.inspect} to IMAP single value option" do
43
+ options(bool_option_symbol => true).to_imap.should == [bool_option_imap]
44
+ options(bool_option_symbol => false).to_imap.should == ["NOT", bool_option_imap]
45
+ options(bool_option_symbol => nil).to_imap.should == []
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ def options(*args)
52
+ described_class.new(*args)
53
+ end
54
+
55
+ end
56
+ end
@@ -76,7 +76,7 @@ module Stockboy
76
76
 
77
77
  it "should call delete on the matching message" do
78
78
  allow(provider).to receive(:client).and_yield(imap)
79
- allow(provider).to receive(:fetch_imap_message_keys) { [5] }
79
+ allow(provider).to receive(:search) { [5] }
80
80
 
81
81
  provider.matching_message
82
82
 
@@ -87,5 +87,64 @@ module Stockboy
87
87
  end
88
88
  end
89
89
 
90
+ describe "#client" do
91
+
92
+ before do
93
+ provider.host, provider.username, provider.password = "hhh", "uuu", "ppp"
94
+ provider.mailbox = "UNBOX"
95
+ end
96
+
97
+ it "reuses open connections in nested contexts" do
98
+ net_imap = expect_connection("hhh", "uuu", "ppp", "UNBOX")
99
+
100
+ provider.client do |connection|
101
+ expect(connection).to be net_imap
102
+ provider.client do |i|
103
+ expect(connection).to be net_imap
104
+ end
105
+ end
106
+ end
107
+
108
+ it "closes connections when catching exceptions" do
109
+ net_imap = expect_connection("hhh", "uuu", "ppp", "UNBOX")
110
+ provider.client { |i| raise Net::IMAP::Error }
111
+ provider.errors[:response].should include "IMAP connection error"
112
+ end
113
+
114
+ end
115
+
116
+ describe "#search_keys" do
117
+ it "uses configured options by default" do
118
+ provider.since = Date.new(2012, 12, 21)
119
+ provider.subject = "Earth"
120
+ provider.from = "me@example.com"
121
+
122
+ provider.search_keys.should == [
123
+ "SUBJECT", "Earth",
124
+ "FROM", "me@example.com",
125
+ "SINCE", "21-Dec-2012"
126
+ ]
127
+ end
128
+
129
+ it "replaces defaults with given options" do
130
+ provider.since = Date.new(2012, 12, 21)
131
+ provider.subject = "Earth"
132
+ provider.search_keys(subject: "Improbability").should == ["SUBJECT", "Improbability"]
133
+ end
134
+
135
+ it "returns the same array given" do
136
+ provider.search_keys(["SINCE", "21-DEC-12"]).should == ["SINCE", "21-DEC-12"]
137
+ end
138
+ end
139
+
140
+ def expect_connection(host, user, pass, mailbox)
141
+ net_imap = double("IMAP")
142
+ expect(Net::IMAP).to receive(:new).with(host) { net_imap }
143
+ expect(net_imap).to receive(:login).with(user, pass)
144
+ expect(net_imap).to receive(:examine).with(mailbox)
145
+ expect(net_imap).to receive(:disconnect)
146
+ net_imap
147
+ end
148
+
90
149
  end
91
150
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stockboy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Vit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-05 00:00:00.000000000 Z
11
+ date: 2013-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -146,6 +146,7 @@ files:
146
146
  - lib/stockboy/providers/ftp.rb
147
147
  - lib/stockboy/providers/http.rb
148
148
  - lib/stockboy/providers/imap.rb
149
+ - lib/stockboy/providers/imap/search_options.rb
149
150
  - lib/stockboy/providers/soap.rb
150
151
  - lib/stockboy/railtie.rb
151
152
  - lib/stockboy/reader.rb
@@ -201,6 +202,7 @@ files:
201
202
  - spec/stockboy/providers/file_spec.rb
202
203
  - spec/stockboy/providers/ftp_spec.rb
203
204
  - spec/stockboy/providers/http_spec.rb
205
+ - spec/stockboy/providers/imap/search_options_spec.rb
204
206
  - spec/stockboy/providers/imap_spec.rb
205
207
  - spec/stockboy/providers/soap_spec.rb
206
208
  - spec/stockboy/providers_spec.rb
@@ -247,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
247
249
  version: '0'
248
250
  requirements: []
249
251
  rubyforge_project: stockboy
250
- rubygems_version: 2.0.6
252
+ rubygems_version: 2.0.14
251
253
  signing_key:
252
254
  specification_version: 4
253
255
  summary: Multi-source data normalization library
@@ -279,6 +281,7 @@ test_files:
279
281
  - spec/stockboy/providers/file_spec.rb
280
282
  - spec/stockboy/providers/ftp_spec.rb
281
283
  - spec/stockboy/providers/http_spec.rb
284
+ - spec/stockboy/providers/imap/search_options_spec.rb
282
285
  - spec/stockboy/providers/imap_spec.rb
283
286
  - spec/stockboy/providers/soap_spec.rb
284
287
  - spec/stockboy/providers_spec.rb