imap_processor 1.0
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 +2 -0
- data/.autotest +23 -0
- data/History.txt +6 -0
- data/Manifest.txt +9 -0
- data/README.txt +55 -0
- data/Rakefile +13 -0
- data/bin/imap_keywords +6 -0
- data/lib/imap_processor.rb +442 -0
- data/lib/imap_processor/keywords.rb +158 -0
- data/lib/imap_sasl_plain.rb +65 -0
- metadata +103 -0
- metadata.gz.sig +1 -0
data.tar.gz.sig
ADDED
data/.autotest
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'autotest/restart'
|
4
|
+
|
5
|
+
# Autotest.add_hook :initialize do |at|
|
6
|
+
# at.extra_files << "../some/external/dependency.rb"
|
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
|
20
|
+
|
21
|
+
# Autotest.add_hook :run_command do |at|
|
22
|
+
# system "rake build"
|
23
|
+
# end
|
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
= imap_processor
|
2
|
+
|
3
|
+
* http://seattlerb.rubyforge.org/imap_processor
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
IMAPProcessor is a client for processing messages on an IMAP server. It
|
8
|
+
provides some basic mechanisms for connecting to an IMAP server, determining
|
9
|
+
capabilities and handling messages.
|
10
|
+
|
11
|
+
IMAPProcessor ships with the imap_keywords executable which can query an IMAP
|
12
|
+
server for keywords set on messages in mailboxes.
|
13
|
+
|
14
|
+
== FEATURES/PROBLEMS:
|
15
|
+
|
16
|
+
* Connection toolkit
|
17
|
+
* Executable toolkit
|
18
|
+
* Only known to work with SASL/PLAIN authentication
|
19
|
+
|
20
|
+
== SYNOPSIS:
|
21
|
+
|
22
|
+
See IMAPProcessor and IMAPProcessor::Keywords for details
|
23
|
+
|
24
|
+
== REQUIREMENTS:
|
25
|
+
|
26
|
+
* IMAP server
|
27
|
+
|
28
|
+
== INSTALL:
|
29
|
+
|
30
|
+
* gem install imap_processor
|
31
|
+
|
32
|
+
== LICENSE:
|
33
|
+
|
34
|
+
(The MIT License)
|
35
|
+
|
36
|
+
Copyright (c) 2009 Eric Hodel
|
37
|
+
|
38
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
39
|
+
a copy of this software and associated documentation files (the
|
40
|
+
'Software'), to deal in the Software without restriction, including
|
41
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
42
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
43
|
+
permit persons to whom the Software is furnished to do so, subject to
|
44
|
+
the following conditions:
|
45
|
+
|
46
|
+
The above copyright notice and this permission notice shall be
|
47
|
+
included in all copies or substantial portions of the Software.
|
48
|
+
|
49
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
50
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
51
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
52
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
53
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
54
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
55
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'hoe'
|
5
|
+
$:.unshift 'lib'
|
6
|
+
require 'imap_processor'
|
7
|
+
|
8
|
+
Hoe.new 'imap_processor', IMAPProcessor::VERSION do |ip|
|
9
|
+
ip.rubyforge_name = 'seattlerb'
|
10
|
+
ip.developer('Eric Hodel', 'drbrain@segment7.net')
|
11
|
+
end
|
12
|
+
|
13
|
+
# vim: syntax=Ruby
|
data/bin/imap_keywords
ADDED
@@ -0,0 +1,442 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'optparse'
|
3
|
+
require 'net/imap'
|
4
|
+
require 'imap_sasl_plain'
|
5
|
+
|
6
|
+
##
|
7
|
+
# IMAPProcessor is a client for processing messages on an IMAP server.
|
8
|
+
#
|
9
|
+
# Subclasses need to provide:
|
10
|
+
#
|
11
|
+
# * A process_args class method that adds any extra options to the default
|
12
|
+
# IMAPProcessor options.
|
13
|
+
# * An initialize method that connects to an IMAP server and sets the @imap
|
14
|
+
# instance variable
|
15
|
+
# * A run method that uses the IMAP connection to process messages.
|
16
|
+
|
17
|
+
class IMAPProcessor
|
18
|
+
|
19
|
+
##
|
20
|
+
# The version of IMAPProcessor you are using
|
21
|
+
|
22
|
+
VERSION = '1.0'
|
23
|
+
|
24
|
+
##
|
25
|
+
# A Connection Struct that has +imap+ and +capability+ accessors
|
26
|
+
|
27
|
+
class Connection < Struct.new :imap, :capability
|
28
|
+
|
29
|
+
##
|
30
|
+
# Does this connection support the IDLE extension?
|
31
|
+
|
32
|
+
def idle?
|
33
|
+
capability.include? 'IDLE'
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Does this connection support the UIDPLUS extension?
|
38
|
+
|
39
|
+
def uidplus?
|
40
|
+
capability.include? 'UIDPLUS'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Net::IMAP connection, set this via #initialize
|
47
|
+
|
48
|
+
attr_reader :imap
|
49
|
+
|
50
|
+
##
|
51
|
+
# Options Hash from process_args
|
52
|
+
|
53
|
+
attr_reader :options
|
54
|
+
|
55
|
+
@@options = {}
|
56
|
+
@@extra_options = []
|
57
|
+
|
58
|
+
##
|
59
|
+
# Adds a --move option to the option parser which stores the destination
|
60
|
+
# mailbox in the MoveTo option. Call this from a subclass' process_args
|
61
|
+
# method.
|
62
|
+
|
63
|
+
def self.add_move
|
64
|
+
@@options[:MoveTo] = nil
|
65
|
+
|
66
|
+
@@extra_options << proc do |opts, options|
|
67
|
+
opts.on( "--move=MAILBOX",
|
68
|
+
"Mailbox to move message to",
|
69
|
+
"Default: #{options[:MoveTo].inspect}",
|
70
|
+
"Options file name: MoveTo") do |mailbox|
|
71
|
+
options[:MoveTo] = mailbox
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Handles processing of +args+ loading defaults from a file in ~ based on
|
78
|
+
# +processor_file+. Extra option defaults can be specified by
|
79
|
+
# +required_options+. Yields an option parser instance to add new
|
80
|
+
# OptionParser options to:
|
81
|
+
#
|
82
|
+
# class MyProcessor < IMAPProcessor
|
83
|
+
# def self.process_args(args)
|
84
|
+
# required_options = {
|
85
|
+
# :MoveTo => [nil, "MoveTo not set"],
|
86
|
+
# }
|
87
|
+
#
|
88
|
+
# super __FILE__, args, required_options do |opts, options|
|
89
|
+
# opts.banner << "Explain my_processor's executable"
|
90
|
+
#
|
91
|
+
# opts.on( "--move=MAILBOX",
|
92
|
+
# "Mailbox to move message to",
|
93
|
+
# "Default: #{options[:MoveTo].inspect}",
|
94
|
+
# "Options file name: MoveTo") do |mailbox|
|
95
|
+
# options[:MoveTo] = mailbox
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
|
100
|
+
def self.process_args(processor_file, args,
|
101
|
+
required_options = {}) # :yield: OptionParser
|
102
|
+
opts_file_name = File.basename processor_file, '.rb'
|
103
|
+
opts_file = File.expand_path "~/.#{opts_file_name}"
|
104
|
+
options = @@options.dup
|
105
|
+
|
106
|
+
if required_options then
|
107
|
+
required_options.each do |option, (default, message)|
|
108
|
+
raise ArgumentError,
|
109
|
+
"required_options message is missing for #{option}" if
|
110
|
+
default.nil? and message.nil?
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
if File.exist? opts_file then
|
115
|
+
unless File.stat(opts_file).mode & 077 == 0 then
|
116
|
+
$stderr.puts "WARNING! #{opts_file} is group/other readable or writable!"
|
117
|
+
$stderr.puts "WARNING! I'm not doing a thing until you fix it!"
|
118
|
+
exit 1
|
119
|
+
end
|
120
|
+
|
121
|
+
options.merge! YAML.load_file(opts_file)
|
122
|
+
end
|
123
|
+
|
124
|
+
options[:SSL] ||= true
|
125
|
+
options[:Username] ||= ENV['USER']
|
126
|
+
options[:Root] ||= nil
|
127
|
+
options[:Verbose] ||= false
|
128
|
+
options[:Debug] ||= false
|
129
|
+
|
130
|
+
required_options.each do |k,(v,m)|
|
131
|
+
options[k] ||= v
|
132
|
+
end
|
133
|
+
|
134
|
+
opts = OptionParser.new do |opts|
|
135
|
+
opts.program_name = File.basename $0
|
136
|
+
opts.banner = "Usage: #{opts.program_name} [options]\n\n"
|
137
|
+
|
138
|
+
opts.separator ''
|
139
|
+
opts.separator 'Connection options:'
|
140
|
+
|
141
|
+
opts.on("-H", "--host HOST",
|
142
|
+
"IMAP server host",
|
143
|
+
"Default: #{options[:Host].inspect}",
|
144
|
+
"Options file name: Host") do |host|
|
145
|
+
options[:Host] = host
|
146
|
+
end
|
147
|
+
|
148
|
+
opts.on("-P", "--port PORT",
|
149
|
+
"IMAP server port",
|
150
|
+
"Default: The correct port SSL/non-SSL mode",
|
151
|
+
"Options file name: Port") do |port|
|
152
|
+
options[:Port] = port
|
153
|
+
end
|
154
|
+
|
155
|
+
opts.on("-s", "--[no-]ssl",
|
156
|
+
"Use SSL for IMAP connection",
|
157
|
+
"Default: #{options[:SSL].inspect}",
|
158
|
+
"Options file name: SSL") do |ssl|
|
159
|
+
options[:SSL] = ssl
|
160
|
+
end
|
161
|
+
|
162
|
+
opts.on( "--[no-]debug",
|
163
|
+
"Display Net::IMAP debugging info",
|
164
|
+
"Default: #{options[:Debug].inspect}",
|
165
|
+
"Options file name: Debug") do |debug|
|
166
|
+
options[:Debug] = debug
|
167
|
+
end
|
168
|
+
|
169
|
+
opts.separator ''
|
170
|
+
opts.separator 'Login options:'
|
171
|
+
|
172
|
+
opts.on("-u", "--username USERNAME",
|
173
|
+
"IMAP username",
|
174
|
+
"Default: #{options[:Username].inspect}",
|
175
|
+
"Options file name: Username") do |username|
|
176
|
+
options[:Username] = username
|
177
|
+
end
|
178
|
+
|
179
|
+
opts.on("-p", "--password PASSWORD",
|
180
|
+
"IMAP password",
|
181
|
+
"Default: Read from ~/.imap_cleanse",
|
182
|
+
"Options file name: Password") do |password|
|
183
|
+
options[:Password] = password
|
184
|
+
end
|
185
|
+
|
186
|
+
authenticators = Net::IMAP.send :class_variable_get, :@@authenticators
|
187
|
+
auth_types = authenticators.keys.sort.join ', '
|
188
|
+
opts.on("-a", "--auth AUTH", auth_types,
|
189
|
+
"IMAP authentication type override",
|
190
|
+
"Authentication type will be auto-",
|
191
|
+
"discovered",
|
192
|
+
"Default: #{options[:Auth].inspect}",
|
193
|
+
"Options file name: Auth") do |auth|
|
194
|
+
options[:Auth] = auth
|
195
|
+
end
|
196
|
+
|
197
|
+
opts.separator ''
|
198
|
+
opts.separator "IMAP options:"
|
199
|
+
|
200
|
+
opts.on("-r", "--root ROOT",
|
201
|
+
"Root of mailbox hierarchy",
|
202
|
+
"Default: #{options[:Root].inspect}",
|
203
|
+
"Options file name: Root") do |root|
|
204
|
+
options[:Root] = root
|
205
|
+
end
|
206
|
+
|
207
|
+
opts.on("-b", "--boxes BOXES", Array,
|
208
|
+
"Comma-separated list of mailbox names",
|
209
|
+
"to search",
|
210
|
+
"Default: #{options[:Boxes].inspect}",
|
211
|
+
"Options file name: Boxes") do |boxes|
|
212
|
+
options[:Boxes] = boxes
|
213
|
+
end
|
214
|
+
|
215
|
+
opts.on("-v", "--[no-]verbose",
|
216
|
+
"Be verbose",
|
217
|
+
"Default: #{options[:Verbose].inspect}",
|
218
|
+
"Options file name: Verbose") do |verbose|
|
219
|
+
options[:Verbose] = verbose
|
220
|
+
end
|
221
|
+
|
222
|
+
opts.on("-q", "--quiet",
|
223
|
+
"Be quiet") do
|
224
|
+
options[:Verbose] = false
|
225
|
+
end
|
226
|
+
|
227
|
+
if block_given? then
|
228
|
+
opts.separator ''
|
229
|
+
opts.separator "#{self} options:"
|
230
|
+
|
231
|
+
yield opts, options if block_given?
|
232
|
+
end
|
233
|
+
|
234
|
+
@@extra_options.each do |block|
|
235
|
+
block.call opts, options
|
236
|
+
end
|
237
|
+
|
238
|
+
opts.separator ''
|
239
|
+
|
240
|
+
opts.banner << <<-EOF
|
241
|
+
|
242
|
+
Options may also be set in the options file ~/.#{opts_file_name}
|
243
|
+
|
244
|
+
Example ~/.#{opts_file_name}:
|
245
|
+
\tHost=mail.example.com
|
246
|
+
\tPassword=my password
|
247
|
+
|
248
|
+
EOF
|
249
|
+
end
|
250
|
+
|
251
|
+
opts.parse! args
|
252
|
+
|
253
|
+
options[:Port] ||= options[:SSL] ? 993 : 143
|
254
|
+
|
255
|
+
if options[:Host].nil? or
|
256
|
+
options[:Password].nil? or
|
257
|
+
options[:Boxes].nil? or
|
258
|
+
required_options.any? { |k,(v,m)| options[k].nil? } then
|
259
|
+
$stderr.puts opts
|
260
|
+
$stderr.puts
|
261
|
+
$stderr.puts "Host name not set" if options[:Host].nil?
|
262
|
+
$stderr.puts "Password not set" if options[:Password].nil?
|
263
|
+
$stderr.puts "Boxes not set" if options[:Boxes].nil?
|
264
|
+
required_options.each do |option_name, (option_value, missing_message)|
|
265
|
+
$stderr.puts missing_message if options[option_name].nil?
|
266
|
+
end
|
267
|
+
exit 1
|
268
|
+
end
|
269
|
+
|
270
|
+
return options
|
271
|
+
end
|
272
|
+
|
273
|
+
##
|
274
|
+
# Sets up an IMAP processor's options then calls its \#run method.
|
275
|
+
|
276
|
+
def self.run(args = ARGV, &block)
|
277
|
+
options = process_args args
|
278
|
+
client = new(options, &block)
|
279
|
+
client.run
|
280
|
+
rescue SystemExit
|
281
|
+
raise
|
282
|
+
rescue Exception => e
|
283
|
+
$stderr.puts "Failed to finish with exception: #{e.class}:#{e.message}"
|
284
|
+
$stderr.puts "\t#{e.backtrace.join "\n\t"}"
|
285
|
+
|
286
|
+
exit 1
|
287
|
+
ensure
|
288
|
+
client.imap.logout if client
|
289
|
+
end
|
290
|
+
|
291
|
+
##
|
292
|
+
# Handles the basic settings from +options+ including verbosity, mailboxes
|
293
|
+
# to process, and Net::IMAP::debug
|
294
|
+
|
295
|
+
def initialize(options)
|
296
|
+
@options = options
|
297
|
+
@verbose = true
|
298
|
+
@boxes = options[:Boxes]
|
299
|
+
Net::IMAP.debug = options[:Debug]
|
300
|
+
end
|
301
|
+
|
302
|
+
##
|
303
|
+
# Connects to IMAP server +host+ at +port+ using ssl if +ssl+ is true then
|
304
|
+
# logs in as +username+ with +password+. IMAPProcessor is only known to
|
305
|
+
# work with PLAIN auth on SSL sockets.
|
306
|
+
#
|
307
|
+
# Returns a Connection object.
|
308
|
+
|
309
|
+
def connect(host, port, ssl, username, password, auth = nil)
|
310
|
+
imap = Net::IMAP.new host, port, ssl
|
311
|
+
log "Connected to imap://#{host}:#{port}/"
|
312
|
+
|
313
|
+
capability = imap.capability
|
314
|
+
|
315
|
+
log "Capabilities: #{capability.join ', '}"
|
316
|
+
|
317
|
+
auth_caps = capability.select { |c| c =~ /^AUTH/ }
|
318
|
+
|
319
|
+
if auth.nil? then
|
320
|
+
raise "Couldn't find a supported auth type" if auth_caps.empty?
|
321
|
+
auth = auth_caps.first.sub(/AUTH=/, '')
|
322
|
+
end
|
323
|
+
|
324
|
+
auth = auth.upcase
|
325
|
+
log "Trying #{auth} authentication"
|
326
|
+
imap.authenticate auth, username, password
|
327
|
+
log "Logged in as #{username}"
|
328
|
+
|
329
|
+
Connection.new imap, capability
|
330
|
+
end
|
331
|
+
|
332
|
+
##
|
333
|
+
# Yields each uid and message as a TMail::Message for +uids+ of MIME type
|
334
|
+
# +type+.
|
335
|
+
#
|
336
|
+
# If there's an exception raised during handling a message the subject,
|
337
|
+
# message-id and inspected body are logged.
|
338
|
+
#
|
339
|
+
# Returns the uids of successfully handled messages.
|
340
|
+
|
341
|
+
def each_message(uids, type) # :yields: TMail::Mail
|
342
|
+
parts = mime_parts uids, type
|
343
|
+
|
344
|
+
uids = []
|
345
|
+
|
346
|
+
each_part parts, true do |uid, message|
|
347
|
+
mail = TMail::Mail.parse message
|
348
|
+
|
349
|
+
begin
|
350
|
+
yield uid, mail
|
351
|
+
|
352
|
+
uids << uid
|
353
|
+
rescue => e
|
354
|
+
log e.message
|
355
|
+
puts "\t#{e.backtrace.join "\n\t"}" unless $DEBUG # backtrace at bottom
|
356
|
+
log "Subject: #{mail.subject}"
|
357
|
+
log "Message-Id: #{mail.message_id}"
|
358
|
+
p mail.body if verbose?
|
359
|
+
|
360
|
+
raise if $DEBUG
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
uids
|
365
|
+
end
|
366
|
+
|
367
|
+
##
|
368
|
+
# Yields each message part from +parts+. If +header+ is true, a complete
|
369
|
+
# message is yielded, appropriately joined for use with TMail::Mail.
|
370
|
+
|
371
|
+
def each_part(parts, header = false) # :yields: uid, message
|
372
|
+
parts.each do |uid, section|
|
373
|
+
sequence = ["BODY[#{section}]"]
|
374
|
+
sequence.unshift "BODY[#{section}.MIME]" unless section == 'TEXT'
|
375
|
+
sequence.unshift 'BODY[HEADER]' if header
|
376
|
+
|
377
|
+
body = @imap.fetch(uid, sequence).first
|
378
|
+
|
379
|
+
sequence = sequence.map { |item| body.attr[item] }
|
380
|
+
|
381
|
+
unless section == 'TEXT' and header then
|
382
|
+
sequence[0].sub!(/\r\n\z/, '')
|
383
|
+
end
|
384
|
+
|
385
|
+
yield uid, sequence.join
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
##
|
390
|
+
# Logs +message+ to $stderr if verbose
|
391
|
+
|
392
|
+
def log(message)
|
393
|
+
return unless @verbose
|
394
|
+
$stderr.puts "# #{message}"
|
395
|
+
end
|
396
|
+
|
397
|
+
##
|
398
|
+
# Retrieves the BODY data item name for the +mime_type+ part from messages
|
399
|
+
# +uids+. Returns an array of uid/part pairs. If no matching part with
|
400
|
+
# +mime_type+ is found the uid is omitted.
|
401
|
+
#
|
402
|
+
# Returns an Array of uid, section pairs.
|
403
|
+
#
|
404
|
+
# Use a subsequent Net::IMAP#fetch to retrieve the selected part.
|
405
|
+
|
406
|
+
def mime_parts(uids, mime_type)
|
407
|
+
media_type, subtype = mime_type.upcase.split('/', 2)
|
408
|
+
|
409
|
+
structures = @imap.fetch uids, 'BODYSTRUCTURE'
|
410
|
+
|
411
|
+
structures.zip(uids).map do |body, uid|
|
412
|
+
section = nil
|
413
|
+
structure = body.attr['BODYSTRUCTURE']
|
414
|
+
|
415
|
+
case structure
|
416
|
+
when Net::IMAP::BodyTypeMultipart then
|
417
|
+
parts = structure.parts
|
418
|
+
|
419
|
+
section = parts.each_with_index do |part, index|
|
420
|
+
break index if part.media_type == media_type and
|
421
|
+
part.subtype == subtype
|
422
|
+
end
|
423
|
+
|
424
|
+
next unless Integer === section
|
425
|
+
when Net::IMAP::BodyTypeText, Net::IMAP::BodyTypeBasic then
|
426
|
+
section = 'TEXT' if structure.media_type == media_type and
|
427
|
+
structure.subtype == subtype
|
428
|
+
end
|
429
|
+
|
430
|
+
[uid, section]
|
431
|
+
end.compact
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# Did the user set --verbose?
|
436
|
+
|
437
|
+
def verbose?
|
438
|
+
@verbose
|
439
|
+
end
|
440
|
+
|
441
|
+
end
|
442
|
+
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'imap_processor'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Lists keywords present on a server
|
5
|
+
|
6
|
+
class IMAPProcessor::Keywords < IMAPProcessor
|
7
|
+
|
8
|
+
def self.process_args(args)
|
9
|
+
required_options = {
|
10
|
+
:List => true
|
11
|
+
}
|
12
|
+
|
13
|
+
super __FILE__, args, required_options do |opts, options|
|
14
|
+
opts.banner << <<-EOF
|
15
|
+
imap_keywords lists keywords on an IMAP server and allows you to delete
|
16
|
+
previously set keywords.
|
17
|
+
EOF
|
18
|
+
|
19
|
+
# opts.on( "--add",
|
20
|
+
# "Add keyword(s) to all messages") do |add|
|
21
|
+
# options[:Add] = add
|
22
|
+
# end
|
23
|
+
|
24
|
+
opts.on( "--delete",
|
25
|
+
"Delete keyword(s) from all messages") do |delete|
|
26
|
+
options[:Delete] = delete
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on( "--keywords=KEYWORDS", Array,
|
30
|
+
"Select messages with keyword(s),",
|
31
|
+
"which will be ANDed") do |keywords|
|
32
|
+
options[:Keywords] = keywords
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on( "--[no-]list",
|
36
|
+
"Don't display messages") do |list|
|
37
|
+
options[:List] = list
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on( "--not",
|
41
|
+
"Select messages without --keywords") do
|
42
|
+
options[:Not] = true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(options)
|
48
|
+
super
|
49
|
+
|
50
|
+
@add = options[:Add]
|
51
|
+
@delete = options[:Delete]
|
52
|
+
@keywords = options[:Keywords]
|
53
|
+
@not = options[:Not] ? 'NOT' : nil
|
54
|
+
@list = options[:List]
|
55
|
+
|
56
|
+
if @add and @delete then
|
57
|
+
raise OptionParser::InvalidOption, "--add and --delete are exclusive"
|
58
|
+
elsif @keywords.nil? and (@add or @delete) then
|
59
|
+
raise OptionParser::InvalidOption,
|
60
|
+
"--add and --delete require --keywords"
|
61
|
+
end
|
62
|
+
|
63
|
+
connection = connect options[:Host], options[:Port], options[:SSL],
|
64
|
+
options[:Username], options[:Password], options[:Auth]
|
65
|
+
|
66
|
+
@imap = connection.imap
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Turns +flags+ into a format usable by the IMAP server
|
71
|
+
|
72
|
+
def flags_to_literal(flags)
|
73
|
+
flags.flatten.map do |flag|
|
74
|
+
case flag
|
75
|
+
when Symbol then "\\#{flag}"
|
76
|
+
else flag
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Makes a SEARCH argument set from +keywords+
|
83
|
+
|
84
|
+
def make_search(keywords)
|
85
|
+
if keywords then
|
86
|
+
keywords.map do |kw|
|
87
|
+
case kw
|
88
|
+
when '\Answered', '\Deleted', '\Draft', '\Flagged',
|
89
|
+
'\Recent', '\Seen' then
|
90
|
+
[@not, kw[1..-1].upcase]
|
91
|
+
else
|
92
|
+
[@not, 'KEYWORD', kw]
|
93
|
+
end
|
94
|
+
end.flatten.compact
|
95
|
+
else
|
96
|
+
%w[ALL]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def run
|
101
|
+
@boxes.each do |mailbox|
|
102
|
+
response = @imap.select mailbox
|
103
|
+
log "Selected mailbox #{mailbox}"
|
104
|
+
puts "Previously set flags:"
|
105
|
+
puts flags_to_literal(@imap.responses['FLAGS']).join(' ')
|
106
|
+
puts
|
107
|
+
|
108
|
+
puts "Permanent flags:"
|
109
|
+
puts flags_to_literal(@imap.responses['PERMANENTFLAGS']).join(' ')
|
110
|
+
puts
|
111
|
+
|
112
|
+
search = make_search @keywords
|
113
|
+
|
114
|
+
log "SEARCH #{search.join ' '}"
|
115
|
+
uids = @imap.search search
|
116
|
+
|
117
|
+
if uids.empty? then
|
118
|
+
puts "No messages"
|
119
|
+
next
|
120
|
+
else
|
121
|
+
puts "#{uids.length} messages in #{mailbox}#{@list ? ':' : ''}"
|
122
|
+
end
|
123
|
+
|
124
|
+
@imap.store uids, '-FLAGS.SILENT', @keywords if @delete
|
125
|
+
|
126
|
+
show_messages uids
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Displays messages in +uids+ and their keywords
|
132
|
+
|
133
|
+
def show_messages(uids)
|
134
|
+
return unless @list
|
135
|
+
|
136
|
+
responses = @imap.fetch uids, [
|
137
|
+
'BODY.PEEK[HEADER]',
|
138
|
+
'FLAGS'
|
139
|
+
]
|
140
|
+
|
141
|
+
responses.each do |res|
|
142
|
+
header = res.attr['BODY[HEADER]']
|
143
|
+
|
144
|
+
header =~ /^Subject: (.*)/i
|
145
|
+
puts "Subject: #{$1}"
|
146
|
+
|
147
|
+
header =~ /^Message-Id: (.*)/i
|
148
|
+
puts "Message-Id: #{$1}"
|
149
|
+
|
150
|
+
flags = res.attr['FLAGS'].map { |flag| flag.inspect }.join ', '
|
151
|
+
|
152
|
+
puts "Flags: #{flags}"
|
153
|
+
puts
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'net/imap'
|
3
|
+
|
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
|
+
##
|
28
|
+
# RFC 2595 PLAIN Authenticator for Net::IMAP. Only for use with SSL (but not
|
29
|
+
# enforced).
|
30
|
+
|
31
|
+
class Net::IMAP::PlainAuthenticator
|
32
|
+
|
33
|
+
##
|
34
|
+
# From RFC 2595 Section 6. PLAIN SASL Authentication
|
35
|
+
#
|
36
|
+
# The mechanism consists of a single message from the client to the
|
37
|
+
# server. The client sends the authorization identity (identity to
|
38
|
+
# login as), followed by a US-ASCII NUL character, followed by the
|
39
|
+
# authentication identity (identity whose password will be used),
|
40
|
+
# followed by a US-ASCII NUL character, followed by the clear-text
|
41
|
+
# password. The client may leave the authorization identity empty to
|
42
|
+
# indicate that it is the same as the authentication identity.
|
43
|
+
|
44
|
+
def process(data)
|
45
|
+
return [@user, @user, @password].join("\0")
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
##
|
51
|
+
# Creates a new PlainAuthenticator that will authenticate with +user+ and
|
52
|
+
# +password+.
|
53
|
+
|
54
|
+
def initialize(user, password)
|
55
|
+
@user = user
|
56
|
+
@password = password
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
if defined? OpenSSL then
|
62
|
+
Net::IMAP.add_authenticator 'PLAIN', Net::IMAP::PlainAuthenticator
|
63
|
+
end
|
64
|
+
|
65
|
+
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: imap_processor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.0"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Hodel
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain:
|
11
|
+
- |
|
12
|
+
-----BEGIN CERTIFICATE-----
|
13
|
+
MIIDNjCCAh6gAwIBAgIBADANBgkqhkiG9w0BAQUFADBBMRAwDgYDVQQDDAdkcmJy
|
14
|
+
YWluMRgwFgYKCZImiZPyLGQBGRYIc2VnbWVudDcxEzARBgoJkiaJk/IsZAEZFgNu
|
15
|
+
ZXQwHhcNMDcxMjIxMDIwNDE0WhcNMDgxMjIwMDIwNDE0WjBBMRAwDgYDVQQDDAdk
|
16
|
+
cmJyYWluMRgwFgYKCZImiZPyLGQBGRYIc2VnbWVudDcxEzARBgoJkiaJk/IsZAEZ
|
17
|
+
FgNuZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbbgLrGLGIDE76
|
18
|
+
LV/cvxdEzCuYuS3oG9PrSZnuDweySUfdp/so0cDq+j8bqy6OzZSw07gdjwFMSd6J
|
19
|
+
U5ddZCVywn5nnAQ+Ui7jMW54CYt5/H6f2US6U0hQOjJR6cpfiymgxGdfyTiVcvTm
|
20
|
+
Gj/okWrQl0NjYOYBpDi+9PPmaH2RmLJu0dB/NylsDnW5j6yN1BEI8MfJRR+HRKZY
|
21
|
+
mUtgzBwF1V4KIZQ8EuL6I/nHVu07i6IkrpAgxpXUfdJQJi0oZAqXurAV3yTxkFwd
|
22
|
+
g62YrrW26mDe+pZBzR6bpLE+PmXCzz7UxUq3AE0gPHbiMXie3EFE0oxnsU3lIduh
|
23
|
+
sCANiQ8BAgMBAAGjOTA3MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
24
|
+
BBS5k4Z75VSpdM0AclG2UvzFA/VW5DANBgkqhkiG9w0BAQUFAAOCAQEAHagT4lfX
|
25
|
+
kP/hDaiwGct7XPuVGbrOsKRVD59FF5kETBxEc9UQ1clKWngf8JoVuEoKD774dW19
|
26
|
+
bU0GOVWO+J6FMmT/Cp7nuFJ79egMf/gy4gfUfQMuvfcr6DvZUPIs9P/TlK59iMYF
|
27
|
+
DIOQ3DxdF3rMzztNUCizN4taVscEsjCcgW6WkUJnGdqlu3OHWpQxZBJkBTjPCoc6
|
28
|
+
UW6on70SFPmAy/5Cq0OJNGEWBfgD9q7rrs/X8GGwUWqXb85RXnUVi/P8Up75E0ag
|
29
|
+
14jEc90kN+C7oI/AGCBN0j6JnEtYIEJZibjjDJTSMWlUKKkj30kq7hlUC2CepJ4v
|
30
|
+
x52qPcexcYZR7w==
|
31
|
+
-----END CERTIFICATE-----
|
32
|
+
|
33
|
+
date: 2009-05-12 00:00:00 -07:00
|
34
|
+
default_executable:
|
35
|
+
dependencies:
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: hoe
|
38
|
+
type: :development
|
39
|
+
version_requirement:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.12.1
|
45
|
+
version:
|
46
|
+
description: |-
|
47
|
+
IMAPProcessor is a client for processing messages on an IMAP server. It
|
48
|
+
provides some basic mechanisms for connecting to an IMAP server, determining
|
49
|
+
capabilities and handling messages.
|
50
|
+
|
51
|
+
IMAPProcessor ships with the imap_keywords executable which can query an IMAP
|
52
|
+
server for keywords set on messages in mailboxes.
|
53
|
+
email:
|
54
|
+
- drbrain@segment7.net
|
55
|
+
executables:
|
56
|
+
- imap_keywords
|
57
|
+
extensions: []
|
58
|
+
|
59
|
+
extra_rdoc_files:
|
60
|
+
- History.txt
|
61
|
+
- Manifest.txt
|
62
|
+
- README.txt
|
63
|
+
files:
|
64
|
+
- .autotest
|
65
|
+
- History.txt
|
66
|
+
- Manifest.txt
|
67
|
+
- README.txt
|
68
|
+
- Rakefile
|
69
|
+
- bin/imap_keywords
|
70
|
+
- lib/imap_processor.rb
|
71
|
+
- lib/imap_processor/keywords.rb
|
72
|
+
- lib/imap_sasl_plain.rb
|
73
|
+
has_rdoc: true
|
74
|
+
homepage: http://seattlerb.rubyforge.org/imap_processor
|
75
|
+
licenses: []
|
76
|
+
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options:
|
79
|
+
- --main
|
80
|
+
- README.txt
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: "0"
|
88
|
+
version:
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: "0"
|
94
|
+
version:
|
95
|
+
requirements: []
|
96
|
+
|
97
|
+
rubyforge_project: seattlerb
|
98
|
+
rubygems_version: 1.3.3
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: IMAPProcessor is a client for processing messages on an IMAP server
|
102
|
+
test_files: []
|
103
|
+
|
metadata.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
<���X�:�+A���@(TM}��v�<Qj���������ԭ\�ֈ�,�}�<�U�@���~bR��z�&2��C�.MF�aZ1��=|f�R�[�b�O�����yO��mv�KU�νwFfP�+>$�Jf&�R��w������J6Cj��D)B�Y�Z��
|