imap_guard 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -49,7 +49,7 @@ For instance, the pattern passed to `subject` and `from` is a mere string.
49
49
  IMAP doesn't allow advanced filtering such as regexp matching.
50
50
 
51
51
  To do so, you can pass an optional block to `delete`.
52
- The yielded object is a [Mail](https://github.com/mikel/mail) instance of the current mail providing many methods.
52
+ The yielded object is a [Mail] instance of the current mail providing many methods.
53
53
  However, wrapping the mail into a nice `Mail` object is slow and you should avoid to use it if you can.
54
54
 
55
55
  ```ruby
@@ -84,6 +84,36 @@ guard.move query, 'destination_folder' do |mail|
84
84
  end
85
85
  ```
86
86
 
87
+ ### Advanced features
88
+
89
+ #### Mailbox list
90
+
91
+ You can list all mailboxes:
92
+
93
+ ```ruby
94
+ p guard.list
95
+ ```
96
+
97
+ #### Selected mailbox
98
+
99
+ You can output the currently selected mailbox:
100
+
101
+ ```ruby
102
+ p guard.mailbox # nil if none has been selected
103
+ ```
104
+
105
+ #### Debug block
106
+
107
+ You can pass a block which will be yielded for each matched email:
108
+
109
+ ```ruby
110
+ # Print out the subject for each email
111
+ guard.debug = ->(mail) { print "#{mail.subject}: " }
112
+ ```
113
+
114
+ You can think of it as Ruby's [Object#tap](http://ruby-doc.org/core-2.0/Object.html#method-i-tap) method.
115
+ Note this is slow since it needs to fetch the whole email to return a [Mail] object.
116
+
87
117
  ## Contributing
88
118
 
89
119
  Bug reports and patches are most welcome.
@@ -92,3 +122,6 @@ Bug reports and patches are most welcome.
92
122
 
93
123
  MIT
94
124
 
125
+
126
+ [Mail]: https://github.com/mikel/mail
127
+
@@ -4,53 +4,98 @@ require 'mail'
4
4
  require 'colored'
5
5
 
6
6
  module IMAPGuard
7
+ # Guard allows you to process your mailboxes.
7
8
  class Guard
9
+ # [Proc] Matched emails are passed to this debug lambda if present
10
+ attr_accessor :debug
11
+
12
+ # @return [OpenStruct] IMAPGuard settings
8
13
  attr_reader :settings
9
14
 
15
+ # @return [String] Currently selected mailbox
16
+ attr_reader :mailbox
17
+
10
18
  def initialize settings
11
19
  self.settings = settings
12
20
  end
13
21
 
22
+ # Authenticates to the given IMAP server
14
23
  # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/net/imap/rdoc/Net/IMAP.html#method-c-new
24
+ # @return [void]
15
25
  def login
16
26
  @imap = Net::IMAP.new(@settings.host, @settings.port, true, nil, false)
17
27
  @imap.login(@settings.username, @settings.password)
18
28
  verbose.puts "Logged in successfully"
19
29
  end
20
30
 
31
+ # Selects a mailbox (folder)
32
+ # @see {settings.read_only}
33
+ # @return [void]
21
34
  def select mailbox
22
35
  if @settings.read_only
23
36
  @imap.examine(mailbox) # open in read-only
24
37
  else
25
38
  @imap.select(mailbox) # open in read-write
26
39
  end
40
+ @mailbox = mailbox
27
41
  end
28
42
 
43
+ # Moves messages matching the query and filter block
44
+ # @param query IMAP query
29
45
  # @param mailbox Destination mailbox
46
+ # @param filter Optional filter block
47
+ # @return [void]
30
48
  def move query, mailbox, &filter
31
49
  operation = lambda { |message_id|
32
- @imap.copy(message_id, mailbox) unless @settings.read_only
33
- @imap.store(message_id, "+FLAGS", [:Deleted])
34
- "moved to #{mailbox}".cyan
50
+ unless @settings.read_only
51
+ @imap.copy(message_id, mailbox)
52
+ @imap.store(message_id, "+FLAGS", [Net::IMAP::DELETED])
53
+ end
54
+
55
+ "moved to #{mailbox}".yellow
35
56
  }
36
57
  process query, operation, &filter
37
58
  end
38
59
 
60
+ # Deletes messages matching the query and filter block
61
+ # @param query IMAP query
62
+ # @param filter Optional filter block
63
+ # @return [void]
39
64
  def delete query, &filter
40
65
  operation = lambda { |message_id|
41
- @imap.store(message_id, "+FLAGS", [:Deleted])
66
+ unless @settings.read_only
67
+ @imap.store(message_id, "+FLAGS", [Net::IMAP::DELETED])
68
+ end
69
+
42
70
  'deleted'.red
43
71
  }
44
72
  process query, operation, &filter
45
73
  end
46
74
 
75
+ # @return [Array] Sorted list of all mailboxes
76
+ def list
77
+ @imap.list("", "*").map(&:name).sort
78
+ end
79
+
80
+ # Sends a EXPUNGE command to permanently remove from the currently selected
81
+ # mailbox all messages that have the Deleted flag set.
82
+ # @return [void]
47
83
  def expunge
48
- @imap.expunge
84
+ @imap.expunge unless @settings.read_only
49
85
  end
50
86
 
87
+ # Sends a CLOSE command to close the currently selected mailbox. The CLOSE
88
+ # command permanently removes from the mailbox all messages that have the
89
+ # Deleted flag set.
90
+ # @return [void]
51
91
  def close
52
- puts "Expunging deleted messages and closing mailbox..."
53
- @imap.close
92
+ @imap.close unless @settings.read_only
93
+ end
94
+
95
+ # Disconnects from the server.
96
+ # @return [void]
97
+ def disconnect
98
+ @imap.disconnect
54
99
  end
55
100
 
56
101
  private
@@ -60,35 +105,44 @@ module IMAPGuard
60
105
  count = message_ids.size
61
106
 
62
107
  message_ids.each_with_index do |message_id, index|
63
- print "Processing UID #{message_id} (#{index + 1}/#{count}): "
108
+ print "Processing UID #{message_id} (#{index.succ}/#{count}): "
64
109
 
65
110
  result = true
66
- if block_given?
111
+ if block_given? or debug
67
112
  mail = fetch_mail message_id
68
- result = yield(mail)
69
- verbose.print "(given filter result: #{result.inspect}) "
113
+
114
+ debug.call(mail) if debug
115
+
116
+ if block_given?
117
+ result = yield(mail)
118
+ verbose.print "(given filter result: #{result.inspect}) "
119
+ end
70
120
  end
71
121
 
72
122
  if result
73
123
  puts operation.call(message_id)
74
124
  else
75
- puts "ignored".yellow
125
+ puts "ignored".green
76
126
  end
77
127
  end
128
+
129
+ ensure
130
+ expunge
78
131
  end
79
132
 
133
+ # @note We use "BODY.PEEK[]" to avoid setting the \Seen flag.
80
134
  def fetch_mail message_id
81
- msg = @imap.fetch(message_id, 'RFC822')[0].attr['RFC822']
135
+ msg = @imap.fetch(message_id, 'BODY.PEEK[]').first.attr['BODY[]']
82
136
  Mail.read_from_string msg
83
137
  end
84
138
 
85
139
  def search query
86
140
  unless [Array, String].any? { |type| query.is_a? type }
87
- raise ArgumentError, "query must be either a string holding the entire search string, or a single-dimension array of search keywords and arguments"
141
+ raise TypeError, "query must be either a string holding the entire search string, or a single-dimension array of search keywords and arguments"
88
142
  end
89
143
 
90
144
  messages = @imap.search query
91
- puts "Query: #{query.inspect}: #{messages.count} results".cyan
145
+ puts "Query on #{mailbox}: #{query.inspect}: #{messages.count} results".cyan
92
146
 
93
147
  messages
94
148
  end
@@ -99,7 +153,7 @@ module IMAPGuard
99
153
  else
100
154
  # anonymous null object
101
155
  Class.new do
102
- def method_missing(*args, &block)
156
+ def method_missing(*)
103
157
  nil
104
158
  end
105
159
  end.new
@@ -1,25 +1,60 @@
1
1
  module IMAPGuard
2
+ # Query is a neat DSL to help you generate IMAP search queries.
2
3
  class Query < Array
4
+ # Messages that have the \Seen flag set.
5
+ # @return [Query] self
3
6
  def seen
4
7
  self << 'SEEN'
5
8
  end
6
9
 
10
+ # Messages that do not have the \Answered flag set.
11
+ # @return [Query] self
7
12
  def unanswered
8
13
  self << 'UNANSWERED'
9
14
  end
10
15
 
16
+ # Messages that do not have the \Flagged flag set.
17
+ # @return [Query] self
11
18
  def unflagged
12
19
  self << 'UNFLAGGED'
13
20
  end
14
21
 
22
+ # Messages that match either search key.
23
+ # @note Reverse polish notation is expected,
24
+ # i.e. OR <search-key1> <search-key2>
25
+ # @return [Query] self
26
+ def or
27
+ self << 'OR'
28
+ end
29
+
30
+ # Messages that contain the specified string in the envelope
31
+ # structure's SUBJECT field.
32
+ # @return [Query] self
15
33
  def subject string
16
34
  self << 'SUBJECT' << string
17
35
  end
18
36
 
37
+ # Messages that contain the specified string in the envelope
38
+ # structure's FROM field.
39
+ # @return [Query] self
19
40
  def from string
20
41
  self << 'FROM' << string
21
42
  end
22
43
 
44
+ # Messages that contain the specified string in the envelope
45
+ # structure's TO field.
46
+ # @return [Query] self
47
+ def to string
48
+ self << 'TO' << string
49
+ end
50
+
51
+ # Messages whose internal date (disregarding time and timezone)
52
+ # is earlier than the specified date.
53
+ # @param date Depending of its type:
54
+ # - String: uses it as is
55
+ # - Fixnum: _n_ days before today
56
+ # - Date: uses this date
57
+ # @return [Query] self
23
58
  def before date
24
59
  case date
25
60
  when String
@@ -1,3 +1,4 @@
1
1
  module ImapGuard
2
- VERSION = "0.0.2"
2
+ # [String] ImapGuard version
3
+ VERSION = "0.0.3"
3
4
  end
data/lib/imap_guard.rb CHANGED
@@ -2,5 +2,6 @@ require "imap_guard/version"
2
2
  require "imap_guard/guard"
3
3
  require "imap_guard/query"
4
4
 
5
+ # ImapGuard default module
5
6
  module ImapGuard
6
7
  end
@@ -6,6 +6,10 @@ module IMAPGuard
6
6
  $stdout = StringIO.new # mute stdout - comment to debug
7
7
  end
8
8
 
9
+ after do
10
+ $stdout = STDOUT
11
+ end
12
+
9
13
  let(:settings) do
10
14
  {
11
15
  host: 'localhost',
@@ -16,7 +20,7 @@ module IMAPGuard
16
20
  end
17
21
 
18
22
  let(:imap) {
19
- double('Net::IMAP', search: [7, 28])
23
+ double('Net::IMAP', search: [7, 28], expunge: nil, select: nil, list: [])
20
24
  }
21
25
 
22
26
  def guard_instance custom_settings = {}
@@ -46,6 +50,26 @@ module IMAPGuard
46
50
  end
47
51
  end
48
52
 
53
+ describe "#mailbox" do
54
+ it "returns nil when no mailbox has been selected" do
55
+ guard_instance.mailbox.should be_nil
56
+ end
57
+
58
+ it "returns the currently selected mailbox" do
59
+ guard = guard_instance
60
+
61
+ guard.select 'Sent'
62
+ guard.mailbox.should eq 'Sent'
63
+ end
64
+ end
65
+
66
+ describe "#list" do
67
+ it "returns the list of mailboxes" do
68
+ imap.should_receive(:list)
69
+ guard_instance.list.should eq []
70
+ end
71
+ end
72
+
49
73
  describe "#search" do
50
74
  before do
51
75
  imap.should_receive(:search) do |arg|
@@ -88,6 +112,16 @@ module IMAPGuard
88
112
 
89
113
  guard.send(:process, 'ALL', opeartion)
90
114
  end
115
+
116
+ context "with a debug proc" do
117
+ it "calls the proc" do
118
+ block = ->(mail) {}
119
+ guard.debug = block
120
+ block.should_receive(:call).twice
121
+
122
+ guard.send(:process, 'ALL', opeartion)
123
+ end
124
+ end
91
125
  end
92
126
 
93
127
  context "with a filter block" do
@@ -151,6 +185,13 @@ module IMAPGuard
151
185
  end
152
186
  end
153
187
 
188
+ describe "#disconnect" do
189
+ it "disconnects from the server" do
190
+ imap.should_receive(:disconnect)
191
+ guard_instance.disconnect
192
+ end
193
+ end
194
+
154
195
  describe "#verbose" do
155
196
  context "with settings.verbose = true" do
156
197
  let(:guard) { guard_instance(verbose: true) }
@@ -27,6 +27,13 @@ module IMAPGuard
27
27
  end
28
28
  end
29
29
 
30
+ describe "#or" do
31
+ it "adds 'OR'" do
32
+ subject.or
33
+ subject.last.should eq 'OR'
34
+ end
35
+ end
36
+
30
37
  describe "#subject" do
31
38
  it "adds the search value" do
32
39
  subject.subject("Hey you")
@@ -41,6 +48,13 @@ module IMAPGuard
41
48
  end
42
49
  end
43
50
 
51
+ describe "#to" do
52
+ it "adds the search value" do
53
+ subject.to("root@example.net")
54
+ subject.last.should eq "root@example.net"
55
+ end
56
+ end
57
+
44
58
  describe "#before" do
45
59
  context "when I pass 'nil' as an argument" do
46
60
  it "raises" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-24 00:00:00.000000000 Z
12
+ date: 2013-04-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mail
@@ -210,7 +210,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
210
210
  version: '0'
211
211
  segments:
212
212
  - 0
213
- hash: -500228931
213
+ hash: 903698273
214
214
  required_rubygems_version: !ruby/object:Gem::Requirement
215
215
  none: false
216
216
  requirements:
@@ -219,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
219
219
  version: '0'
220
220
  segments:
221
221
  - 0
222
- hash: -500228931
222
+ hash: 903698273
223
223
  requirements: []
224
224
  rubyforge_project:
225
225
  rubygems_version: 1.8.25