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.
@@ -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
- "Don't display messages") do |list|
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 = options[:Add]
51
- @delete = options[:Delete]
50
+ @add = options[:Add]
51
+ @delete = options[:Delete]
52
52
  @keywords = options[:Keywords]
53
- @not = options[:Not] ? 'NOT' : nil
54
- @list = options[: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 options[:Host], options[:Port], options[:SSL],
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 =~ /^Subject: (.*)/i
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
+
@@ -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
+