IMAPCleanse 1.0.0 → 1.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/Manifest.txt CHANGED
@@ -1,16 +1,10 @@
1
- .DS_Store
2
- CVS/Entries
3
- CVS/Repository
4
- CVS/Root
5
1
  LICENSE
6
2
  Manifest.txt
7
3
  README
8
4
  Rakefile
9
- bin/CVS/Entries
10
- bin/CVS/Repository
11
- bin/CVS/Root
12
5
  bin/imap_cleanse
13
- lib/CVS/Entries
14
- lib/CVS/Repository
15
- lib/CVS/Root
6
+ bin/imap_flag
16
7
  lib/imap_cleanse.rb
8
+ lib/imap_client.rb
9
+ lib/imap_flag.rb
10
+ lib/imap_sasl_plain.rb
data/README CHANGED
@@ -10,7 +10,11 @@ http://seattlerb.rubyforge.org/IMAPCleanse/
10
10
 
11
11
  == About
12
12
 
13
- IMAPCleanse removes old, read, unflagged messages from your IMAP mailboxes.
13
+ IMAPCleanse removes old, read, unflagged messages from my IMAP mailboxes.
14
+
15
+ IMAPFlag flags messages I find interesting so I don\'t have to!
16
+
17
+ Both these tools can do this for you, too!
14
18
 
15
19
  == Why?
16
20
 
@@ -19,8 +23,20 @@ have context when reading threads. Since I'm lazy my more-trafficed mailboxes
19
23
  can end up with tens of thousands of read messages. Deleting this many
20
24
  messages with Mail.app is time consuming and boring.
21
25
 
22
- So I wrote IMAPCleanse to clean out my old mailboxes for me. If I want to keep
23
- a message around for forever I'll just flag it and IMAPCleanse won't touch it.
26
+ So I wrote imap_cleanse to clean out my old mailboxes for me. If I want to
27
+ keep a message around for forever I'll just flag it and imap_cleanse won't
28
+ touch it.
29
+
30
+ imap_cleanse eventually became known as Part One of my Plan for Total Email
31
+ Domination.
32
+
33
+ Next up I decided to automatically flag messages that were interesting. (Part
34
+ Two of my Plan for Total Email Domination.) I defined interesting as messages
35
+ I responded to, messages I wrote (naturally!) and messages in response to
36
+ messages I wrote.
37
+
38
+ Part Three of my Plan for Total Email Domination is awaiting more flagged
39
+ messages.
24
40
 
25
41
  == Installing IMAPCleanse
26
42
 
@@ -28,19 +44,19 @@ Just install the gem:
28
44
 
29
45
  $ sudo gem install IMAPCleanse
30
46
 
31
- == Using IMAPCleanse
47
+ == Using imap_cleanse
32
48
 
33
49
  In short:
34
50
 
35
51
  imap_cleanse -H mail.example.com -p mypassword -b Lists/FreeBSD/current,Lists/Ruby -a 30
36
52
 
37
- The help for IMAPCleanse should be sufficiently verbose, but here's a couple of
53
+ The help for imap_cleanse should be sufficiently verbose, but here's a couple of
38
54
  tips:
39
55
 
40
56
  === --noop and --verbose
41
57
 
42
- The --noop flag tells IMAPCleanse not to delete anything. When combined with
43
- the --verbose flag you can see how many messages IMAPCleanse would have deleted
58
+ The --noop flag tells imap_cleanse not to delete anything. When combined with
59
+ the --verbose flag you can see how many messages imap_cleanse would have deleted
44
60
  from which mailboxes.
45
61
 
46
62
  $ ruby -Ilib bin/imap_cleanse -nv
@@ -60,13 +76,14 @@ from which mailboxes.
60
76
  # Found 0 messages
61
77
  # Done. Found 0 messages in 23 mailboxes
62
78
 
