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,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