imap_processor 1.1.1 → 1.2
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.tar.gz.sig +0 -0
- data/.autotest +3 -18
- data/History.txt +15 -0
- data/Manifest.txt +7 -0
- data/README.txt +4 -2
- data/Rakefile +4 -4
- data/bin/imap_archive +5 -0
- data/bin/imap_idle +6 -0
- data/lib/imap_processor.rb +104 -20
- data/lib/imap_processor/archive.rb +81 -0
- data/lib/imap_processor/idle.rb +76 -0
- data/lib/imap_processor/keywords.rb +9 -14
- data/lib/imap_sasl_plain.rb +1 -26
- data/lib/net/imap/date.rb +24 -0
- data/lib/net/imap/idle.rb +39 -0
- data/test/test_imap_processor.rb +148 -0
- metadata +19 -8
- metadata.gz.sig +0 -0
data.tar.gz.sig
CHANGED
Binary file
|
data/.autotest
CHANGED
@@ -2,22 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'autotest/restart'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# at.libs << ":../some/external"
|
9
|
-
#
|
10
|
-
# at.add_exception 'vendor'
|
11
|
-
#
|
12
|
-
# at.add_mapping(/dependency.rb/) do |f, _|
|
13
|
-
# at.files_matching(/test_.*rb$/)
|
14
|
-
# end
|
15
|
-
#
|
16
|
-
# %w(TestA TestB).each do |klass|
|
17
|
-
# at.extra_class_map[klass] = "test/test_misc.rb"
|
18
|
-
# end
|
19
|
-
# end
|
5
|
+
Autotest.add_hook :initialize do |at|
|
6
|
+
at.testlib = 'minitest/autorun'
|
7
|
+
end
|
20
8
|
|
21
|
-
# Autotest.add_hook :run_command do |at|
|
22
|
-
# system "rake build"
|
23
|
-
# end
|
data/History.txt
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
=== 1.2 / 2009-06-02
|
2
|
+
|
3
|
+
* 2 major enhancements
|
4
|
+
* imap_archive which archives old mail to dated mailboxes
|
5
|
+
* imap_idle which lists messages that were added or expunged from a mailbox
|
6
|
+
|
7
|
+
* 4 minor enhancements
|
8
|
+
* Added IMAPProcessor#create_mailbox
|
9
|
+
* Added IMAPProcessor#delete_messages
|
10
|
+
* Added IMAPProcessor#move_messages
|
11
|
+
* Disabled verification of SSL certs for 1.9
|
12
|
+
|
13
|
+
* 1 bug fix
|
14
|
+
* Fixed options file names, they should be Symbol keys
|
15
|
+
|
1
16
|
=== 1.1.1 / 2009-05-19
|
2
17
|
|
3
18
|
* 1 bug fix
|
data/Manifest.txt
CHANGED
@@ -3,7 +3,14 @@ History.txt
|
|
3
3
|
Manifest.txt
|
4
4
|
README.txt
|
5
5
|
Rakefile
|
6
|
+
bin/imap_archive
|
7
|
+
bin/imap_idle
|
6
8
|
bin/imap_keywords
|
7
9
|
lib/imap_processor.rb
|
10
|
+
lib/imap_processor/archive.rb
|
11
|
+
lib/imap_processor/idle.rb
|
8
12
|
lib/imap_processor/keywords.rb
|
9
13
|
lib/imap_sasl_plain.rb
|
14
|
+
lib/net/imap/date.rb
|
15
|
+
lib/net/imap/idle.rb
|
16
|
+
test/test_imap_processor.rb
|
data/README.txt
CHANGED
@@ -8,8 +8,10 @@ IMAPProcessor is a client for processing messages on an IMAP server. It
|
|
8
8
|
provides some basic mechanisms for connecting to an IMAP server, determining
|
9
9
|
capabilities and handling messages.
|
10
10
|
|
11
|
-
IMAPProcessor ships with the imap_keywords
|
12
|
-
server for keywords set on messages in mailboxes
|
11
|
+
IMAPProcessor ships with the executables imap_keywords which can query an IMAP
|
12
|
+
server for keywords set on messages in mailboxes, imap_idle which can show new
|
13
|
+
messages in a mailbox and imap_archive which will archive old messages to a
|
14
|
+
new mailbox.
|
13
15
|
|
14
16
|
== FEATURES/PROBLEMS:
|
15
17
|
|
data/Rakefile
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'hoe'
|
5
|
-
$:.unshift 'lib'
|
6
|
-
require 'imap_processor'
|
7
5
|
|
8
|
-
Hoe.
|
6
|
+
Hoe.plugin :seattlerb
|
7
|
+
|
8
|
+
Hoe.spec 'imap_processor' do |ip|
|
9
9
|
ip.rubyforge_name = 'seattlerb'
|
10
|
-
ip.developer
|
10
|
+
ip.developer 'Eric Hodel', 'drbrain@segment7.net'
|
11
11
|
end
|
12
12
|
|
13
13
|
# vim: syntax=Ruby
|
data/bin/imap_archive
ADDED
data/bin/imap_idle
ADDED
data/lib/imap_processor.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'optparse'
|
3
3
|
require 'net/imap'
|
4
|
+
require 'net/imap/date'
|
4
5
|
require 'imap_sasl_plain'
|
5
6
|
|
6
7
|
##
|
@@ -19,7 +20,13 @@ class IMAPProcessor
|
|
19
20
|
##
|
20
21
|
# The version of IMAPProcessor you are using
|
21
22
|
|
22
|
-
VERSION = '1.
|
23
|
+
VERSION = '1.2'
|
24
|
+
|
25
|
+
##
|
26
|
+
# Base IMAPProcessor error class
|
27
|
+
|
28
|
+
class Error < RuntimeError
|
29
|
+
end
|
23
30
|
|
24
31
|
##
|
25
32
|
# A Connection Struct that has +imap+ and +capability+ accessors
|
@@ -67,7 +74,7 @@ class IMAPProcessor
|
|
67
74
|
opts.on( "--move=MAILBOX",
|
68
75
|
"Mailbox to move message to",
|
69
76
|
"Default: #{options[:MoveTo].inspect}",
|
70
|
-
"Options file name: MoveTo") do |mailbox|
|
77
|
+
"Options file name: :MoveTo") do |mailbox|
|
71
78
|
options[:MoveTo] = mailbox
|
72
79
|
end
|
73
80
|
end
|
@@ -91,15 +98,18 @@ class IMAPProcessor
|
|
91
98
|
# opts.on( "--move=MAILBOX",
|
92
99
|
# "Mailbox to move message to",
|
93
100
|
# "Default: #{options[:MoveTo].inspect}",
|
94
|
-
# "Options file name: MoveTo") do |mailbox|
|
101
|
+
# "Options file name: :MoveTo") do |mailbox|
|
95
102
|
# options[:MoveTo] = mailbox
|
96
103
|
# end
|
97
104
|
# end
|
98
105
|
# end
|
106
|
+
#
|
107
|
+
# NOTE: You can add a --move option using ::add_move
|
99
108
|
|
100
109
|
def self.process_args(processor_file, args,
|
101
110
|
required_options = {}) # :yield: OptionParser
|
102
111
|
opts_file_name = File.basename processor_file, '.rb'
|
112
|
+
opts_file_name = "imap_#{opts_file_name}" unless opts_file_name =~ /^imap_/
|
103
113
|
opts_file = File.expand_path "~/.#{opts_file_name}"
|
104
114
|
options = @@options.dup
|
105
115
|
|
@@ -131,7 +141,7 @@ class IMAPProcessor
|
|
131
141
|
options[k] ||= v
|
132
142
|
end
|
133
143
|
|
134
|
-
|
144
|
+
op = OptionParser.new do |opts|
|
135
145
|
opts.program_name = File.basename $0
|
136
146
|
opts.banner = "Usage: #{opts.program_name} [options]\n\n"
|
137
147
|
|
@@ -141,28 +151,28 @@ class IMAPProcessor
|
|
141
151
|
opts.on("-H", "--host HOST",
|
142
152
|
"IMAP server host",
|
143
153
|
"Default: #{options[:Host].inspect}",
|
144
|
-
"Options file name: Host") do |host|
|
154
|
+
"Options file name: :Host") do |host|
|
145
155
|
options[:Host] = host
|
146
156
|
end
|
147
157
|
|
148
158
|
opts.on("-P", "--port PORT",
|
149
159
|
"IMAP server port",
|
150
160
|
"Default: The correct port SSL/non-SSL mode",
|
151
|
-
"Options file name: Port") do |port|
|
161
|
+
"Options file name: :Port") do |port|
|
152
162
|
options[:Port] = port
|
153
163
|
end
|
154
164
|
|
155
165
|
opts.on("-s", "--[no-]ssl",
|
156
166
|
"Use SSL for IMAP connection",
|
157
167
|
"Default: #{options[:SSL].inspect}",
|
158
|
-
"Options file name: SSL") do |ssl|
|
168
|
+
"Options file name: :SSL") do |ssl|
|
159
169
|
options[:SSL] = ssl
|
160
170
|
end
|
161
171
|
|
162
172
|
opts.on( "--[no-]debug",
|
163
173
|
"Display Net::IMAP debugging info",
|
164
174
|
"Default: #{options[:Debug].inspect}",
|
165
|
-
"Options file name: Debug") do |debug|
|
175
|
+
"Options file name: :Debug") do |debug|
|
166
176
|
options[:Debug] = debug
|
167
177
|
end
|
168
178
|
|
@@ -172,14 +182,14 @@ class IMAPProcessor
|
|
172
182
|
opts.on("-u", "--username USERNAME",
|
173
183
|
"IMAP username",
|
174
184
|
"Default: #{options[:Username].inspect}",
|
175
|
-
"Options file name: Username") do |username|
|
185
|
+
"Options file name: :Username") do |username|
|
176
186
|
options[:Username] = username
|
177
187
|
end
|
178
188
|
|
179
189
|
opts.on("-p", "--password PASSWORD",
|
180
190
|
"IMAP password",
|
181
191
|
"Default: Read from ~/.#{opts_file_name}",
|
182
|
-
"Options file name: Password") do |password|
|
192
|
+
"Options file name: :Password") do |password|
|
183
193
|
options[:Password] = password
|
184
194
|
end
|
185
195
|
|
@@ -190,7 +200,7 @@ class IMAPProcessor
|
|
190
200
|
"Authentication type will be auto-",
|
191
201
|
"discovered",
|
192
202
|
"Default: #{options[:Auth].inspect}",
|
193
|
-
"Options file name: Auth") do |auth|
|
203
|
+
"Options file name: :Auth") do |auth|
|
194
204
|
options[:Auth] = auth
|
195
205
|
end
|
196
206
|
|
@@ -200,7 +210,7 @@ class IMAPProcessor
|
|
200
210
|
opts.on("-r", "--root ROOT",
|
201
211
|
"Root of mailbox hierarchy",
|
202
212
|
"Default: #{options[:Root].inspect}",
|
203
|
-
"Options file name: Root") do |root|
|
213
|
+
"Options file name: :Root") do |root|
|
204
214
|
options[:Root] = root
|
205
215
|
end
|
206
216
|
|
@@ -208,14 +218,14 @@ class IMAPProcessor
|
|
208
218
|
"Comma-separated list of mailbox names",
|
209
219
|
"to search",
|
210
220
|
"Default: #{options[:Boxes].inspect}",
|
211
|
-
"Options file name: Boxes") do |boxes|
|
221
|
+
"Options file name: :Boxes") do |boxes|
|
212
222
|
options[:Boxes] = boxes
|
213
223
|
end
|
214
224
|
|
215
225
|
opts.on("-v", "--[no-]verbose",
|
216
226
|
"Be verbose",
|
217
227
|
"Default: #{options[:Verbose].inspect}",
|
218
|
-
"Options file name: Verbose") do |verbose|
|
228
|
+
"Options file name: :Verbose") do |verbose|
|
219
229
|
options[:Verbose] = verbose
|
220
230
|
end
|
221
231
|
|
@@ -248,7 +258,7 @@ Example ~/.#{opts_file_name}:
|
|
248
258
|
EOF
|
249
259
|
end
|
250
260
|
|
251
|
-
|
261
|
+
op.parse! args
|
252
262
|
|
253
263
|
options[:Port] ||= options[:SSL] ? 993 : 143
|
254
264
|
|
@@ -306,8 +316,13 @@ Example ~/.#{opts_file_name}:
|
|
306
316
|
#
|
307
317
|
# Returns a Connection object.
|
308
318
|
|
309
|
-
def connect(host
|
310
|
-
|
319
|
+
def connect(host = @options[:Host],
|
320
|
+
port = @options[:Port],
|
321
|
+
ssl = @options[:SSL],
|
322
|
+
username = @options[:Username],
|
323
|
+
password = @options[:Password],
|
324
|
+
auth = @options[:Auth]) # :yields: Connection
|
325
|
+
imap = Net::IMAP.new host, port, ssl, nil, false
|
311
326
|
log "Connected to imap://#{host}:#{port}/"
|
312
327
|
|
313
328
|
capability = imap.capability
|
@@ -326,7 +341,41 @@ Example ~/.#{opts_file_name}:
|
|
326
341
|
imap.authenticate auth, username, password
|
327
342
|
log "Logged in as #{username}"
|
328
343
|
|
329
|
-
Connection.new imap, capability
|
344
|
+
connection = Connection.new imap, capability
|
345
|
+
|
346
|
+
if block_given? then
|
347
|
+
begin
|
348
|
+
yield connection
|
349
|
+
ensure
|
350
|
+
connection.imap.logout
|
351
|
+
end
|
352
|
+
else
|
353
|
+
return connection
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
##
|
358
|
+
# Create the mailbox +name+ if it doesn't exist. Note that this will SELECT
|
359
|
+
# the mailbox if it exists.
|
360
|
+
|
361
|
+
def create_mailbox name
|
362
|
+
log "LIST #{name}"
|
363
|
+
list = imap.list '', name
|
364
|
+
return if list
|
365
|
+
log "CREATE #{name}"
|
366
|
+
imap.create name
|
367
|
+
end
|
368
|
+
|
369
|
+
##
|
370
|
+
# Delete and +expunge+ the specified +uids+.
|
371
|
+
|
372
|
+
def delete_messages uids, expunge = true
|
373
|
+
log "DELETING [...#{uids.size} uids]"
|
374
|
+
imap.store uids, '+FLAGS.SILENT', [:Deleted]
|
375
|
+
if expunge then
|
376
|
+
log "EXPUNGE"
|
377
|
+
imap.expunge
|
378
|
+
end
|
330
379
|
end
|
331
380
|
|
332
381
|
##
|
@@ -379,7 +428,7 @@ Example ~/.#{opts_file_name}:
|
|
379
428
|
sequence.unshift "BODY[#{section}.MIME]" unless section == 'TEXT'
|
380
429
|
sequence.unshift 'BODY[HEADER]' if header
|
381
430
|
|
382
|
-
body =
|
431
|
+
body = imap.fetch(uid, sequence).first
|
383
432
|
|
384
433
|
sequence = sequence.map { |item| body.attr[item] }
|
385
434
|
|
@@ -411,7 +460,7 @@ Example ~/.#{opts_file_name}:
|
|
411
460
|
def mime_parts(uids, mime_type)
|
412
461
|
media_type, subtype = mime_type.upcase.split('/', 2)
|
413
462
|
|
414
|
-
structures =
|
463
|
+
structures = imap.fetch uids, 'BODYSTRUCTURE'
|
415
464
|
|
416
465
|
structures.zip(uids).map do |body, uid|
|
417
466
|
section = nil
|
@@ -436,6 +485,41 @@ Example ~/.#{opts_file_name}:
|
|
436
485
|
end.compact
|
437
486
|
end
|
438
487
|
|
488
|
+
##
|
489
|
+
# Move the specified +uids+ to a new +destination+ then delete and +expunge+
|
490
|
+
# them. Creates the destination mailbox if it doesn't exist.
|
491
|
+
|
492
|
+
def move_messages uids, destination, expunge = true
|
493
|
+
return if uids.empty?
|
494
|
+
log "COPY [...#{uids.size} uids]"
|
495
|
+
|
496
|
+
begin
|
497
|
+
imap.copy uids, destination
|
498
|
+
rescue Net::IMAP::NoResponseError => e
|
499
|
+
# ruby-lang bug #1713
|
500
|
+
#raise unless e.response.data.code.name == 'TRYCREATE'
|
501
|
+
create_mailbox destination
|
502
|
+
imap.copy uids, destination
|
503
|
+
end
|
504
|
+
|
505
|
+
delete_messages uids, expunge
|
506
|
+
end
|
507
|
+
|
508
|
+
##
|
509
|
+
# Displays Date, Subject and Message-Id from messages in +uids+
|
510
|
+
|
511
|
+
def show_messages(uids)
|
512
|
+
return if uids.nil? or (Array === uids and uids.empty?)
|
513
|
+
|
514
|
+
fetch_data = 'BODY.PEEK[HEADER.FIELDS (DATE SUBJECT MESSAGE-ID)]'
|
515
|
+
messages = imap.fetch uids, fetch_data
|
516
|
+
fetch_data.sub! '.PEEK', '' # stripped by server
|
517
|
+
|
518
|
+
messages.each do |res|
|
519
|
+
puts res.attr[fetch_data].delete("\r")
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
439
523
|
##
|
440
524
|
# Did the user set --verbose?
|
441
525
|
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'imap_processor'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Archives old mail on IMAP server by moving it to dated mailboxen.
|
5
|
+
|
6
|
+
class IMAPProcessor::Archive < IMAPProcessor
|
7
|
+
attr_reader :list, :move
|
8
|
+
|
9
|
+
def self.process_args(args)
|
10
|
+
required_options = {
|
11
|
+
:List => true,
|
12
|
+
:Move => false,
|
13
|
+
}
|
14
|
+
|
15
|
+
super __FILE__, args, required_options do |opts, options|
|
16
|
+
opts.banner << <<-EOF
|
17
|
+
imap_archive archives old mail on IMAP server by moving it to dated mailboxen.
|
18
|
+
EOF
|
19
|
+
|
20
|
+
opts.on("--[no-]list", "Display messages (on by default)") do |list|
|
21
|
+
options[:List] = list
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on("--[no-]move", "Move the messages (off by default)") do |move|
|
25
|
+
options[:Move] = move
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(options)
|
31
|
+
super
|
32
|
+
|
33
|
+
@list = options[:List]
|
34
|
+
@move = options[:Move]
|
35
|
+
|
36
|
+
connection = connect
|
37
|
+
|
38
|
+
@imap = connection.imap
|
39
|
+
end
|
40
|
+
|
41
|
+
def the_first
|
42
|
+
t = Time.now
|
43
|
+
the_first = Time.local(t.year, t.month, 1)
|
44
|
+
end
|
45
|
+
|
46
|
+
def last_month
|
47
|
+
t = the_first - 1
|
48
|
+
Time.local(t.year, t.month, 1).strftime("%Y-%m")
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Makes a SEARCH argument set from +keywords+
|
53
|
+
|
54
|
+
def make_search
|
55
|
+
%W[SENTBEFORE #{the_first.imapdate}]
|
56
|
+
end
|
57
|
+
|
58
|
+
def run
|
59
|
+
@boxes.each do |mailbox|
|
60
|
+
destination = "#{mailbox}.#{last_month}"
|
61
|
+
|
62
|
+
log "SELECT #{mailbox}"
|
63
|
+
response = imap.select mailbox
|
64
|
+
|
65
|
+
search = make_search
|
66
|
+
|
67
|
+
log "SEARCH #{search.join ' '}"
|
68
|
+
uids = imap.search search
|
69
|
+
|
70
|
+
next if uids.empty?
|
71
|
+
|
72
|
+
puts "#{uids.length} messages in #{mailbox}#{list ? ':' : ''}"
|
73
|
+
|
74
|
+
show_messages uids
|
75
|
+
|
76
|
+
move_messages uids, destination if move
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'imap_processor'
|
2
|
+
require 'net/imap/idle'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Example class that supports IDLE on a mailbox and lists messages added or
|
6
|
+
# expunged.
|
7
|
+
|
8
|
+
class IMAPProcessor::IDLE < IMAPProcessor
|
9
|
+
|
10
|
+
def self.process_args(args)
|
11
|
+
super __FILE__, args do |opts, options|
|
12
|
+
opts.banner << <<-EOF
|
13
|
+
imap_idle lists messages added or expunged from a mailbox
|
14
|
+
EOF
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(options)
|
19
|
+
super
|
20
|
+
|
21
|
+
raise IMAPProcessor::Error, 'only one mailbox is supported' if
|
22
|
+
@boxes.length > 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
mailbox = @boxes.first
|
27
|
+
|
28
|
+
connect do |connection|
|
29
|
+
raise IMAPProcessor::Error, 'IDLE not supported on this server' unless
|
30
|
+
connection.idle?
|
31
|
+
|
32
|
+
imap = connection.imap
|
33
|
+
|
34
|
+
imap.select mailbox
|
35
|
+
exists = imap.responses['EXISTS'].first
|
36
|
+
|
37
|
+
log "Starting IDLE"
|
38
|
+
|
39
|
+
imap.idle do |response|
|
40
|
+
next unless Net::IMAP::UntaggedResponse === response
|
41
|
+
|
42
|
+
case response.name
|
43
|
+
when 'EXPUNGE' then
|
44
|
+
puts "Expunged message #{response.data}"
|
45
|
+
when 'EXISTS' then
|
46
|
+
latest_uid = response.data
|
47
|
+
new = latest_uid - exists
|
48
|
+
puts "#{new} messages added"
|
49
|
+
|
50
|
+
show_messages_in mailbox, ((exists + 1)..latest_uid)
|
51
|
+
|
52
|
+
exists = response.data
|
53
|
+
when 'RECENT' then
|
54
|
+
puts "#{response.data} recent messages"
|
55
|
+
when 'OK' then # ending IDLE
|
56
|
+
else
|
57
|
+
log "Unhandled untagged response: #{response.name}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def show_messages_in(mailbox, uids)
|
64
|
+
connect do |connection|
|
65
|
+
@imap = connection.imap
|
66
|
+
|
67
|
+
@imap.select mailbox
|
68
|
+
|
69
|
+
show_messages uids
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
@imap = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
@@ -33,7 +33,7 @@ previously set keywords.
|
|
33
33
|
end
|
34
34
|
|
35
35
|
opts.on( "--[no-]list",
|
36
|
-
"
|
36
|
+
"Display messages") do |list|
|
37
37
|
options[:List] = list
|
38
38
|
end
|
39
39
|
|
@@ -47,11 +47,11 @@ previously set keywords.
|
|
47
47
|
def initialize(options)
|
48
48
|
super
|
49
49
|
|
50
|
-
@add
|
51
|
-
@delete
|
50
|
+
@add = options[:Add]
|
51
|
+
@delete = options[:Delete]
|
52
52
|
@keywords = options[:Keywords]
|
53
|
-
@not
|
54
|
-
@list
|
53
|
+
@not = options[:Not] ? 'NOT' : nil
|
54
|
+
@list = options[:List]
|
55
55
|
|
56
56
|
if @add and @delete then
|
57
57
|
raise OptionParser::InvalidOption, "--add and --delete are exclusive"
|
@@ -60,8 +60,7 @@ previously set keywords.
|
|
60
60
|
"--add and --delete require --keywords"
|
61
61
|
end
|
62
62
|
|
63
|
-
connection = connect
|
64
|
-
options[:Username], options[:Password], options[:Auth]
|
63
|
+
connection = connect
|
65
64
|
|
66
65
|
@imap = connection.imap
|
67
66
|
end
|
@@ -134,18 +133,14 @@ previously set keywords.
|
|
134
133
|
return unless @list
|
135
134
|
|
136
135
|
responses = @imap.fetch uids, [
|
137
|
-
'BODY.PEEK[HEADER]',
|
136
|
+
Net::IMAP::RawData.new('BODY.PEEK[HEADER.FIELDS (SUBJECT MESSAGE-ID)]'),
|
138
137
|
'FLAGS'
|
139
138
|
]
|
140
139
|
|
141
140
|
responses.each do |res|
|
142
|
-
header = res.attr['BODY[HEADER]']
|
141
|
+
header = res.attr['BODY[HEADER.FIELDS (SUBJECT MESSAGE-ID)]']
|
143
142
|
|
144
|
-
header
|
145
|
-
puts "Subject: #{$1}"
|
146
|
-
|
147
|
-
header =~ /^Message-Id: (.*)/i
|
148
|
-
puts "Message-Id: #{$1}"
|
143
|
+
puts header.chomp
|
149
144
|
|
150
145
|
flags = res.attr['FLAGS'].map { |flag| flag.inspect }.join ', '
|
151
146
|
|
data/lib/imap_sasl_plain.rb
CHANGED
@@ -1,29 +1,5 @@
|
|
1
|
-
require 'time'
|
2
1
|
require 'net/imap'
|
3
2
|
|
4
|
-
class Time
|
5
|
-
|
6
|
-
##
|
7
|
-
# Formats this Time as an IMAP-style date.
|
8
|
-
|
9
|
-
def imapdate
|
10
|
-
strftime '%d-%b-%Y'
|
11
|
-
end
|
12
|
-
|
13
|
-
##
|
14
|
-
# Formats this Time as an IMAP-style datetime.
|
15
|
-
#
|
16
|
-
# RFC 2060 doesn't specify the format of its times. Unfortunately it is
|
17
|
-
# almost but not quite RFC 822 compliant.
|
18
|
-
#--
|
19
|
-
# Go Mr. Leatherpants!
|
20
|
-
|
21
|
-
def imapdatetime
|
22
|
-
strftime '%d-%b-%Y %H:%M %Z'
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
3
|
##
|
28
4
|
# RFC 2595 PLAIN Authenticator for Net::IMAP. Only for use with SSL (but not
|
29
5
|
# enforced).
|
@@ -56,10 +32,9 @@ class Net::IMAP::PlainAuthenticator
|
|
56
32
|
@password = password
|
57
33
|
end
|
58
34
|
|
59
|
-
end
|
35
|
+
end unless defined? Net::IMAP::PlainAuthenticator
|
60
36
|
|
61
37
|
if defined? OpenSSL then
|
62
38
|
Net::IMAP.add_authenticator 'PLAIN', Net::IMAP::PlainAuthenticator
|
63
39
|
end
|
64
40
|
|
65
|
-
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
class Time
|
3
|
+
|
4
|
+
##
|
5
|
+
# Formats this Time as an IMAP-style date.
|
6
|
+
|
7
|
+
def imapdate
|
8
|
+
strftime '%d-%b-%Y'
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Formats this Time as an IMAP-style datetime.
|
13
|
+
#
|
14
|
+
# RFC 2060 doesn't specify the format of its times. Unfortunately it is
|
15
|
+
# almost but not quite RFC 822 compliant.
|
16
|
+
#--
|
17
|
+
# Go Mr. Leatherpants!
|
18
|
+
|
19
|
+
def imapdatetime
|
20
|
+
strftime '%d-%b-%Y %H:%M %Z'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
|
3
|
+
class Net::IMAP
|
4
|
+
|
5
|
+
##
|
6
|
+
# Sends an IDLE command that waits for notifications of new or expunged
|
7
|
+
# messages. Yields responses from the server during the IDLE.
|
8
|
+
#
|
9
|
+
# Use +break+ in the response handler to leave IDLE.
|
10
|
+
|
11
|
+
def idle(&response_handler)
|
12
|
+
raise LocalJumpError, "no block given" unless response_handler
|
13
|
+
|
14
|
+
response = nil
|
15
|
+
|
16
|
+
synchronize do
|
17
|
+
tag = Thread.current[:net_imap_tag] = generate_tag
|
18
|
+
put_string "#{tag} IDLE#{CRLF}"
|
19
|
+
|
20
|
+
add_response_handler response_handler
|
21
|
+
|
22
|
+
begin
|
23
|
+
response = get_tagged_response tag
|
24
|
+
rescue LocalJumpError # can't break cross-threads or something
|
25
|
+
ensure
|
26
|
+
unless response then
|
27
|
+
put_string "DONE#{CRLF}"
|
28
|
+
response = get_tagged_response tag
|
29
|
+
end
|
30
|
+
|
31
|
+
remove_response_handler response_handler
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'imap_processor'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
##
|
6
|
+
# These tests expect a local IMAP server with a 'test' login with a 'test'
|
7
|
+
# password. It's fairly easy to set up dovecot to do this:
|
8
|
+
#
|
9
|
+
# In dovecot.conf:
|
10
|
+
#
|
11
|
+
# mail_location = mbox:~/mail:INBOX=~/Mailbox
|
12
|
+
#
|
13
|
+
# auth default {
|
14
|
+
# mechanisms = plain
|
15
|
+
#
|
16
|
+
# passdb passwd-file {
|
17
|
+
# args = scheme=plain username_format=%n /path/to/etc/dovecot/passwd
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
# userdb passwd-file {
|
21
|
+
# args = username_format=%n /path/to/etc/dovecot/passwd
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
# And in /path/to/etc/dovecot/passwd:
|
26
|
+
#
|
27
|
+
# test:test:<your uid>:<your gid>::/path/to/your/home/dovecot
|
28
|
+
|
29
|
+
class TestIMAPProcessor < MiniTest::Unit::TestCase
|
30
|
+
|
31
|
+
def setup
|
32
|
+
host, port, username, password = 'localhost', 143, 'test', 'test'
|
33
|
+
|
34
|
+
@ip = IMAPProcessor.new :Host => host, :Port => port,
|
35
|
+
:Username => username, :Password => password,
|
36
|
+
:Verbose => false
|
37
|
+
|
38
|
+
@connection = @ip.connect
|
39
|
+
|
40
|
+
@imap = @connection.imap
|
41
|
+
@ip.instance_variable_set :@imap, @imap
|
42
|
+
|
43
|
+
@delim = @imap.list('', 'INBOX').first.delim
|
44
|
+
end
|
45
|
+
|
46
|
+
def teardown
|
47
|
+
@imap.select 'INBOX'
|
48
|
+
uids = @imap.search 'ALL'
|
49
|
+
@imap.store uids, '+FLAGS.SILENT', [:Deleted] unless uids.empty?
|
50
|
+
@imap.expunge
|
51
|
+
@imap.list('', '*').each do |mailbox|
|
52
|
+
next if mailbox.name == 'INBOX'
|
53
|
+
@imap.delete mailbox.name
|
54
|
+
end
|
55
|
+
@imap.disconnect
|
56
|
+
end
|
57
|
+
|
58
|
+
# pre-run cleanup
|
59
|
+
test = self.new nil
|
60
|
+
test.setup
|
61
|
+
test.teardown
|
62
|
+
|
63
|
+
def test_create_mailbox
|
64
|
+
@imap.create "directory#{@delim}"
|
65
|
+
|
66
|
+
assert_equal nil, @ip.create_mailbox('directory')
|
67
|
+
|
68
|
+
refute_nil @ip.create_mailbox('destination')
|
69
|
+
mailbox = @imap.list('', 'destination').first
|
70
|
+
assert_equal 'destination', mailbox.name
|
71
|
+
assert_equal [:Noinferiors, :Unmarked], mailbox.attr
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_delete_messages
|
75
|
+
util_message
|
76
|
+
uids = util_uids
|
77
|
+
|
78
|
+
@ip.delete_messages uids
|
79
|
+
|
80
|
+
assert_empty util_uids
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_delete_messages_no_expunge
|
84
|
+
util_message
|
85
|
+
uids = util_uids
|
86
|
+
|
87
|
+
@ip.delete_messages uids, false
|
88
|
+
|
89
|
+
uids = util_uids
|
90
|
+
|
91
|
+
refute_empty uids
|
92
|
+
assert_includes :Deleted, @imap.fetch(uids, 'FLAGS').first.attr['FLAGS']
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_move_messages
|
96
|
+
util_message
|
97
|
+
uids = util_uids
|
98
|
+
|
99
|
+
@ip.move_messages uids, 'destination'
|
100
|
+
|
101
|
+
assert_equal 0, @imap.search('ALL').length
|
102
|
+
|
103
|
+
assert_equal 1, @imap.list('', 'destination').length
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_show_messages
|
107
|
+
now = Time.now
|
108
|
+
util_message nil, now
|
109
|
+
uids = util_uids
|
110
|
+
|
111
|
+
out, = capture_io do
|
112
|
+
@ip.show_messages uids
|
113
|
+
end
|
114
|
+
|
115
|
+
expected = <<-EXPECTED
|
116
|
+
Subject: message 1
|
117
|
+
Date: #{now.rfc2822}
|
118
|
+
Message-Id: 1
|
119
|
+
|
120
|
+
EXPECTED
|
121
|
+
|
122
|
+
assert_equal expected, out
|
123
|
+
end
|
124
|
+
|
125
|
+
def util_message(flags = nil, time = Time.now)
|
126
|
+
@count ||= 0
|
127
|
+
@count += 1
|
128
|
+
|
129
|
+
message = <<-MESSAGE
|
130
|
+
From: from@example.com
|
131
|
+
To: to@example.com
|
132
|
+
Subject: message #{@count}
|
133
|
+
Date: #{time.rfc2822}
|
134
|
+
Message-Id: #{@count}
|
135
|
+
|
136
|
+
Hi, this is message number #{@count}
|
137
|
+
MESSAGE
|
138
|
+
|
139
|
+
@imap.append 'INBOX', message, flags, time
|
140
|
+
end
|
141
|
+
|
142
|
+
def util_uids
|
143
|
+
@imap.select 'INBOX'
|
144
|
+
@imap.search 'ALL'
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: imap_processor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: "1.2"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Hodel
|
@@ -30,7 +30,7 @@ cert_chain:
|
|
30
30
|
x52qPcexcYZR7w==
|
31
31
|
-----END CERTIFICATE-----
|
32
32
|
|
33
|
-
date: 2009-
|
33
|
+
date: 2009-07-07 00:00:00 -07:00
|
34
34
|
default_executable:
|
35
35
|
dependencies:
|
36
36
|
- !ruby/object:Gem::Dependency
|
@@ -41,18 +41,22 @@ dependencies:
|
|
41
41
|
requirements:
|
42
42
|
- - ">="
|
43
43
|
- !ruby/object:Gem::Version
|
44
|
-
version:
|
44
|
+
version: 2.3.2
|
45
45
|
version:
|
46
46
|
description: |-
|
47
47
|
IMAPProcessor is a client for processing messages on an IMAP server. It
|
48
48
|
provides some basic mechanisms for connecting to an IMAP server, determining
|
49
49
|
capabilities and handling messages.
|
50
50
|
|
51
|
-
IMAPProcessor ships with the imap_keywords
|
52
|
-
server for keywords set on messages in mailboxes
|
51
|
+
IMAPProcessor ships with the executables imap_keywords which can query an IMAP
|
52
|
+
server for keywords set on messages in mailboxes, imap_idle which can show new
|
53
|
+
messages in a mailbox and imap_archive which will archive old messages to a
|
54
|
+
new mailbox.
|
53
55
|
email:
|
54
56
|
- drbrain@segment7.net
|
55
57
|
executables:
|
58
|
+
- imap_archive
|
59
|
+
- imap_idle
|
56
60
|
- imap_keywords
|
57
61
|
extensions: []
|
58
62
|
|
@@ -66,10 +70,17 @@ files:
|
|
66
70
|
- Manifest.txt
|
67
71
|
- README.txt
|
68
72
|
- Rakefile
|
73
|
+
- bin/imap_archive
|
74
|
+
- bin/imap_idle
|
69
75
|
- bin/imap_keywords
|
70
76
|
- lib/imap_processor.rb
|
77
|
+
- lib/imap_processor/archive.rb
|
78
|
+
- lib/imap_processor/idle.rb
|
71
79
|
- lib/imap_processor/keywords.rb
|
72
80
|
- lib/imap_sasl_plain.rb
|
81
|
+
- lib/net/imap/date.rb
|
82
|
+
- lib/net/imap/idle.rb
|
83
|
+
- test/test_imap_processor.rb
|
73
84
|
has_rdoc: true
|
74
85
|
homepage: http://seattlerb.rubyforge.org/imap_processor
|
75
86
|
licenses: []
|
@@ -95,9 +106,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
106
|
requirements: []
|
96
107
|
|
97
108
|
rubyforge_project: seattlerb
|
98
|
-
rubygems_version: 1.3.
|
109
|
+
rubygems_version: 1.3.4
|
99
110
|
signing_key:
|
100
111
|
specification_version: 3
|
101
112
|
summary: IMAPProcessor is a client for processing messages on an IMAP server
|
102
|
-
test_files:
|
103
|
-
|
113
|
+
test_files:
|
114
|
+
- test/test_imap_processor.rb
|
metadata.gz.sig
CHANGED
Binary file
|