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 +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/
|