imap_processor 1.1.1 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 632f5afa32e055708809e6817e2532f5a5f880b48d9bb808102e63e2bad6cff7
4
+ data.tar.gz: 145bf8485b69a4629b32b12748edc780899db542e6a180db88a61ee6fc93ba56
5
+ SHA512:
6
+ metadata.gz: e1c5b332b9d8e3dbb9f105d888cb796a1e901633f40147a241ed2ac0ec7f63664f8a9b78a0eed149d8653eace16de68190d2737462294192ecaaa0bf331d1746
7
+ data.tar.gz: 5561900484e14b7fe02508402af345a07a8999c56b2fdf05dd91989698b4b5a87bdd9f09acebf84232e7505b3a9ce8e57e8e9fc42041567555cf889592ec055c
Binary file
data.tar.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- AW���Bg-G��zF��H��X�d�Z�lF�:�Q?��`�C���G-��"���:���4�Ćh����/1��Ǎ
2
- `fx��L��>1P�=Pv��8�᧹����H���
3
- ��в��o��G���\i�֬�)a�(�5c͌S�@"�Ŭ�=�͞nM��H ��|���.�poa�et'�ruCw��8ƌ�?Zz�C( ׾�iA�0�`?��"L�cp�q��ŋ��3�-�@!�YЌժU�Dr-"&
1
+ C��>Ƙo�3����0��:��Oɩ��KiXZ��$���foL(�=��=��W-
2
+ 5m���݆+cV�1���㹶-\�GLD��W���U�����0�&1-�͔C���*��0��n~��C��8�;)���8���x���.ϔ1�/�e ����1��AN$�F�ͭDK9�6�� V5��g�
data/.autotest CHANGED
@@ -2,22 +2,7 @@
2
2
 
3
3
  require 'autotest/restart'
4
4
 
5
- # Autotest.add_hook :initialize do |at|
6
- # at.extra_files << "../some/external/dependency.rb"
7
- #
8
- # at.libs << ":../some/external"
9
- #
10
- # at.add_exception 'vendor'
11
- #
12
- # at.add_mapping(/dependency.rb/) do |f, _|
13
- # at.files_matching(/test_.*rb$/)
14
- # end
15
- #
16
- # %w(TestA TestB).each do |klass|
17
- # at.extra_class_map[klass] = "test/test_misc.rb"
18
- # end
19
- # end
5
+ Autotest.add_hook :initialize do |at|
6
+ at.testlib = 'minitest/autorun'
7
+ end
20
8
 
21
- # Autotest.add_hook :run_command do |at|
22
- # system "rake build"
23
- # end
@@ -0,0 +1,103 @@
1
+ === 1.7 / 2020-06-04
2
+
3
+ * 4 minor enhancements:
4
+
5
+ * Added #noop? to improve readability.
6
+ * Documented how to use this with gmail.
7
+ * Improved imap_archive output by distinguishing hosts.
8
+ * Improved show_messages output to be cleaner and have better info (from/to).
9
+
10
+ * 2 bug fixes:
11
+
12
+ * Fixed --noop by covering all imap calls that modify w/ checks.
13
+ * Fixed option processing with multiple accounts by dup'ing args to OptionParser.
14
+
15
+ === 1.6 / 2014-10-17
16
+
17
+ * 1 minor enhancement:
18
+
19
+ * Add XOAUTH2 authentication type. (mattbeedle)
20
+
21
+ === 1.5 / 2014-08-06
22
+
23
+ * 3 major enhancements:
24
+
25
+ * IMAPProcessor#process_args now returns an array of option hashes.
26
+ * IMAPProcessor.run now enumerates the array returned from process_args.
27
+ * You can now specify multiple host configs w/ an array of hashes in your config files.
28
+
29
+ * 4 minor enhancements:
30
+
31
+ * Added --merge to imap_archive.
32
+ * Added --noop/-n to manually disable destructive actions. (needs to propagate down).
33
+ * Added imap_cleanse, imap_flag, imap_learn; migrated from IMAPCleanse.
34
+ * Added support for LOGIN. (bleything)
35
+
36
+ * 7 bug fixes:
37
+
38
+ * Fixed initializers in flag and cleanse.
39
+ * Fixed odd bug w/ running on empty folders. Never saw that before. odd...
40
+ * Handle unparsable date entries. Stupid spammers...
41
+ * Now calculating latest month when not splitting directly from the date
42
+ * Removed 1.9/2.0 warnings.
43
+ * Removed dead rubyforge setting in Rakefile
44
+ * Split was still defaulting to true.
45
+
46
+ === 1.4 / 2011-01-10
47
+
48
+ * 6 minor enhancements:
49
+
50
+ * Added explicit help option (-h didn't work)
51
+ * Added folder separator support (osx server uses '.' not '/')
52
+ * Added imap_mkdir command
53
+ * Added opts_file_name class var so subclass option processing can refer to file
54
+ * Extended imap_archive to archive multiple months per box, as necessary. Allowing easy archiving of big mailboxes
55
+ * Handles server-provided CAPABILITY to avoid an extra round-trip
56
+
57
+ * 1 bug fix:
58
+
59
+ * Fixed doco.
60
+
61
+ === 1.3 / 2009-08-04
62
+
63
+ * 1 major enhancement
64
+ * IMAP IDLE support now matches ruby trunk's support. See Net::IMAP#idle
65
+ and Net::IMAP#idle_done
66
+
67
+ === 1.2 / 2009-06-02
68
+
69
+ * 2 major enhancements
70
+ * imap_archive which archives old mail to dated mailboxes
71
+ * imap_idle which lists messages that were added or expunged from a mailbox
72
+
73
+ * 4 minor enhancements
74
+ * Added IMAPProcessor#create_mailbox
75
+ * Added IMAPProcessor#delete_messages
76
+ * Added IMAPProcessor#move_messages
77
+ * Disabled verification of SSL certs for 1.9
78
+
79
+ * 1 bug fix
80
+ * Fixed options file names, they should be Symbol keys
81
+
82
+ === 1.1.1 / 2009-05-19
83
+
84
+ * 1 bug fix
85
+ * Got the skip test backwards
86
+
87
+ === 1.1 / 2009-05-18
88
+
89
+ * 1 minor enhancement
90
+ * IMAPProcessor#each_message allows messages to be omitted from the returned
91
+ uid list (skipped)
92
+
93
+ === 1.0.1 / 2009-05-15
94
+
95
+ * 2 bug fix
96
+ * Show correct name of options file for --password help
97
+ * Fix --quiet
98
+
99
+ === 1.0.0 / 2009-05-12
100
+
101
+ * 1 major enhancement
102
+ * Birthday!
103
+
@@ -1,9 +1,25 @@
1
1
  .autotest
2
- History.txt
2
+ History.rdoc
3
3
  Manifest.txt
4
- README.txt
4
+ README.rdoc
5
5
  Rakefile
6
+ bin/imap_archive
7
+ bin/imap_cleanse
8
+ bin/imap_flag
9
+ bin/imap_idle
6
10
  bin/imap_keywords
11
+ bin/imap_learn
12
+ bin/imap_mkdir
7
13
  lib/imap_processor.rb
14
+ lib/imap_processor/archive.rb
15
+ lib/imap_processor/cleanse.rb
16
+ lib/imap_processor/client.rb
17
+ lib/imap_processor/flag.rb
18
+ lib/imap_processor/idle.rb
8
19
  lib/imap_processor/keywords.rb
20
+ lib/imap_processor/learn.rb
21
+ lib/imap_processor/mkdir.rb
9
22
  lib/imap_sasl_plain.rb
23
+ lib/net/imap/date.rb
24
+ lib/net/imap/idle.rb
25
+ test/test_imap_processor.rb
@@ -1,6 +1,7 @@
1
1
  = imap_processor
2
2
 
3
- * http://seattlerb.rubyforge.org/imap_processor
3
+ home :: https://github.com/seattlerb/imap_processor
4
+ rdoc :: http://docs.seattlerb.org/imap_processor
4
5
 
5
6
  == DESCRIPTION:
6
7
 
@@ -8,8 +9,16 @@ IMAPProcessor is a client for processing messages on an IMAP server. It
8
9
  provides some basic mechanisms for connecting to an IMAP server, determining
9
10
  capabilities and handling messages.
10
11
 
11
- IMAPProcessor ships with the imap_keywords executable which can query an IMAP
12
- server for keywords set on messages in mailboxes.
12
+ IMAPProcessor ships with several executables which can query and
13
+ manipulate IMAP mailboxes in several different ways:
14
+
15
+ imap_archive :: Archives old messages to a new dated mailbox.
16
+ imap_cleanse :: Delete messages older than a certain age in specified mailboxes.
17
+ imap_flag :: Flag messages to/from certain people.
18
+ imap_idle :: Shows new messages in a mailbox.
19
+ imap_keywords :: Queries an IMAP server for keywords set on messages
20
+ imap_learn :: Flags messages based on what you've flagged before.
21
+ imap_mkdir :: Ensures that certain mailboxes exist.
13
22
 
14
23
  == FEATURES/PROBLEMS:
15
24
 
@@ -19,7 +28,28 @@ server for keywords set on messages in mailboxes.
19
28
 
20
29
  == SYNOPSIS:
21
30
 
22
- See IMAPProcessor and IMAPProcessor::Keywords for details
31
+ Run any command with --help for details.
32
+
33
+ == Google Mail:
34
+
35
+ This is kinda painful. You need to have Two Factor Authentication
36
+ enabled with google, then you need to create an app specific password
37
+ as described here: https://support.google.com/accounts/answer/185833
38
+
39
+ Then, your config needs to be set up like this:
40
+
41
+ - :Host: imap.googlemail.com
42
+ :SSL: true
43
+ :Auth: PLAIN
44
+ :Username: your.address@gmail.com
45
+ :Password: app-specific-password
46
+
47
+ Specifically, you need to set auth to PLAIN, and your password needs
48
+ to your app specific password. Run with --debug to help figure
49
+ problems out.
50
+
51
+ Google is threatening to turn this off at some point and require
52
+ oauth... at which point... I have no idea. I give up I guess.
23
53
 
24
54
  == REQUIREMENTS:
25
55
 
@@ -33,7 +63,7 @@ See IMAPProcessor and IMAPProcessor::Keywords for details
33
63
 
34
64
  (The MIT License)
35
65
 
36
- Copyright (c) 2009 Eric Hodel
66
+ Copyright (c) Eric Hodel, Ryan Davis, Seattle.rb
37
67
 
38
68
  Permission is hereby granted, free of charge, to any person obtaining
39
69
  a copy of this software and associated documentation files (the
data/Rakefile CHANGED
@@ -2,12 +2,15 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'hoe'
5
- $:.unshift 'lib'
6
- require 'imap_processor'
7
5
 
8
- Hoe.new 'imap_processor', IMAPProcessor::VERSION do |ip|
9
- ip.rubyforge_name = 'seattlerb'
10
- ip.developer('Eric Hodel', 'drbrain@segment7.net')
6
+ Hoe.plugin :seattlerb
7
+ Hoe.plugin :rdoc
8
+
9
+ Hoe.spec 'imap_processor' do
10
+ developer 'Ryan Davis', 'ryand-ruby@zenspider.com'
11
+ developer 'Eric Hodel', 'drbrain@segment7.net'
12
+
13
+ license "MIT"
11
14
  end
12
15
 
13
16
  # vim: syntax=Ruby
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'imap_processor/archive'
4
+
5
+ IMAPProcessor::Archive.run ARGV
@@ -0,0 +1,5 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'imap_processor/cleanse'
4
+
5
+ IMAPProcessor::Cleanse.run ARGV
@@ -0,0 +1,5 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'imap_processor/flag'
4
+
5
+ IMAPProcessor::Flag.run
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'imap_processor/idle'
4
+
5
+ IMAPProcessor::IDLE.run ARGV
6
+
@@ -0,0 +1,5 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'imap_processor/learn'
4
+
5
+ IMAPProcessor::Learn.run
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'imap_processor/mkdir'
4
+
5
+ IMAPProcessor::Mkdir.run ARGV
@@ -1,7 +1,9 @@
1
1
  require 'rubygems'
2
2
  require 'optparse'
3
3
  require 'net/imap'
4
+ require 'net/imap/date'
4
5
  require 'imap_sasl_plain'
6
+ require 'yaml'
5
7
 
6
8
  ##
7
9
  # IMAPProcessor is a client for processing messages on an IMAP server.
@@ -13,13 +15,24 @@ require 'imap_sasl_plain'
13
15
  # * An initialize method that connects to an IMAP server and sets the @imap
14
16
  # instance variable
15
17
  # * A run method that uses the IMAP connection to process messages.
18
+ #
19
+ # Reference:
20
+ #
21
+ # email: http://www.ietf.org/rfc/rfc0822.txt
22
+ # imap: http://www.ietf.org/rfc/rfc3501.txt
16
23
 
17
24
  class IMAPProcessor
18
25
 
19
26
  ##
20
27
  # The version of IMAPProcessor you are using
21
28
 
22
- VERSION = '1.1.1'
29
+ VERSION = "1.7"
30
+
31
+ ##
32
+ # Base IMAPProcessor error class
33
+
34
+ class Error < RuntimeError
35
+ end
23
36
 
24
37
  ##
25
38
  # A Connection Struct that has +imap+ and +capability+ accessors
@@ -67,7 +80,7 @@ class IMAPProcessor
67
80
  opts.on( "--move=MAILBOX",
68
81
  "Mailbox to move message to",
69
82
  "Default: #{options[:MoveTo].inspect}",
70
- "Options file name: MoveTo") do |mailbox|
83
+ "Options file name: :MoveTo") do |mailbox|
71
84
  options[:MoveTo] = mailbox
72
85
  end
73
86
  end
@@ -84,24 +97,28 @@ class IMAPProcessor
84
97
  # required_options = {
85
98
  # :MoveTo => [nil, "MoveTo not set"],
86
99
  # }
87
- #
88
- # super __FILE__, args, required_options do |opts, options|
89
- # opts.banner << "Explain my_processor's executable"
90
- #
91
- # opts.on( "--move=MAILBOX",
92
- # "Mailbox to move message to",
93
- # "Default: #{options[:MoveTo].inspect}",
94
- # "Options file name: MoveTo") do |mailbox|
95
- # options[:MoveTo] = mailbox
100
+ #
101
+ # super __FILE__, args, required_options do |opts, options|
102
+ # opts.banner << "Explain my_processor's executable"
103
+ #
104
+ # opts.on( "--move=MAILBOX",
105
+ # "Mailbox to move message to",
106
+ # "Default: #{options[:MoveTo].inspect}",
107
+ # "Options file name: :MoveTo") do |mailbox|
108
+ # options[:MoveTo] = mailbox
109
+ # end
96
110
  # end
97
111
  # end
98
112
  # end
113
+ #
114
+ # NOTE: You can add a --move option using ::add_move
99
115
 
100
116
  def self.process_args(processor_file, args,
101
117
  required_options = {}) # :yield: OptionParser
102
- opts_file_name = File.basename processor_file, '.rb'
103
- opts_file = File.expand_path "~/.#{opts_file_name}"
104
- options = @@options.dup
118
+ @@opts_file_name = File.basename processor_file, '.rb'
119
+ @@opts_file_name = "imap_#{@@opts_file_name}" unless
120
+ @@opts_file_name =~ /^imap_/
121
+ opts_file = File.expand_path "~/.#{@@opts_file_name}"
105
122
 
106
123
  if required_options then
107
124
  required_options.each do |option, (default, message)|
@@ -111,6 +128,8 @@ class IMAPProcessor
111
128
  end
112
129
  end
113
130
 
131
+ defaults = [{}]
132
+
114
133
  if File.exist? opts_file then
115
134
  unless File.stat(opts_file).mode & 077 == 0 then
116
135
  $stderr.puts "WARNING! #{opts_file} is group/other readable or writable!"
@@ -118,165 +137,188 @@ class IMAPProcessor
118
137
  exit 1
119
138
  end
120
139
 
121
- options.merge! YAML.load_file(opts_file)
140
+ defaults = Array(YAML.load_file(opts_file))
122
141
  end
123
142
 
124
- options[:SSL] ||= true
125
- options[:Username] ||= ENV['USER']
126
- options[:Root] ||= nil
127
- options[:Verbose] ||= false
128
- options[:Debug] ||= false
143
+ defaults.map { |default|
144
+ options = default.merge @@options.dup
129
145
 
130
- required_options.each do |k,(v,m)|
131
- options[k] ||= v
132
- end
146
+ options[:SSL] = true unless options.key? :SSL
147
+ options[:Username] ||= ENV['USER']
148
+ options[:Root] ||= nil
149
+ options[:Verbose] ||= false
150
+ options[:Debug] ||= false
133
151
 
134
- opts = OptionParser.new do |opts|
135
- opts.program_name = File.basename $0
136
- opts.banner = "Usage: #{opts.program_name} [options]\n\n"
152
+ required_options.each do |k,(v,_)|
153
+ options[k] ||= v
154
+ end
137
155
 
138
- opts.separator ''
139
- opts.separator 'Connection options:'
156
+ op = OptionParser.new do |opts|
157
+ opts.program_name = File.basename $0
158
+ opts.banner = "Usage: #{opts.program_name} [options]\n\n"
140
159
 
141
- opts.on("-H", "--host HOST",
142
- "IMAP server host",
143
- "Default: #{options[:Host].inspect}",
144
- "Options file name: Host") do |host|
145
- options[:Host] = host
146
- end
160
+ opts.separator ''
161
+ opts.separator 'Connection options:'
147
162
 
148
- opts.on("-P", "--port PORT",
149
- "IMAP server port",
150
- "Default: The correct port SSL/non-SSL mode",
151
- "Options file name: Port") do |port|
152
- options[:Port] = port
153
- end
163
+ opts.on_tail("-h", "--help", "Show this message") do
164
+ puts opts
165
+ exit
166
+ end
154
167
 
155
- opts.on("-s", "--[no-]ssl",
156
- "Use SSL for IMAP connection",
157
- "Default: #{options[:SSL].inspect}",
158
- "Options file name: SSL") do |ssl|
159
- options[:SSL] = ssl
160
- end
168
+ opts.on("-H", "--host HOST",
169
+ "IMAP server host",
170
+ "Default: #{options[:Host].inspect}",
171
+ "Options file name: :Host") do |host|
172
+ options[:Host] = host
173
+ end
161
174
 
162
- opts.on( "--[no-]debug",
163
- "Display Net::IMAP debugging info",
164
- "Default: #{options[:Debug].inspect}",
165
- "Options file name: Debug") do |debug|
166
- options[:Debug] = debug
167
- end
175
+ opts.on("-P", "--port PORT",
176
+ "IMAP server port",
177
+ "Default: The correct port SSL/non-SSL mode",
178
+ "Options file name: :Port") do |port|
179
+ options[:Port] = port
180
+ end
168
181
 
169
- opts.separator ''
170
- opts.separator 'Login options:'
182
+ opts.on("-s", "--[no-]ssl",
183
+ "Use SSL for IMAP connection",
184
+ "Default: #{options[:SSL].inspect}",
185
+ "Options file name: :SSL") do |ssl|
186
+ options[:SSL] = ssl
187
+ end
171
188
 
172
- opts.on("-u", "--username USERNAME",
173
- "IMAP username",
174
- "Default: #{options[:Username].inspect}",
175
- "Options file name: Username") do |username|
176
- options[:Username] = username
177
- end
189
+ opts.on( "--[no-]debug",
190
+ "Display Net::IMAP debugging info",
191
+ "Default: #{options[:Debug].inspect}",
192
+ "Options file name: :Debug") do |debug|
193
+ options[:Debug] = debug
194
+ end
178
195
 
179
- opts.on("-p", "--password PASSWORD",
180
- "IMAP password",
181
- "Default: Read from ~/.#{opts_file_name}",
182
- "Options file name: Password") do |password|
183
- options[:Password] = password
184
- end
196
+ opts.separator ''
197
+ opts.separator 'Login options:'
185
198
 
186
- authenticators = Net::IMAP.send :class_variable_get, :@@authenticators
187
- auth_types = authenticators.keys.sort.join ', '
188
- opts.on("-a", "--auth AUTH", auth_types,
189
- "IMAP authentication type override",
190
- "Authentication type will be auto-",
191
- "discovered",
192
- "Default: #{options[:Auth].inspect}",
193
- "Options file name: Auth") do |auth|
194
- options[:Auth] = auth
195
- end
199
+ opts.on("-u", "--username USERNAME",
200
+ "IMAP username",
201
+ "Default: #{options[:Username].inspect}",
202
+ "Options file name: :Username") do |username|
203
+ options[:Username] = username
204
+ end
196
205
 
197
- opts.separator ''
198
- opts.separator "IMAP options:"
206
+ opts.on("-p", "--password PASSWORD",
207
+ "IMAP password",
208
+ "Default: Read from ~/.#{@@opts_file_name}",
209
+ "Options file name: :Password") do |password|
210
+ options[:Password] = password
211
+ end
199
212
 
200
- opts.on("-r", "--root ROOT",
201
- "Root of mailbox hierarchy",
202
- "Default: #{options[:Root].inspect}",
203
- "Options file name: Root") do |root|
204
- options[:Root] = root
205
- end
213
+ authenticators = Net::IMAP.send :class_variable_get, :@@authenticators
214
+ auth_types = authenticators.keys.sort.join ', '
215
+ opts.on("-a", "--auth AUTH", auth_types,
216
+ "IMAP authentication type override",
217
+ "Authentication type will be auto-",
218
+ "discovered",
219
+ "Default: #{options[:Auth].inspect}",
220
+ "Options file name: :Auth") do |auth|
221
+ options[:Auth] = auth
222
+ end
206
223
 
207
- opts.on("-b", "--boxes BOXES", Array,
208
- "Comma-separated list of mailbox names",
209
- "to search",
210
- "Default: #{options[:Boxes].inspect}",
211
- "Options file name: Boxes") do |boxes|
212
- options[:Boxes] = boxes
213
- end
224
+ opts.separator ''
225
+ opts.separator "IMAP options:"
214
226
 
215
- opts.on("-v", "--[no-]verbose",
216
- "Be verbose",
217
- "Default: #{options[:Verbose].inspect}",
218
- "Options file name: Verbose") do |verbose|
219
- options[:Verbose] = verbose
220
- end
227
+ opts.on("-r", "--root ROOT",
228
+ "Root of mailbox hierarchy",
229
+ "Default: #{options[:Root].inspect}",
230
+ "Options file name: :Root") do |root|
231
+ options[:Root] = root
232
+ end
221
233
 
222
- opts.on("-q", "--quiet",
223
- "Be quiet") do
224
- options[:Verbose] = false
225
- end
234
+ opts.on("-b", "--boxes BOXES", Array,
235
+ "Comma-separated list of mailbox names",
236
+ "to search",
237
+ "Default: #{options[:Boxes].inspect}",
238
+ "Options file name: :Boxes") do |boxes|
239
+ options[:Boxes] = boxes
240
+ end
226
241
 
227
- if block_given? then
228
- opts.separator ''
229
- opts.separator "#{self} options:"
242
+ opts.on("-v", "--[no-]verbose",
243
+ "Be verbose",
244
+ "Default: #{options[:Verbose].inspect}",
245
+ "Options file name: :Verbose") do |verbose|
246
+ options[:Verbose] = verbose
247
+ end
230
248
 
231
- yield opts, options if block_given?
232
- end
249
+ opts.on("-n", "--noop",
250
+ "Perform no destructive operations",
251
+ "Best used with the verbose option",
252
+ "Default: #{options[:Noop].inspect}",
253
+ "Options file name: Noop") do |noop|
254
+ options[:Noop] = noop
255
+ end
233
256
 
234
- @@extra_options.each do |block|
235
- block.call opts, options
236
- end
257
+ opts.on("-q", "--quiet",
258
+ "Be quiet") do
259
+ options[:Verbose] = false
260
+ end
237
261
 
238
- opts.separator ''
262
+ if block_given? then
263
+ opts.separator ''
264
+ opts.separator "#{self} options:"
265
+
266
+ yield opts, options if block_given?
267
+ end
268
+
269
+ @@extra_options.each do |block|
270
+ block.call opts, options
271
+ end
272
+
273
+ opts.separator ''
239
274
 
240
- opts.banner << <<-EOF
275
+ opts.banner << <<-EOF
241
276
 
242
- Options may also be set in the options file ~/.#{opts_file_name}
277
+ Options may also be set in the options file ~/.#{@@opts_file_name}
243
278
 
244
- Example ~/.#{opts_file_name}:
279
+ Example ~/.#{@@opts_file_name}:
245
280
  \tHost=mail.example.com
246
281
  \tPassword=my password
247
282
 
248
- EOF
249
- end
283
+ EOF
284
+
285
+ end # OptionParser.new do
286
+
287
+ op.parse! args.dup
250
288
 
251
- opts.parse! args
252
-
253
- options[:Port] ||= options[:SSL] ? 993 : 143
254
-
255
- if options[:Host].nil? or
256
- options[:Password].nil? or
257
- options[:Boxes].nil? or
258
- required_options.any? { |k,(v,m)| options[k].nil? } then
259
- $stderr.puts opts
260
- $stderr.puts
261
- $stderr.puts "Host name not set" if options[:Host].nil?
262
- $stderr.puts "Password not set" if options[:Password].nil?
263
- $stderr.puts "Boxes not set" if options[:Boxes].nil?
264
- required_options.each do |option_name, (option_value, missing_message)|
265
- $stderr.puts missing_message if options[option_name].nil?
289
+ options[:Port] ||= options[:SSL] ? 993 : 143
290
+
291
+ # HACK: removed :Boxes -- push down
292
+ required_keys = [:Host, :Password] + required_options.keys
293
+ if required_keys.any? { |k| options[k].nil? } then
294
+ $stderr.puts op
295
+ $stderr.puts
296
+ $stderr.puts "Host name not set" if options[:Host].nil?
297
+ $stderr.puts "Password not set" if options[:Password].nil?
298
+ $stderr.puts "Boxes not set" if options[:Boxes].nil?
299
+ required_options.each do |option_name, (_, missing_message)|
300
+ $stderr.puts missing_message if options[option_name].nil?
301
+ end
302
+ exit 1
266
303
  end
267
- exit 1
268
- end
269
304
 
270
- return options
305
+ options
306
+ } # defaults.map
271
307
  end
272
308
 
273
309
  ##
274
310
  # Sets up an IMAP processor's options then calls its \#run method.
275
311
 
276
312
  def self.run(args = ARGV, &block)
277
- options = process_args args
278
- client = new(options, &block)
279
- client.run
313
+ client = nil
314
+ multi_options = process_args args
315
+
316
+ multi_options.each do |options|
317
+ client = new(options, &block)
318
+ client.run
319
+ end
320
+ rescue Interrupt
321
+ exit
280
322
  rescue SystemExit
281
323
  raise
282
324
  rescue Exception => e
@@ -285,7 +327,7 @@ Example ~/.#{opts_file_name}:
285
327
 
286
328
  exit 1
287
329
  ensure
288
- client.imap.logout if client
330
+ client.imap.logout if client and client.imap
289
331
  end
290
332
 
291
333
  ##
@@ -299,34 +341,102 @@ Example ~/.#{opts_file_name}:
299
341
  Net::IMAP.debug = options[:Debug]
300
342
  end
301
343
 
344
+ ##
345
+ # Extracts capability information for +imap+ from +res+ or by contacting the
346
+ # server.
347
+
348
+ def capability imap, res = nil
349
+ return imap.capability unless res
350
+
351
+ data = res.data
352
+
353
+ if data.code and data.code.name == 'CAPABILITY' then
354
+ data.code.data.split ' '
355
+ else
356
+ imap.capability
357
+ end
358
+ end
359
+
302
360
  ##
303
361
  # Connects to IMAP server +host+ at +port+ using ssl if +ssl+ is true then
304
- # logs in as +username+ with +password+. IMAPProcessor is only known to
305
- # work with PLAIN auth on SSL sockets.
362
+ # authenticates with +username+ and +password+. IMAPProcessor is only known
363
+ # to work with PLAIN auth on SSL sockets. IMAPProcessor does not support
364
+ # LOGIN.
306
365
  #
307
366
  # Returns a Connection object.
308
367
 
309
- def connect(host, port, ssl, username, password, auth = nil)
310
- imap = Net::IMAP.new host, port, ssl
368
+ def connect(host = @options[:Host],
369
+ port = @options[:Port],
370
+ ssl = @options[:SSL],
371
+ username = @options[:Username],
372
+ password = @options[:Password],
373
+ auth = @options[:Auth]) # :yields: Connection
374
+ imap = Net::IMAP.new host, port, ssl, nil, false
311
375
  log "Connected to imap://#{host}:#{port}/"
312
376
 
313
- capability = imap.capability
377
+ capabilities = capability imap, imap.greeting
314
378
 
315
- log "Capabilities: #{capability.join ', '}"
379
+ log "Capabilities: #{capabilities.join ', '}"
316
380
 
317
- auth_caps = capability.select { |c| c =~ /^AUTH/ }
381
+ auth_caps = capabilities.select { |c| c =~ /^AUTH/ }
318
382
 
319
383
  if auth.nil? then
320
384
  raise "Couldn't find a supported auth type" if auth_caps.empty?
321
385
  auth = auth_caps.first.sub(/AUTH=/, '')
322
386
  end
323
387
 
324
- auth = auth.upcase
325
- log "Trying #{auth} authentication"
326
- imap.authenticate auth, username, password
327
- log "Logged in as #{username}"
388
+ # Net::IMAP supports using AUTHENTICATE with LOGIN, PLAIN, and
389
+ # CRAM-MD5... if the server reports a different AUTH method, then we
390
+ # should fall back to using LOGIN
391
+ if %w( LOGIN PLAIN CRAM-MD5 XOAUTH2 ).include?( auth.upcase )
392
+ auth = auth.upcase
393
+ log "Trying #{auth} authentication"
394
+ res = imap.authenticate auth, username, password
395
+ log "Logged in as #{username} using AUTHENTICATE"
396
+ else
397
+ log "Trying to authenticate via LOGIN"
398
+ res = imap.login username, password
399
+ log "Logged in as #{username} using LOGIN"
400
+ end
401
+
402
+ # CAPABILITY may have changed
403
+ capabilities = capability imap, res
404
+
405
+ connection = Connection.new imap, capabilities
406
+
407
+ if block_given? then
408
+ begin
409
+ yield connection
410
+ ensure
411
+ connection.imap.logout
412
+ end
413
+ else
414
+ return connection
415
+ end
416
+ end
417
+
418
+ ##
419
+ # Create the mailbox +name+ if it doesn't exist. Note that this will SELECT
420
+ # the mailbox if it exists.
421
+
422
+ def create_mailbox name
423
+ log "LIST #{name}"
424
+ list = imap.list '', name
425
+ return if list
426
+ log "CREATE #{name}"
427
+ imap.create name unless noop?
428
+ end
328
429
 
329
- Connection.new imap, capability
430
+ ##
431
+ # Delete and +expunge+ the specified +uids+.
432
+
433
+ def delete_messages uids, expunge = true
434
+ log "DELETING [...#{uids.size} uids]"
435
+ imap.store uids, '+FLAGS.SILENT', [:Deleted] unless noop?
436
+ if expunge then
437
+ log "EXPUNGE"
438
+ imap.expunge unless noop?
439
+ end
330
440
  end
331
441
 
332
442
  ##
@@ -347,8 +457,6 @@ Example ~/.#{opts_file_name}:
347
457
  uids = []
348
458
 
349
459
  each_part parts, true do |uid, message|
350
- skip = false
351
-
352
460
  mail = TMail::Mail.parse message
353
461
 
354
462
  begin
@@ -379,7 +487,7 @@ Example ~/.#{opts_file_name}:
379
487
  sequence.unshift "BODY[#{section}.MIME]" unless section == 'TEXT'
380
488
  sequence.unshift 'BODY[HEADER]' if header
381
489
 
382
- body = @imap.fetch(uid, sequence).first
490
+ body = imap.fetch(uid, sequence).first
383
491
 
384
492
  sequence = sequence.map { |item| body.attr[item] }
385
493
 
@@ -411,7 +519,7 @@ Example ~/.#{opts_file_name}:
411
519
  def mime_parts(uids, mime_type)
412
520
  media_type, subtype = mime_type.upcase.split('/', 2)
413
521
 
414
- structures = @imap.fetch uids, 'BODYSTRUCTURE'
522
+ structures = imap.fetch uids, 'BODYSTRUCTURE'
415
523
 
416
524
  structures.zip(uids).map do |body, uid|
417
525
  section = nil
@@ -436,6 +544,43 @@ Example ~/.#{opts_file_name}:
436
544
  end.compact
437
545
  end
438
546
 
547
+ ##
548
+ # Move the specified +uids+ to a new +destination+ then delete and +expunge+
549
+ # them. Creates the destination mailbox if it doesn't exist.
550
+
551
+ def move_messages uids, destination, expunge = true
552
+ return if uids.empty?
553
+ log "COPY [...#{uids.size} uids]"
554
+
555
+ begin
556
+ imap.copy uids, destination unless noop?
557
+ rescue Net::IMAP::NoResponseError
558
+ unless noop? then
559
+ create_mailbox destination
560
+ imap.copy uids, destination
561
+ end
562
+ end
563
+
564
+ delete_messages uids, expunge
565
+ end
566
+
567
+ ##
568
+ # Displays Date, Subject and Message-Id from messages in +uids+
569
+
570
+ def show_messages(uids)
571
+ return if uids.nil? or (Array === uids and uids.empty?)
572
+
573
+ fetch_data = 'BODY.PEEK[HEADER.FIELDS (DATE FROM TO SUBJECT)]'
574
+ messages = imap.fetch uids, fetch_data
575
+ fetch_data.sub! '.PEEK', '' # stripped by server
576
+
577
+ messages ||= []
578
+
579
+ messages.each do |res|
580
+ puts res.attr[fetch_data].delete("\r").gsub(/^/, " ")
581
+ end
582
+ end
583
+
439
584
  ##
440
585
  # Did the user set --verbose?
441
586
 
@@ -443,5 +588,10 @@ Example ~/.#{opts_file_name}:
443
588
  @verbose
444
589
  end
445
590
 
446
- end
591
+ ##
592
+ # Did the user set --noop?
447
593
 
594
+ def noop?
595
+ options[:Noop]
596
+ end
597
+ end