imap_processor 1.1.1 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|