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 +4 -10
- data/README +43 -9
- data/Rakefile +17 -5
- data/bin/imap_flag +6 -0
- data/lib/imap_cleanse.rb +20 -298
- data/lib/imap_client.rb +293 -0
- data/lib/imap_flag.rb +126 -0
- data/lib/imap_sasl_plain.rb +57 -0
- metadata +10 -15
- data/.DS_Store +0 -0
- data/CVS/Entries +0 -6
- data/CVS/Repository +0 -1
- data/CVS/Root +0 -1
- data/bin/CVS/Entries +0 -2
- data/bin/CVS/Repository +0 -1
- data/bin/CVS/Root +0 -1
- data/lib/CVS/Entries +0 -2
- data/lib/CVS/Repository +0 -1
- data/lib/CVS/Root +0 -1
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
|
-
|
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
|
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
|
23
|
-
a message around for forever I'll just flag it and
|
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
|
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
|
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
|
43
|
-
the --verbose flag you can see how many messages
|
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
|
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
|
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.
|
12
|
-
s.summary = '
|
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 =
|
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
|
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
data/lib/imap_cleanse.rb
CHANGED
@@ -1,346 +1,68 @@
|
|
1
|
-
require '
|
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
|
-
|
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
|
-
#
|
230
|
-
#
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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
|
321
|
-
|
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
|
data/lib/imap_client.rb
ADDED
@@ -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.
|
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.
|
7
|
-
date: 2006-
|
8
|
-
summary:
|
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
|
-
-
|
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
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
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
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/
|