stockboy 0.5.4 → 0.5.5

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