imap_guard 0.0.2 → 0.0.3
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/README.md +34 -1
- data/lib/imap_guard/guard.rb +70 -16
- data/lib/imap_guard/query.rb +35 -0
- data/lib/imap_guard/version.rb +2 -1
- data/lib/imap_guard.rb +1 -0
- data/spec/imap_guard/guard_spec.rb +42 -1
- data/spec/imap_guard/query_spec.rb +14 -0
- metadata +4 -4
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]
|
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
|
+
|
data/lib/imap_guard/guard.rb
CHANGED
@@ -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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
@
|
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
|
-
|
53
|
-
|
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
|
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
|
-
|
69
|
-
|
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".
|
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, '
|
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
|
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(*
|
156
|
+
def method_missing(*)
|
103
157
|
nil
|
104
158
|
end
|
105
159
|
end.new
|
data/lib/imap_guard/query.rb
CHANGED
@@ -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
|
data/lib/imap_guard/version.rb
CHANGED
data/lib/imap_guard.rb
CHANGED
@@ -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.
|
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-
|
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:
|
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:
|
222
|
+
hash: 903698273
|
223
223
|
requirements: []
|
224
224
|
rubyforge_project:
|
225
225
|
rubygems_version: 1.8.25
|