63
- (Since I just ran IMAPCleanse it didn't have anything to do.)
79
+ (Since I just ran imap_cleanse it didn't have anything to do.)
64
80
 
65
81
  === ~/.imap_cleanse
66
82
 
67
83
  The ~/.imap_cleanse file can hold your password and other options so you don't
68
84
  have to type them in on the command line every time. The format is simple,
69
- just the options file name followed by '=' followed by the argument.
85
+ just the option name followed by '=' followed by the argument. (Check -v for
86
+ option names.)
70
87
 
71
88
  No whitespace is stripped from options, so be sure to do that yourself. Mine
72
89
  looks something like this:
@@ -77,4 +94,21 @@ looks something like this:
77
94
  Boxes=Lists/FreeBSD/current,Lists/FreeBSD/performance,Lists/FreeBSD/Soekris,Lists/FreeBSD/stable,Lists/Ruby
78
95
  Age=30
79
96
  Password=my password
97
+ Email=drbrain@segment7.net
98
+
99
+ == Using imap_flag
100
+
101
+ In short:
102
+
103
+ imap_flag -H mail.example.com -p mypassword -b Lists/FreeBSD/current,Lists/Ruby -e drbrain@segment7.net
104
+
105
+ The help for imap_flag should be sufficiently verbose and the tips are the same
106
+ as those for imap_cleanse. (imap_flag even reads ~/.imap_cleanse, so you can
107
+ shove that extra Email option right in there!)
108
+
109
+ == Bugs
110
+
111
+ Yeah, there probably is one, or maybe even three. Report them here:
112
+
113
+ http://rubyforge.org/tracker/?group_id=1513
80
114
 
data/Rakefile CHANGED
@@ -3,21 +3,24 @@ require 'rake'
3
3
  require 'rake/testtask'
4
4
  require 'rake/rdoctask'
5
5
  require 'rake/gempackagetask'
6
+ require 'rake/contrib/sshpublisher'
6
7
 
7
8
  $VERBOSE = nil
8
9
 
9
10
  spec = Gem::Specification.new do |s|
10
11
  s.name = 'IMAPCleanse'
11
- s.version = '1.0.0'
12
- s.summary = 'Cleanses your IMAP mailboxes of oldness'
13
- s.description = 'IMAPCleanse removes old, read, unflagged messages from your IMAP mailboxes so you don\'t have to!'
12
+ s.version = '1.1.0'
13
+ s.summary = 'Removes mailbox oldness, finds mailbox interestingness!'
14
+ s.description = 'IMAPCleanse removes old, read, unflagged messages from your IMAP mailboxes so you don\'t have to!
15
+
16
+ IMAPFlag flags messages I find interesting so I don\'t have to!'
14
17
  s.author = 'Eric Hodel'
15
18
  s.email = 'drbrain@segment7.net'
16
19
 
17
20
  s.has_rdoc = true
18
21
  s.files = File.read('Manifest.txt').split($/)
19
22
  s.require_path = 'lib'
20
- s.executables = 'imap_cleanse'
23
+ s.executables = %w[imap_cleanse imap_flag]
21
24
  end
22
25
 
23
26
  desc 'Run tests'
@@ -40,7 +43,16 @@ Rake::RDocTask.new :rdoc do |rd|
40
43
  rd.rdoc_files.add 'lib', 'README', 'LICENSE'
41
44
  rd.main = 'README'
42
45
  rd.options << '-d' if `which dot` =~ /\/dot/
43
- rd.options << '-t IMAPCreate'
46
+ rd.options << '-t IMAPCleanse'
47
+ end
48
+
49
+ desc 'Upload RDoc to RubyForge'
50
+ task :upload => :rdoc do
51
+ user = "#{ENV['USER']}@rubyforge.org"
52
+ project = '/var/www/gforge-projects/seattlerb/IMAPCleanse'
53
+ local_dir = 'doc'
54
+ pub = Rake::SshDirPublisher.new user, project, local_dir
55
+ pub.upload
44
56
  end
45
57
 
46
58
  desc 'Build Gem'
data/bin/imap_flag ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'imap_flag'
4
+
5
+ IMAPFlag.run
6
+
data/lib/imap_cleanse.rb CHANGED
@@ -1,346 +1,68 @@
1
- require 'net/imap'
2
- require 'optparse'
3
- require 'pp'
4
- require 'time'
5
-
6
- class Time
7
-
8
- ##
9
- # Formats this Time as an IMAP-style date.
10
- #
11
- # RFC 2060 doesn't specify the format of its times. Unfortunately it is
12
- # almost but not quite RFC 822 dates.
13
-
14
- def imapdate
15
- strftime '%d-%b-%Y %H:%M %Z'
16
- end
17
- end
18
-
19
- ##
20
- # RFC 2595 PLAIN Authenticator for Net::IMAP. Only for use with SSL (but not
21
- # enforced).
22
-
23
- class Net::IMAP::PlainAuthenticator
24
-
25
- ##
26
- # From RFC 2595 Section 6. PLAIN SASL Authentication
27
- #
28
- # The mechanism consists of a single message from the client to the
29
- # server. The client sends the authorization identity (identity to
30
- # login as), followed by a US-ASCII NUL character, followed by the
31
- # authentication identity (identity whose password will be used),
32
- # followed by a US-ASCII NUL character, followed by the clear-text
33
- # password. The client may leave the authorization identity empty to
34
- # indicate that it is the same as the authentication identity.
35
-
36
- def process(data)
37
- return [@user, @user, @password].join("\0")
38
- end
39
-
40
- private
41
-
42
- ##
43
- # Creates a new PlainAuthenticator that will authenticate with +user+ and
44
- # +password+.
45
-
46
- def initialize(user, password)
47
- @user = user
48
- @password = password
49
- end
50
-
51
- end
52
-
53
- if defined? OpenSSL then
54
- Net::IMAP.add_authenticator 'PLAIN', Net::IMAP::PlainAuthenticator
55
- end
1
+ require 'imap_client'
56
2
 
57
3
  ##
58
4
  # IMAPCleanse removes old messages from your IMAP mailboxes so you don't have
59
5
  # to!
6
+ #
7
+ # aka part three of my Plan for Total Email Domination.
8
+ #
9
+ # IMAPClient doesn't remove messages you haven't read nor messages you've
10
+ # flagged. See also IMAPFlag for automatic flagging goodness!
60
11
 
61
- class IMAPCleanse
12
+ class IMAPCleanse < IMAPClient
62
13
 
63
14
  ##
64
15
  # Handles processing of +args+.
65
16
 
66
17
  def self.process_args(args)
67
- opts_file = File.expand_path '~/.imap_cleanse'
68
-
69
- unless File.stat(opts_file).mode & 077 == 0 then
70
- STDERR.puts "WARNING! #{opts_file} is group/other readable or writable!"
71
- STDERR.puts "WARNING! I'm not doing a thing until you fix it!"
72
- exit 1
73
- end
74
-
75
- options = {}
76
-
77
- File.readlines(opts_file).map { |l| l.chomp.split '=', 2 }.each do |k,v|
78
- v = true if v == 'true'
79
- v = false if v == 'false'
80
- v = Integer(v) rescue v
81
- options[k.intern] = v
82
- end
83
-
84
- options[:SSL] ||= true
85
- options[:Username] ||= ENV['USER']
86
- options[:Root] ||= 'mail'
87
- options[:Noop] ||= false
88
- options[:Verbose] ||= false
89
-
90
- opts = OptionParser.new do |opts|
91
- opts.banner = 'Usage: imap_cleanse [options]'
92
- opts.separator ''
93
- opts.separator 'Options may also be set in the options file ~/.imap_cleanse.'
94
- opts.separator ''
95
- opts.separator 'Example ~/.imap_cleanse:'
96
- opts.separator "\tHost=mail.example.com"
97
- opts.separator "\tPassword=my password"
98
-
99
- opts.separator ''
100
- opts.separator 'Connection options:'
101
-
102
- opts.on("-H", "--host HOST",
103
- "IMAP server host",
104
- "Default: #{options[:Host].inspect}",
105
- "Options file name: Host") do |host|
106
- options[:Host] = host
107
- end
108
-
109
- opts.on("-P", "--port PORT",
110
- "IMAP server port",
111
- "Default: The correct port SSL/non-SSL mode",
112
- "Options file name: Port") do |port|
113
- options[:Port] = port
114
- end
115
-
116
- opts.on("-s", "--[no-]ssl",
117
- "Use SSL for IMAP connection",
118
- "Default: #{options[:SSL].inspect}",
119
- "Options file name: SSL") do |ssl|
120
- options[:SSL] = ssl
121
- end
122
-
123
- opts.separator ''
124
- opts.separator 'Login options:'
125
-
126
- opts.on("-u", "--username USERNAME",
127
- "IMAP username",
128
- "Default: #{options[:Username].inspect}",
129
- "Options file name: Username") do |username|
130
- options[:Username] = username
131
- end
132
-
133
- opts.on("-p", "--password PASSWORD",
134
- "IMAP password",
135
- "Default: Read from ~/.imap_cleanse",
136
- "Options file name: Password") do |password|
137
- options[:Password] = password
138
- end
139
-
140
- opts.separator ''
141
- opts.separator 'Cleansing options:'
142
-
143
- opts.on("-r", "--root ROOT",
144
- "Root of mailbox hierarchy",
145
- "Default: #{options[:Root].inspect}",
146
- "Options file name: Root") do |root|
147
- options[:Root] = root
148
- end
149
-
150
- opts.on("-b", "--boxes BOXES",
151
- "Comma-separated list of mailbox name",
152
- "prefixes to cleanse",
153
- "Default: #{options[:Boxes].inspect}",
154
- "Options file name: Boxes") do |boxes|
155
- options[:Boxes] = boxes
156
- end
18
+ extra_options = { :Age => [nil, 'Age option not set'] }
157
19
 
20
+ super args, extra_options do |opts, options|
158
21
  opts.on("-a", "--age AGE",
159
22
  "Delete messages more than AGE days old",
160
23
  "Default: #{options[:Age].inspect}",
161
24
  "Options file name: Age", Integer) do |age|
162
25
  options[:Age] = age
163
26
  end
164
-
165
- opts.on("-n", "--noop",
166
- "Perform no destructive operations",
167
- "Best used with the verbose option",
168
- "Default: #{options[:Noop].inspect}",
169
- "Options file name: Noop") do |noop|
170
- options[:Noop] = noop
171
- end
172
-
173
- opts.on("-v", "--[no-]verbose",
174
- "Be verbose",
175
- "Default: #{options[:Verbose].inspect}",
176
- "Options file name: Verbose") do |verbose|
177
- options[:Verbose] = verbose
178
- end
179
-
180
- opts.separator ''
181
-
182
- opts.on("-h", "--help",
183
- "You're looking at it") do
184
- STDERR.puts opts
185
- exit 1
186
- end
187
-
188
- opts.separator ''
189
- end
190
-
191
- opts.parse! args
192
-
193
- options[:Port] ||= options[:SSL] ? 993 : 143
194
-
195
- if options[:Host].nil? or
196
- options[:Password].nil? or
197
- options[:Boxes].nil? or
198
- options[:Age].nil? then
199
- STDERR.puts opts
200
- STDERR.puts
201
- STDERR.puts "Host name not set" if options[:Host].nil?
202
- STDERR.puts "Password not set" if options[:Password].nil?
203
- STDERR.puts "Boxes option not set" if options[:Boxes].nil?
204
- STDERR.puts "Age option not set" if options[:Age].nil?
205
- exit 1
206
27
  end
207
-
208
- return options
209
- end
210
-
211
- ##
212
- # Sets up IMAPCleanse options then cleanses mailboxes.
213
-
214
- def self.run(args = ARGV)
215
- options = process_args args
216
- cleanser = new options
217
- cleanser.cleanse
218
28
  end
219
29
 
220
30
  ##
221
31
  # Creates a new IMAPCleanse from +options+.
222
32
  #
223
33
  # Options include:
224
- # +:Verbose+:: Verbose flag
225
- # +:Noop+:: Don't delete anything flag
226
- # +:Root+:: IMAP root path
227
- # +:Boxes+:: Comma-separated list of mailbox prefixes to cleanse
228
34
  # +:Age+:: Delete messages older than this many days ago
229
- # +:Host+:: IMAP server
230
- # +:Port+:: IMAP server port
231
- # +:SSL+:: SSL flag
232
- # +:Username+:: IMAP username
233
- # +:Password+:: IMAP password
35
+ #
36
+ # and all options from IMAPClient
234
37
 
235
38
  def initialize(options)
236
- @verbose = options[:Verbose]
237
- @noop = options[:Noop]
238
- @root = options[:Root]
239
-
240
- boxes = options[:Boxes].split(',').map { |b| Regexp.escape b }
241
- @box_re = /^#{Regexp.escape @root}\/#{Regexp.union(*boxes)}/
242
-
243
39
  @before_date = (Time.now - 86400 * options[:Age]).imapdate
244
-
245
- connect options[:Host], options[:Port], options[:SSL],
246
- options[:Username], options[:Password]
40
+ super
247
41
  end
248
42
 
249
43
  ##
250
44
  # Removes read, unflagged messages from all selected mailboxes...
251
- #
252
- # Unless :Noop was set, then it just prints out what it would do.
253
-
254
- def cleanse
255
- log "Cleansing read, unflagged messages older than #{@before_date}"
256
-
257
- message_count = 0
258
- mailboxes = find_mailboxes
259
45
 
260
- mailboxes.each do |mailbox|
261
- @imap.select mailbox
262
- log "Selected #{mailbox}"
263
-
264
- messages = old_messages_in_curr
265
- next if messages.empty?
266
-
267
- message_count += messages.length
268
-
269
- if @noop then
270
- log "Noop - nothing deleted"
271
- next
272
- end
273
-
274
- purge messages
46
+ def run
47
+ super "Cleansing read, unflagged messages older than #{@before_date}",
48
+ [:Deleted] do
49
+ @imap.expunge
50
+ log "Expunged deleted messages"
275
51
  end
276
-
277
- log "Done. Found #{message_count} messages in #{mailboxes.length} mailboxes"
278
52
  end
279
53
 
280
54
  private
281
55
 
282
- ##
283
- # Connects to IMAP server +host+ at +port+ using ssl if +ssl+ is true then
284
- # logs in as +username+ with +password+. IMAPCleanse will really only work
285
- # with PLAIN auth on SSL sockets, sorry.
286
-
287
- def connect(host, port, ssl, username, password)
288
- @imap = Net::IMAP.new host, port, ssl
289
- log "Connected to #{host}:#{port}"
290
- @imap.authenticate 'PLAIN', username, password
291
- log "Logged in as #{username}"
292
- end
293
-
294
- ##
295
- # Finds mailboxes with messages that were selected by the :Boxes option.
296
-
297
- def find_mailboxes
298
- mailboxes = @imap.list(@root, "*")
299
- mailboxes.reject! { |mailbox| mailbox.attr.include? :Noselect }
300
- mailboxes.map! { |mailbox| mailbox.name }
301
- mailboxes.reject! { |mailbox| mailbox !~ @box_re }
302
- mailboxes = mailboxes.sort_by { |m| m.downcase }
303
- log "Found #{mailboxes.length} mailboxes to cleanse:"
304
- mailboxes.each { |mailbox| log "\t#{mailbox}" } if @verbose
305
- return mailboxes
306
- end
307
-
308
- ##
309
- # Logs +message+ to STDERR if :Verbose was selected.
310
-
311
- def log(message)
312
- return unless @verbose
313
- STDERR.puts "# #{message}"
314
- end
315
-
316
56
  ##
317
57
  # Searches for read, unflagged messages older than :Age in the currently
318
58
  # selected mailbox (see Net::IMAP#select).
319
59
 
320
- def old_messages_in_curr
321
- log "Scanning for messages"
322
- messages = @imap.search [
60
+ def find_messages
61
+ search [
323
62
  'NOT', 'NEW',
324
63
  'NOT', 'FLAGGED',
325
64
  'BEFORE', @before_date
326
- ]
327
- log "Found #{messages.length} messages"
328
- return messages
329
- end
330
-
331
- ##
332
- # Marks +messages+ in the currently selected mailbox for deletion then
333
- # expunges the mailbox (see Net::IMAP#store and Net::IMAP#expunge).
334
-
335
- def purge(messages)
336
- until messages.empty? do
337
- chunk = messages.slice! 0, 500
338
- @imap.store chunk, '+FLAGS', [:Deleted]
339
- end
340
- log "Marked messages for deletion"
341
-
342
- @imap.expunge
343
- log "Expunged deleted messages"
65
+ ], 'read, unflagged messages'
344
66
  end
345
67
 
346
68
  end
@@ -0,0 +1,293 @@
1
+ require 'net/imap'
2
+ require 'imap_sasl_plain'
3
+ require 'optparse'
4
+
5
+ ##
6
+ # An IMAPClient used by IMAPFlag and IMAPCleanse.
7
+ #
8
+ # Probably not very reusable by you, but it has lots of example code.
9
+
10
+ class IMAPClient
11
+
12
+ ##
13
+ # Handles processing of +args+.
14
+
15
+ def self.process_args(args, extra_options)
16
+ opts_file = File.expand_path '~/.imap_cleanse'
17
+
18
+ unless File.stat(opts_file).mode & 077 == 0 then
19
+ $stderr.puts "WARNING! #{opts_file} is group/other readable or writable!"
20
+ $stderr.puts "WARNING! I'm not doing a thing until you fix it!"
21
+ exit 1
22
+ end
23
+
24
+ options = {}
25
+
26
+ File.readlines(opts_file).map { |l| l.chomp.split '=', 2 }.each do |k,v|
27
+ v = true if v == 'true'
28
+ v = false if v == 'false'
29
+ v = Integer(v) rescue v
30
+ options[k.intern] = v
31
+ end
32
+
33
+ options[:SSL] ||= true
34
+ options[:Username] ||= ENV['USER']
35
+ options[:Root] ||= 'mail'
36
+ options[:Noop] ||= false
37
+ options[:Verbose] ||= false
38
+
39
+ extra_options.each do |k,(v,m)|
40
+ options[k] ||= v
41
+ end
42
+
43
+ opts = OptionParser.new do |opts|
44
+ opts.banner = "Usage: #{File.basename $0} [options]"
45
+ opts.separator ''
46
+ opts.separator 'Options may also be set in the options file ~/.imap_cleanse.'
47
+ opts.separator ''
48
+ opts.separator 'Example ~/.imap_cleanse:'
49
+ opts.separator "\tHost=mail.example.com"
50
+ opts.separator "\tPassword=my password"
51
+
52
+ opts.separator ''
53
+ opts.separator 'Connection options:'
54
+
55
+ opts.on("-H", "--host HOST",
56
+ "IMAP server host",
57
+ "Default: #{options[:Host].inspect}",
58
+ "Options file name: Host") do |host|
59
+ options[:Host] = host
60
+ end
61
+
62
+ opts.on("-P", "--port PORT",
63
+ "IMAP server port",
64
+ "Default: The correct port SSL/non-SSL mode",
65
+ "Options file name: Port") do |port|
66
+ options[:Port] = port
67
+ end
68
+
69
+ opts.on("-s", "--[no-]ssl",
70
+ "Use SSL for IMAP connection",
71
+ "Default: #{options[:SSL].inspect}",
72
+ "Options file name: SSL") do |ssl|
73
+ options[:SSL] = ssl
74
+ end
75
+
76
+ opts.separator ''
77
+ opts.separator 'Login options:'
78
+
79
+ opts.on("-u", "--username USERNAME",
80
+ "IMAP username",
81
+ "Default: #{options[:Username].inspect}",
82
+ "Options file name: Username") do |username|
83
+ options[:Username] = username
84
+ end
85
+
86
+ opts.on("-p", "--password PASSWORD",
87
+ "IMAP password",
88
+ "Default: Read from ~/.imap_cleanse",
89
+ "Options file name: Password") do |password|
90
+ options[:Password] = password
91
+ end
92
+
93
+ opts.separator ''
94
+ opts.separator "#{self.class} options:"
95
+
96
+ opts.on("-r", "--root ROOT",
97
+ "Root of mailbox hierarchy",
98
+ "Default: #{options[:Root].inspect}",
99
+ "Options file name: Root") do |root|
100
+ options[:Root] = root
101
+ end
102
+
103
+ opts.on("-b", "--boxes BOXES",
104
+ "Comma-separated list of mailbox name",
105
+ "prefixes to search",
106
+ "Default: #{options[:Boxes].inspect}",
107
+ "Options file name: Boxes") do |boxes|
108
+ options[:Boxes] = boxes
109
+ end
110
+
111
+ yield opts, options
112
+
113
+ opts.on("-n", "--noop",
114
+ "Perform no destructive operations",
115
+ "Best used with the verbose option",
116
+ "Default: #{options[:Noop].inspect}",
117
+ "Options file name: Noop") do |noop|
118
+ options[:Noop] = noop
119
+ end
120
+
121
+ opts.on("-v", "--[no-]verbose",
122
+ "Be verbose",
123
+ "Default: #{options[:Verbose].inspect}",
124
+ "Options file name: Verbose") do |verbose|
125
+ options[:Verbose] = verbose
126
+ end
127
+
128
+ opts.separator ''
129
+
130
+ opts.on("-h", "--help",
131
+ "You're looking at it") do
132
+ $stderr.puts opts
133
+ exit 1
134
+ end
135
+
136
+ opts.separator ''
137
+ end
138
+
139
+ opts.parse! args
140
+
141
+ options[:Port] ||= options[:SSL] ? 993 : 143
142
+
143
+ if options[:Host].nil? or
144
+ options[:Password].nil? or
145
+ options[:Boxes].nil? or
146
+ extra_options.any? { |k,v| options[k].nil? } then
147
+ $stderr.puts opts
148
+ $stderr.puts
149
+ $stderr.puts "Host name not set" if options[:Host].nil?
150
+ $stderr.puts "Password not set" if options[:Password].nil?
151
+ $stderr.puts "Boxes option not set" if options[:Boxes].nil?
152
+ extra_options.each do |k,(v,msg)|
153
+ $stderr.puts msg if options[k].nil?
154
+ end
155
+ exit 1
156
+ end
157
+
158
+ return options
159
+ end
160
+
161
+ ##
162
+ # Sets up an IMAPClient options then runs.
163
+
164
+ def self.run(args = ARGV)
165
+ options = process_args args
166
+ client = new options
167
+ client.run
168
+ end
169
+
170
+ ##
171
+ # Creates a new IMAPClient from +options+.
172
+ #
173
+ # Options include:
174
+ # +:Verbose+:: Verbose flag
175
+ # +:Noop+:: Don't delete anything flag
176
+ # +:Root+:: IMAP root path
177
+ # +:Boxes+:: Comma-separated list of mailbox prefixes to search
178
+ # +:Host+:: IMAP server
179
+ # +:Port+:: IMAP server port
180
+ # +:SSL+:: SSL flag
181
+ # +:Username+:: IMAP username
182
+ # +:Password+:: IMAP password
183
+
184
+ def initialize(options)
185
+ @verbose = options[:Verbose]
186
+ @noop = options[:Noop]
187
+ @root = options[:Root]
188
+
189
+ boxes = options[:Boxes].split(',').map { |b| Regexp.escape b }
190
+ @box_re = /^#{Regexp.escape @root}\/#{Regexp.union(*boxes)}/
191
+
192
+ connect options[:Host], options[:Port], options[:SSL],
193
+ options[:Username], options[:Password]
194
+ end
195
+
196
+ ##
197
+ # Runs the main selecting messages from mailboxes then marking them
198
+ # with +flags+. If a block is given it is run after message marking.
199
+ #
200
+ # Unless :Noop was set, then it just prints out what it would do.
201
+ #
202
+ # Automatically called by IMAPClient::run
203
+
204
+ def run(message, flags)
205
+ log message
206
+
207
+ message_count = 0
208
+ mailboxes = find_mailboxes
209
+
210
+ mailboxes.each do |mailbox|
211
+ @imap.select mailbox
212
+ log "Selected #{mailbox}"
213
+
214
+ messages = find_messages
215
+
216
+ next if messages.empty?
217
+
218
+ message_count += messages.length
219
+
220
+ if @noop then
221
+ log "Noop - doing nothing"
222
+ next
223
+ end
224
+
225
+ mark messages, flags
226
+
227
+ yield if block_given?
228
+ end
229
+
230
+ log "Done. Found #{message_count} messages in #{mailboxes.length} mailboxes"
231
+ end
232
+
233
+ private
234
+
235
+ ##
236
+ # Connects to IMAP server +host+ at +port+ using ssl if +ssl+ is true then
237
+ # logs in as +username+ with +password+. IMAPClient will really only work
238
+ # with PLAIN auth on SSL sockets, sorry.
239
+
240
+ def connect(host, port, ssl, username, password)
241
+ @imap = Net::IMAP.new host, port, ssl
242
+ log "Connected to #{host}:#{port}"
243
+ @imap.authenticate 'PLAIN', username, password
244
+ log "Logged in as #{username}"
245
+ end
246
+
247
+ ##
248
+ # Finds mailboxes with messages that were selected by the :Boxes option.
249
+
250
+ def find_mailboxes
251
+ mailboxes = @imap.list(@root, "*")
252
+ mailboxes.reject! { |mailbox| mailbox.attr.include? :Noselect }
253
+ mailboxes.map! { |mailbox| mailbox.name }
254
+ mailboxes.reject! { |mailbox| mailbox !~ @box_re }
255
+ mailboxes = mailboxes.sort_by { |m| m.downcase }
256
+ log "Found #{mailboxes.length} mailboxes to search:"
257
+ mailboxes.each { |mailbox| log "\t#{mailbox}" } if @verbose
258
+ return mailboxes
259
+ end
260
+
261
+ ##
262
+ # Logs +message+ to $stderr if :Verbose was selected.
263
+
264
+ def log(message)
265
+ return unless @verbose
266
+ $stderr.puts "# #{message}"
267
+ end
268
+
269
+ ##
270
+ # Searches for messages matching +query+ in the selected mailbox
271
+ # (see Net::IMAP#select). Logs 'Scanning for +message+' before searching.
272
+
273
+ def search(query, message)
274
+ log "Scanning for #{message}"
275
+ messages = @imap.search query
276
+ log "Found #{messages.length} messages"
277
+ return messages
278
+ end
279
+
280
+ ##
281
+ # Marks +messages+ in the currently selected mailbox with +flags+
282
+ # (see Net::IMAP#store).
283
+
284
+ def mark(messages, flags)
285
+ until messages.empty? do
286
+ chunk = messages.slice! 0, 500
287
+ @imap.store chunk, '+FLAGS.SILENT', flags
288
+ end
289
+ log "Marked messages with flags"
290
+ end
291
+
292
+ end
293
+
data/lib/imap_flag.rb ADDED
@@ -0,0 +1,126 @@
1
+ require 'imap_client'
2
+
3
+ ##
4
+ # Automatically flag your messages, yo!
5
+ #
6
+ # aka part two of my Plan for Total Email Domination.
7
+ #
8
+ # IMAPFlag flags messages you've responded to, messages you've written and
9
+ # messages in response to messages you've written.
10
+ #
11
+ # If you unflag a message IMAPFlag is smart and doesn't re-flag it.
12
+ #
13
+ # I chose these settings because I find these messages interesting but don't
14
+ # want to manually flag them. Why should I do all the clicking when the
15
+ # computer can do it for me?
16
+
17
+ class IMAPFlag < IMAPClient
18
+
19
+ ##
20
+ # IMAP keyword for automatically flagged messages
21
+
22
+ AUTO_FLAG_KEYWORD = 'IMAPFLAG_AUTO_FLAGGED'
23
+
24
+ ##
25
+ # Message-Id query
26
+
27
+ MESSAGE_ID = 'HEADER.FIELDS (MESSAGE-ID)'
28
+
29
+ ##
30
+ # Handles processing of +args+.
31
+
32
+ def self.process_args(args)
33
+ extra_options = { :Email => [nil, 'Email address not set'] }
34
+
35
+ super args, extra_options do |opts, options|
36
+ opts.on("-e", "--email EMAIL",
37
+ "The email address you use to write email",
38
+ "Default: #{options[:Email].inspect}",
39
+ "Options file name: Email") do |email|
40
+ options[:Email] = email
41
+ end
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Creates a new IMAPFlag from +options+.
47
+ #
48
+ # Options include:
49
+ # +:Email:: Email address used for sending email
50
+ #
51
+ # and all options from IMAPClient
52
+
53
+ def initialize(options)
54
+ @email = options[:Email]
55
+ super
56
+ end
57
+
58
+ ##
59
+ # Removes read, unflagged messages from all selected mailboxes...
60
+
61
+ def run
62
+ super "Flagging messages", [:Flagged, AUTO_FLAG_KEYWORD]
63
+ end
64
+
65
+ private
66
+
67
+ ##
68
+ # Searches for messages I answered and messages I wrote.
69
+
70
+ def find_messages
71
+ return [answered_in_curr, wrote_in_curr, responses_in_curr].flatten
72
+ end
73
+
74
+ ##
75
+ # Answered messages in the selected mailbox.
76
+
77
+ def answered_in_curr
78
+ search [
79
+ 'ANSWERED',
80
+ 'NOT', 'FLAGGED',
81
+ 'NOT', 'KEYWORD', AUTO_FLAG_KEYWORD
82
+ ], 'answered messages'
83
+ end
84
+
85
+ ##
86
+ # Messages I wrote in the selected mailbox.
87
+
88
+ def wrote_in_curr
89
+ search [
90
+ 'FROM', @email,
91
+ 'NOT', 'FLAGGED',
92
+ 'NOT', 'KEYWORD', AUTO_FLAG_KEYWORD
93
+ ], 'messages I wrote'
94
+ end
95
+
96
+ ##
97
+ # Messages in response to messages I wrote in the selected mailbox.
98
+
99
+ def responses_in_curr
100
+ log "Scanning for responses to messages I wrote"
101
+ my_mail = @imap.search [ 'FROM', @email ]
102
+
103
+ return [] if my_mail.empty?
104
+
105
+ msg_ids = @imap.fetch my_mail, "BODY.PEEK[#{MESSAGE_ID}]"
106
+ msg_ids.map! do |data|
107
+ data.attr["BODY[#{MESSAGE_ID}]"].split(':', 2).last.strip
108
+ end
109
+
110
+ messages = msg_ids.map do |id|
111
+ @imap.search([
112
+ 'HEADER', 'In-Reply-To', id,
113
+ 'NOT', 'FLAGGED',
114
+ 'NOT', 'KEYWORD', AUTO_FLAG_KEYWORD
115
+ ])
116
+ end
117
+
118
+ messages.flatten!
119
+
120
+ log "Found #{messages.length} messages"
121
+
122
+ return messages
123
+ end
124
+
125
+ end
126
+
@@ -0,0 +1,57 @@
1
+ require 'time'
2
+ require 'net/imap'
3
+
4
+ class Time
5
+
6
+ ##
7
+ # Formats this Time as an IMAP-style date.
8
+ #
9
+ # RFC 2060 doesn't specify the format of its times. Unfortunately it is
10
+ # almost but not quite RFC 822 dates.
11
+ #--
12
+ # Go Mr. Leatherpants!
13
+
14
+ def imapdate
15
+ strftime '%d-%b-%Y %H:%M %Z'
16
+ end
17
+ end
18
+
19
+ ##
20
+ # RFC 2595 PLAIN Authenticator for Net::IMAP. Only for use with SSL (but not
21
+ # enforced).
22
+
23
+ class Net::IMAP::PlainAuthenticator
24
+
25
+ ##
26
+ # From RFC 2595 Section 6. PLAIN SASL Authentication
27
+ #
28
+ # The mechanism consists of a single message from the client to the
29
+ # server. The client sends the authorization identity (identity to
30
+ # login as), followed by a US-ASCII NUL character, followed by the
31
+ # authentication identity (identity whose password will be used),
32
+ # followed by a US-ASCII NUL character, followed by the clear-text
33
+ # password. The client may leave the authorization identity empty to
34
+ # indicate that it is the same as the authentication identity.
35
+
36
+ def process(data)
37
+ return [@user, @user, @password].join("\0")
38
+ end
39
+
40
+ private
41
+
42
+ ##
43
+ # Creates a new PlainAuthenticator that will authenticate with +user+ and
44
+ # +password+.
45
+
46
+ def initialize(user, password)
47
+ @user = user
48
+ @password = password
49
+ end
50
+
51
+ end
52
+
53
+ if defined? OpenSSL then
54
+ Net::IMAP.add_authenticator 'PLAIN', Net::IMAP::PlainAuthenticator
55
+ end
56
+
57
+
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.11.6
2
+ rubygems_version: 0.8.11.15
3
3
  specification_version: 1
4
4
  name: IMAPCleanse
5
5
  version: !ruby/object:Gem::Version
6
- version: 1.0.0
7
- date: 2006-03-28 00:00:00 -08:00
8
- summary: Cleanses your IMAP mailboxes of oldness
6
+ version: 1.1.0
7
+ date: 2006-05-05 00:00:00 -07:00
8
+ summary: Removes mailbox oldness, finds mailbox interestingness!
9
9
  require_paths:
10
10
  - lib
11
11
  email: drbrain@segment7.net
12
12
  homepage:
13
13
  rubyforge_project:
14
- description: IMAPCleanse removes old, read, unflagged messages from your IMAP mailboxes so you don't have to!
14
+ description: IMAPCleanse removes old, read, unflagged messages from your IMAP mailboxes so you don't have to! IMAPFlag flags messages I find interesting so I don't have to!
15
15
  autorequire:
16
16
  default_executable:
17
17
  bindir: bin
@@ -29,22 +29,16 @@ post_install_message:
29
29
  authors:
30
30
  - Eric Hodel
31
31
  files:
32
- - .DS_Store
33
- - CVS/Entries
34
- - CVS/Repository
35
- - CVS/Root
36
32
  - LICENSE
37
33
  - Manifest.txt
38
34
  - README
39
35
  - Rakefile
40
- - bin/CVS/Entries
41
- - bin/CVS/Repository
42
- - bin/CVS/Root
43
36
  - bin/imap_cleanse
44
- - lib/CVS/Entries
45
- - lib/CVS/Repository
46
- - lib/CVS/Root
37
+ - bin/imap_flag
47
38
  - lib/imap_cleanse.rb
39
+ - lib/imap_client.rb
40
+ - lib/imap_flag.rb
41
+ - lib/imap_sasl_plain.rb
48
42
  test_files: []
49
43
 
50
44
  rdoc_options: []
@@ -53,6 +47,7 @@ extra_rdoc_files: []
53
47
 
54
48
  executables:
55
49
  - imap_cleanse
50
+ - imap_flag
56
51
  extensions: []
57
52
 
58
53
  requirements: []
data/.DS_Store DELETED
Binary file
data/CVS/Entries DELETED
@@ -1,6 +0,0 @@
1
- D/bin////
2
- D/lib////
3
- /Manifest.txt/1.1/Wed Mar 29 02:21:53 2006//
4
- /README/1.1/Wed Mar 29 01:49:02 2006//
5
- /Rakefile/1.1/Wed Mar 29 02:23:42 2006//
6
- /LICENSE/0/dummy timestamp//
data/CVS/Repository DELETED
@@ -1 +0,0 @@
1
- ruby/imap_cleanse
data/CVS/Root DELETED
@@ -1 +0,0 @@
1
- :ext:drbrain@ziz:/home/cvs/
data/bin/CVS/Entries DELETED
@@ -1,2 +0,0 @@
1
- /imap_cleanse/1.1/Wed Mar 29 02:25:22 2006//
2
- D
data/bin/CVS/Repository DELETED
@@ -1 +0,0 @@
1
- ruby/imap_cleanse/bin
data/bin/CVS/Root DELETED
@@ -1 +0,0 @@
1
- :ext:drbrain@ziz:/home/cvs/
data/lib/CVS/Entries DELETED
@@ -1,2 +0,0 @@
1
- /imap_cleanse.rb/1.1/Wed Mar 29 00:45:15 2006//
2
- D
data/lib/CVS/Repository DELETED
@@ -1 +0,0 @@
1
- ruby/imap_cleanse/lib
data/lib/CVS/Root DELETED
@@ -1 +0,0 @@
1
- :ext:drbrain@ziz:/home/cvs/