imap_processor 1.1.1 → 1.7
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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +2 -3
- data/.autotest +3 -18
- data/History.rdoc +103 -0
- data/Manifest.txt +18 -2
- data/{README.txt → README.rdoc} +35 -5
- data/Rakefile +8 -5
- data/bin/imap_archive +5 -0
- data/bin/imap_cleanse +5 -0
- data/bin/imap_flag +5 -0
- data/bin/imap_idle +6 -0
- data/bin/imap_learn +5 -0
- data/bin/imap_mkdir +5 -0
- data/lib/imap_processor.rb +304 -154
- data/lib/imap_processor/archive.rb +128 -0
- data/lib/imap_processor/cleanse.rb +67 -0
- data/lib/imap_processor/client.rb +145 -0
- data/lib/imap_processor/flag.rb +121 -0
- data/lib/imap_processor/idle.rb +74 -0
- data/lib/imap_processor/keywords.rb +9 -14
- data/lib/imap_processor/learn.rb +231 -0
- data/lib/imap_processor/mkdir.rb +25 -0
- data/lib/imap_sasl_plain.rb +1 -26
- data/lib/net/imap/date.rb +24 -0
- data/lib/net/imap/idle.rb +48 -0
- data/test/test_imap_processor.rb +185 -0
- metadata +120 -71
- metadata.gz.sig +0 -0
- data/History.txt +0 -22
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'imap_processor'
|
2
|
+
require 'net/imap/idle'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Example class that supports IDLE on a mailbox and lists messages added or
|
6
|
+
# expunged.
|
7
|
+
|
8
|
+
class IMAPProcessor::IDLE < IMAPProcessor
|
9
|
+
|
10
|
+
def self.process_args(args)
|
11
|
+
super __FILE__, args do |opts, options|
|
12
|
+
opts.banner << <<-EOF
|
13
|
+
imap_idle lists messages added or expunged from a mailbox
|
14
|
+
EOF
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(options)
|
19
|
+
super
|
20
|
+
|
21
|
+
raise IMAPProcessor::Error, 'only one mailbox is supported' if
|
22
|
+
@boxes.length > 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
mailbox = @boxes.first
|
27
|
+
|
28
|
+
connect do |connection|
|
29
|
+
raise IMAPProcessor::Error, 'IDLE not supported on this server' unless
|
30
|
+
connection.idle?
|
31
|
+
|
32
|
+
imap = connection.imap
|
33
|
+
|
34
|
+
imap.select mailbox
|
35
|
+
exists = imap.responses['EXISTS'].first
|
36
|
+
|
37
|
+
log "Starting IDLE"
|
38
|
+
|
39
|
+
imap.idle do |response|
|
40
|
+
next unless Net::IMAP::UntaggedResponse === response
|
41
|
+
|
42
|
+
case response.name
|
43
|
+
when 'EXPUNGE' then
|
44
|
+
puts "Expunged message #{response.data}"
|
45
|
+
when 'EXISTS' then
|
46
|
+
latest_uid = response.data
|
47
|
+
new = latest_uid - exists
|
48
|
+
puts "#{new} messages added"
|
49
|
+
|
50
|
+
show_messages_in mailbox, ((exists + 1)..latest_uid)
|
51
|
+
|
52
|
+
exists = response.data
|
53
|
+
when 'RECENT' then
|
54
|
+
puts "#{response.data} recent messages"
|
55
|
+
when 'OK' then # ending IDLE
|
56
|
+
else
|
57
|
+
log "Unhandled untagged response: #{response.name}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def show_messages_in(mailbox, uids)
|
64
|
+
connect do |connection|
|
65
|
+
imap = connection.imap
|
66
|
+
|
67
|
+
imap.select mailbox
|
68
|
+
|
69
|
+
show_messages uids
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
@@ -33,7 +33,7 @@ previously set keywords.
|
|
33
33
|
end
|
34
34
|
|
35
35
|
opts.on( "--[no-]list",
|
36
|
-
"
|
36
|
+
"Display messages") do |list|
|
37
37
|
options[:List] = list
|
38
38
|
end
|
39
39
|
|
@@ -47,11 +47,11 @@ previously set keywords.
|
|
47
47
|
def initialize(options)
|
48
48
|
super
|
49
49
|
|
50
|
-
@add
|
51
|
-
@delete
|
50
|
+
@add = options[:Add]
|
51
|
+
@delete = options[:Delete]
|
52
52
|
@keywords = options[:Keywords]
|
53
|
-
@not
|
54
|
-
@list
|
53
|
+
@not = options[:Not] ? 'NOT' : nil
|
54
|
+
@list = options[:List]
|
55
55
|
|
56
56
|
if @add and @delete then
|
57
57
|
raise OptionParser::InvalidOption, "--add and --delete are exclusive"
|
@@ -60,8 +60,7 @@ previously set keywords.
|
|
60
60
|
"--add and --delete require --keywords"
|
61
61
|
end
|
62
62
|
|
63
|
-
connection = connect
|
64
|
-
options[:Username], options[:Password], options[:Auth]
|
63
|
+
connection = connect
|
65
64
|
|
66
65
|
@imap = connection.imap
|
67
66
|
end
|
@@ -134,18 +133,14 @@ previously set keywords.
|
|
134
133
|
return unless @list
|
135
134
|
|
136
135
|
responses = @imap.fetch uids, [
|
137
|
-
'BODY.PEEK[HEADER]',
|
136
|
+
Net::IMAP::RawData.new('BODY.PEEK[HEADER.FIELDS (SUBJECT MESSAGE-ID)]'),
|
138
137
|
'FLAGS'
|
139
138
|
]
|
140
139
|
|
141
140
|
responses.each do |res|
|
142
|
-
header = res.attr['BODY[HEADER]']
|
141
|
+
header = res.attr['BODY[HEADER.FIELDS (SUBJECT MESSAGE-ID)]']
|
143
142
|
|
144
|
-
header
|
145
|
-
puts "Subject: #{$1}"
|
146
|
-
|
147
|
-
header =~ /^Message-Id: (.*)/i
|
148
|
-
puts "Message-Id: #{$1}"
|
143
|
+
puts header.chomp
|
149
144
|
|
150
145
|
flags = res.attr['FLAGS'].map { |flag| flag.inspect }.join ', '
|
151
146
|
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'imap_processor/client'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'rbayes'
|
8
|
+
rescue LoadError
|
9
|
+
# ignoring
|
10
|
+
class RBayes
|
11
|
+
def initialize *args
|
12
|
+
# nothing to do
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# IMAPLearn flags messages per-folder based on what you've flagged before.
|
19
|
+
#
|
20
|
+
# aka part three of my Plan for Total Email Domination.
|
21
|
+
|
22
|
+
class IMAPProcessor::Learn < IMAPProcessor::Client
|
23
|
+
|
24
|
+
##
|
25
|
+
# IMAP keyword for learned messages
|
26
|
+
|
27
|
+
LEARN_KEYWORD = 'IMAPLEARN_FLAGGED'
|
28
|
+
|
29
|
+
##
|
30
|
+
# IMAP keyword for tasty messages
|
31
|
+
|
32
|
+
TASTY_KEYWORD = LEARN_KEYWORD + '_TASTY'
|
33
|
+
|
34
|
+
##
|
35
|
+
# IMAP keyword for bland messages
|
36
|
+
|
37
|
+
BLAND_KEYWORD = LEARN_KEYWORD + '_BLAND'
|
38
|
+
|
39
|
+
##
|
40
|
+
# Handles processing of +args+.
|
41
|
+
|
42
|
+
def self.process_args(args)
|
43
|
+
@@options[:Threshold] = [0.85, 'Tastiness threshold not set']
|
44
|
+
|
45
|
+
super __FILE__, args, {} do |opts, options|
|
46
|
+
opts.on("-t", "--threshold THRESHOLD",
|
47
|
+
"Flag messages more tasty than THRESHOLD",
|
48
|
+
"Default: #{options[:Threshold].inspect}",
|
49
|
+
"Options file name: Threshold", Float) do |threshold|
|
50
|
+
options[:Threshold] = threshold
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Creates a new IMAPLearn from +options+.
|
57
|
+
#
|
58
|
+
# Options include:
|
59
|
+
# +:Threshold+:: Tastiness threshold for flagging
|
60
|
+
#
|
61
|
+
# and all options from IMAPClient
|
62
|
+
|
63
|
+
def initialize(options)
|
64
|
+
super
|
65
|
+
|
66
|
+
@db_root = File.join '~', '.imap_learn',
|
67
|
+
"#{options[:User]}@#{options[:Host]}:#{options[:Port]}"
|
68
|
+
@db_root = File.expand_path @db_root
|
69
|
+
|
70
|
+
@threshold = options[:Threshold]
|
71
|
+
|
72
|
+
@classifiers = Hash.new do |h,k|
|
73
|
+
filter_db = File.join @db_root, "#{k}.db"
|
74
|
+
FileUtils.mkdir_p File.dirname(filter_db)
|
75
|
+
h[k] = RBayes.new filter_db
|
76
|
+
end
|
77
|
+
|
78
|
+
@unlearned_flagged = []
|
79
|
+
@tasty_unflagged = []
|
80
|
+
@bland_flagged = []
|
81
|
+
@tasty_unlearned = []
|
82
|
+
@bland_unlearned = []
|
83
|
+
|
84
|
+
@noop = false
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Flags tasty messages from all selected mailboxes.
|
89
|
+
|
90
|
+
def run
|
91
|
+
log "Flagging tasty messages"
|
92
|
+
|
93
|
+
message_count = 0
|
94
|
+
mailboxes = find_mailboxes
|
95
|
+
|
96
|
+
mailboxes.each do |mailbox|
|
97
|
+
@mailbox = mailbox
|
98
|
+
@imap.select @mailbox
|
99
|
+
log "Selected #{@mailbox}"
|
100
|
+
|
101
|
+
message_count += process_unlearned_flagged
|
102
|
+
message_count += process_tasty_unflagged
|
103
|
+
message_count += process_bland_flagged
|
104
|
+
message_count += process_unlearned
|
105
|
+
end
|
106
|
+
|
107
|
+
log "Done. Found #{message_count} messages in #{mailboxes.length} mailboxes"
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
##
|
113
|
+
# Returns an Array of tasty message sequence numbers.
|
114
|
+
|
115
|
+
def unlearned_flagged_in_curr
|
116
|
+
log "Finding unlearned, flagged messages"
|
117
|
+
|
118
|
+
@unlearned_flagged = @imap.search [
|
119
|
+
'FLAGGED',
|
120
|
+
'NOT', 'KEYWORD', LEARN_KEYWORD
|
121
|
+
]
|
122
|
+
|
123
|
+
update_db @unlearned_flagged, :add_tasty
|
124
|
+
|
125
|
+
@unlearned_flagged.length
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Returns an Array of message sequence numbers that should be marked as
|
130
|
+
# bland.
|
131
|
+
|
132
|
+
def tasty_unflagged_in_curr
|
133
|
+
log "Finding messages re-marked bland"
|
134
|
+
|
135
|
+
@bland_flagged = @imap.search [
|
136
|
+
'NOT', 'FLAGGED',
|
137
|
+
'KEYWORD', TASTY_KEYWORD
|
138
|
+
]
|
139
|
+
|
140
|
+
update_db @tasty_unflagged, :remove_tasty, :add_bland
|
141
|
+
|
142
|
+
@bland_flagged.length
|
143
|
+
end
|
144
|
+
|
145
|
+
##
|
146
|
+
# Returns an Array of tasty message sequence numbers that should be marked
|
147
|
+
# as tasty.
|
148
|
+
|
149
|
+
def bland_flagged_in_curr
|
150
|
+
log "Finding messages re-marked tasty"
|
151
|
+
@bland_flagged = @imap.search [
|
152
|
+
'FLAGGED',
|
153
|
+
'KEYWORD', BLAND_KEYWORD
|
154
|
+
]
|
155
|
+
|
156
|
+
update_db @bland_flagged, :remove_bland, :add_tasty
|
157
|
+
|
158
|
+
@bland_flagged.length
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Returns two Arrays, one of tasty message sequence numbers and one of bland
|
163
|
+
# message sequence numbers.
|
164
|
+
|
165
|
+
def unlearned_in_curr
|
166
|
+
log "Learning new, unmarked messages"
|
167
|
+
unlearned = @imap.search [
|
168
|
+
'NOT', 'KEYWORD', LEARN_KEYWORD
|
169
|
+
]
|
170
|
+
|
171
|
+
tasty = []
|
172
|
+
bland = []
|
173
|
+
|
174
|
+
chunk unlearned do |messages|
|
175
|
+
bodies = @imap.fetch messages, 'RFC822'
|
176
|
+
bodies.each do |body|
|
177
|
+
text = body.attr['RFC822']
|
178
|
+
bucket = classify(text) ? tasty : bland
|
179
|
+
bucket << body.seqno
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
update_db tasty, :add_tasty
|
184
|
+
update_db bland, :add_bland
|
185
|
+
|
186
|
+
tasty.length + bland.length
|
187
|
+
end
|
188
|
+
|
189
|
+
def chunk(messages, size = 20)
|
190
|
+
messages = messages.dup
|
191
|
+
|
192
|
+
until messages.empty? do
|
193
|
+
chunk = messages.slice! 0, size
|
194
|
+
yield chunk
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Returns true if +text+ is "tasty"
|
200
|
+
|
201
|
+
def classify(text)
|
202
|
+
rating = @classifiers[@mailbox].rate text
|
203
|
+
rating > @threshold
|
204
|
+
end
|
205
|
+
|
206
|
+
def update_db(messages, *actions)
|
207
|
+
chunk messages do |chunk|
|
208
|
+
bodies = @imap.fetch chunk, 'RFC822'
|
209
|
+
bodies.each do |body|
|
210
|
+
text = body.attr['RFC822']
|
211
|
+
actions.each do |action|
|
212
|
+
@classifiers[@mailbox].update_db_with text, action
|
213
|
+
case action
|
214
|
+
when :add_bland then
|
215
|
+
@imap.store body.seqno, '+FLAG.SILENT',
|
216
|
+
[LEARN_KEYWORD, BLAND_KEYWORD]
|
217
|
+
when :add_tasty then
|
218
|
+
@imap.store body.seqno, '+FLAG.SILENT',
|
219
|
+
[:Flagged, LEARN_KEYWORD, TASTY_KEYWORD]
|
220
|
+
when :remove_bland then
|
221
|
+
@imap.store body.seqno, '-FLAG.SILENT', [BLAND_KEYWORD]
|
222
|
+
when :remove_tasty then
|
223
|
+
@imap.store body.seqno, '-FLAG.SILENT', [TASTY_KEYWORD]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'imap_processor'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Creates folders in IMAP.
|
5
|
+
|
6
|
+
class IMAPProcessor::Mkdir < IMAPProcessor
|
7
|
+
attr_reader :sep
|
8
|
+
|
9
|
+
def self.process_args(args)
|
10
|
+
super __FILE__, args
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(options)
|
14
|
+
super
|
15
|
+
|
16
|
+
@imap = connect.imap
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
ARGV.each do |mailbox|
|
21
|
+
create_mailbox mailbox
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
data/lib/imap_sasl_plain.rb
CHANGED
@@ -1,29 +1,5 @@
|
|
1
|
-
require 'time'
|
2
1
|
require 'net/imap'
|
3
2
|
|
4
|
-
class Time
|
5
|
-
|
6
|
-
##
|
7
|
-
# Formats this Time as an IMAP-style date.
|
8
|
-
|
9
|
-
def imapdate
|
10
|
-
strftime '%d-%b-%Y'
|
11
|
-
end
|
12
|
-
|
13
|
-
##
|
14
|
-
# Formats this Time as an IMAP-style datetime.
|
15
|
-
#
|
16
|
-
# RFC 2060 doesn't specify the format of its times. Unfortunately it is
|
17
|
-
# almost but not quite RFC 822 compliant.
|
18
|
-
#--
|
19
|
-
# Go Mr. Leatherpants!
|
20
|
-
|
21
|
-
def imapdatetime
|
22
|
-
strftime '%d-%b-%Y %H:%M %Z'
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
26
|
-
|
27
3
|
##
|
28
4
|
# RFC 2595 PLAIN Authenticator for Net::IMAP. Only for use with SSL (but not
|
29
5
|
# enforced).
|
@@ -56,10 +32,9 @@ class Net::IMAP::PlainAuthenticator
|
|
56
32
|
@password = password
|
57
33
|
end
|
58
34
|
|
59
|
-
end
|
35
|
+
end unless defined? Net::IMAP::PlainAuthenticator
|
60
36
|
|
61
37
|
if defined? OpenSSL then
|
62
38
|
Net::IMAP.add_authenticator 'PLAIN', Net::IMAP::PlainAuthenticator
|
63
39
|
end
|
64
40
|
|
65
|
-
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
class Time
|
3
|
+
|
4
|
+
##
|
5
|
+
# Formats this Time as an IMAP-style date.
|
6
|
+
|
7
|
+
def imapdate
|
8
|
+
strftime '%d-%b-%Y'
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# Formats this Time as an IMAP-style datetime.
|
13
|
+
#
|
14
|
+
# RFC 2060 doesn't specify the format of its times. Unfortunately it is
|
15
|
+
# almost but not quite RFC 822 compliant.
|
16
|
+
#--
|
17
|
+
# Go Mr. Leatherpants!
|
18
|
+
|
19
|
+
def imapdatetime
|
20
|
+
strftime '%d-%b-%Y %H:%M %Z'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|