IMAPCleanse 1.0.0 → 1.1.0

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