sup 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +12 -0
  4. data/CONTRIBUTORS +84 -0
  5. data/Gemfile +3 -0
  6. data/HACKING +42 -0
  7. data/History.txt +361 -0
  8. data/LICENSE +280 -0
  9. data/README.md +70 -0
  10. data/Rakefile +12 -0
  11. data/ReleaseNotes +231 -0
  12. data/bin/sup +434 -0
  13. data/bin/sup-add +118 -0
  14. data/bin/sup-config +243 -0
  15. data/bin/sup-dump +43 -0
  16. data/bin/sup-import-dump +101 -0
  17. data/bin/sup-psych-ify-config-files +21 -0
  18. data/bin/sup-recover-sources +87 -0
  19. data/bin/sup-sync +210 -0
  20. data/bin/sup-sync-back-maildir +127 -0
  21. data/bin/sup-tweak-labels +140 -0
  22. data/contrib/colorpicker.rb +100 -0
  23. data/contrib/completion/_sup.zsh +114 -0
  24. data/devel/console.sh +3 -0
  25. data/devel/count-loc.sh +3 -0
  26. data/devel/load-index.rb +9 -0
  27. data/devel/profile.rb +12 -0
  28. data/devel/start-console.rb +5 -0
  29. data/doc/FAQ.txt +119 -0
  30. data/doc/Hooks.txt +79 -0
  31. data/doc/Philosophy.txt +69 -0
  32. data/lib/sup.rb +467 -0
  33. data/lib/sup/account.rb +90 -0
  34. data/lib/sup/buffer.rb +768 -0
  35. data/lib/sup/colormap.rb +239 -0
  36. data/lib/sup/contact.rb +67 -0
  37. data/lib/sup/crypto.rb +461 -0
  38. data/lib/sup/draft.rb +119 -0
  39. data/lib/sup/hook.rb +159 -0
  40. data/lib/sup/horizontal_selector.rb +59 -0
  41. data/lib/sup/idle.rb +42 -0
  42. data/lib/sup/index.rb +882 -0
  43. data/lib/sup/interactive_lock.rb +89 -0
  44. data/lib/sup/keymap.rb +140 -0
  45. data/lib/sup/label.rb +87 -0
  46. data/lib/sup/logger.rb +77 -0
  47. data/lib/sup/logger/singleton.rb +10 -0
  48. data/lib/sup/maildir.rb +257 -0
  49. data/lib/sup/mbox.rb +187 -0
  50. data/lib/sup/message.rb +803 -0
  51. data/lib/sup/message_chunks.rb +328 -0
  52. data/lib/sup/mode.rb +140 -0
  53. data/lib/sup/modes/buffer_list_mode.rb +50 -0
  54. data/lib/sup/modes/completion_mode.rb +55 -0
  55. data/lib/sup/modes/compose_mode.rb +38 -0
  56. data/lib/sup/modes/console_mode.rb +125 -0
  57. data/lib/sup/modes/contact_list_mode.rb +148 -0
  58. data/lib/sup/modes/edit_message_async_mode.rb +110 -0
  59. data/lib/sup/modes/edit_message_mode.rb +728 -0
  60. data/lib/sup/modes/file_browser_mode.rb +109 -0
  61. data/lib/sup/modes/forward_mode.rb +82 -0
  62. data/lib/sup/modes/help_mode.rb +19 -0
  63. data/lib/sup/modes/inbox_mode.rb +85 -0
  64. data/lib/sup/modes/label_list_mode.rb +138 -0
  65. data/lib/sup/modes/label_search_results_mode.rb +38 -0
  66. data/lib/sup/modes/line_cursor_mode.rb +203 -0
  67. data/lib/sup/modes/log_mode.rb +57 -0
  68. data/lib/sup/modes/person_search_results_mode.rb +12 -0
  69. data/lib/sup/modes/poll_mode.rb +19 -0
  70. data/lib/sup/modes/reply_mode.rb +228 -0
  71. data/lib/sup/modes/resume_mode.rb +52 -0
  72. data/lib/sup/modes/scroll_mode.rb +252 -0
  73. data/lib/sup/modes/search_list_mode.rb +204 -0
  74. data/lib/sup/modes/search_results_mode.rb +59 -0
  75. data/lib/sup/modes/text_mode.rb +76 -0
  76. data/lib/sup/modes/thread_index_mode.rb +1033 -0
  77. data/lib/sup/modes/thread_view_mode.rb +941 -0
  78. data/lib/sup/person.rb +134 -0
  79. data/lib/sup/poll.rb +272 -0
  80. data/lib/sup/rfc2047.rb +56 -0
  81. data/lib/sup/search.rb +110 -0
  82. data/lib/sup/sent.rb +58 -0
  83. data/lib/sup/service/label_service.rb +45 -0
  84. data/lib/sup/source.rb +244 -0
  85. data/lib/sup/tagger.rb +50 -0
  86. data/lib/sup/textfield.rb +253 -0
  87. data/lib/sup/thread.rb +452 -0
  88. data/lib/sup/time.rb +93 -0
  89. data/lib/sup/undo.rb +38 -0
  90. data/lib/sup/update.rb +30 -0
  91. data/lib/sup/util.rb +747 -0
  92. data/lib/sup/util/ncurses.rb +274 -0
  93. data/lib/sup/util/path.rb +9 -0
  94. data/lib/sup/util/query.rb +17 -0
  95. data/lib/sup/util/uri.rb +15 -0
  96. data/lib/sup/version.rb +3 -0
  97. data/sup.gemspec +53 -0
  98. data/test/dummy_source.rb +61 -0
  99. data/test/gnupg_test_home/gpg.conf +1 -0
  100. data/test/gnupg_test_home/pubring.gpg +0 -0
  101. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  102. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  103. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  104. data/test/gnupg_test_home/secring.gpg +0 -0
  105. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -0
  106. data/test/gnupg_test_home/trustdb.gpg +0 -0
  107. data/test/integration/test_label_service.rb +18 -0
  108. data/test/messages/bad-content-transfer-encoding-1.eml +8 -0
  109. data/test/messages/binary-content-transfer-encoding-2.eml +21 -0
  110. data/test/messages/missing-line.eml +9 -0
  111. data/test/test_crypto.rb +109 -0
  112. data/test/test_header_parsing.rb +168 -0
  113. data/test/test_helper.rb +7 -0
  114. data/test/test_message.rb +532 -0
  115. data/test/test_messages_dir.rb +147 -0
  116. data/test/test_yaml_migration.rb +85 -0
  117. data/test/test_yaml_regressions.rb +17 -0
  118. data/test/unit/service/test_label_service.rb +19 -0
  119. data/test/unit/test_horizontal_selector.rb +40 -0
  120. data/test/unit/util/test_query.rb +46 -0
  121. data/test/unit/util/test_string.rb +57 -0
  122. data/test/unit/util/test_uri.rb +19 -0
  123. metadata +423 -0
data/bin/sup ADDED
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
5
+
6
+ require 'rubygems'
7
+ require 'ncursesw'
8
+
9
+ require 'sup/util/ncurses'
10
+
11
+ no_gpgme = false
12
+ begin
13
+ require 'gpgme'
14
+ rescue LoadError
15
+ no_gpgme = true
16
+ end
17
+
18
+ require 'fileutils'
19
+ require 'trollop'
20
+ require "sup"
21
+
22
+ if ENV['SUP_PROFILE']
23
+ require 'ruby-prof'
24
+ RubyProf.start
25
+ end
26
+
27
+ if no_gpgme
28
+ info "No 'gpgme' gem detected. Install it for email encryption, decryption and signatures."
29
+ end
30
+
31
+ $opts = Trollop::options do
32
+ version "sup v#{Redwood::VERSION}"
33
+ banner <<EOS
34
+ Sup is a curses-based email client.
35
+
36
+ Usage:
37
+ sup [options]
38
+
39
+ Options are:
40
+ EOS
41
+ opt :list_hooks, "List all hooks and descriptions, and quit."
42
+ opt :no_threads, "Turn off threading. Helps with debugging. (Necessarily disables background polling for new messages.)"
43
+ opt :no_initial_poll, "Don't poll for new messages when starting."
44
+ opt :search, "Search for this query upon startup", :type => String
45
+ opt :compose, "Compose message to this recipient upon startup", :type => String
46
+ opt :subject, "When composing, use this subject", :type => String, :short => "j"
47
+ end
48
+
49
+ Trollop::die :subject, "requires --compose" if $opts[:subject] && !$opts[:compose]
50
+
51
+ Redwood::HookManager.register "startup", <<EOS
52
+ Executes at startup
53
+ No variables.
54
+ No return value.
55
+ EOS
56
+
57
+ Redwood::HookManager.register "shutdown", <<EOS
58
+ Executes when sup is shutting down. May be run when sup is crashing,
59
+ so don\'t do anything too important. Run before the label, contacts,
60
+ and people are saved.
61
+ No variables.
62
+ No return value.
63
+ EOS
64
+
65
+ if $opts[:list_hooks]
66
+ Redwood.start
67
+ Redwood::HookManager.print_hooks
68
+ exit
69
+ end
70
+
71
+ Thread.abort_on_exception = true # make debugging possible
72
+ Thread.current.priority = 1 # keep ui responsive
73
+
74
+ module Redwood
75
+
76
+ global_keymap = Keymap.new do |k|
77
+ k.add :quit_ask, "Quit Sup, but ask first", 'q'
78
+ k.add :quit_now, "Quit Sup immediately", 'Q'
79
+ k.add :help, "Show help", '?'
80
+ k.add :roll_buffers, "Switch to next buffer", 'b'
81
+ k.add :roll_buffers_backwards, "Switch to previous buffer", 'B'
82
+ k.add :kill_buffer, "Kill the current buffer", 'x'
83
+ k.add :list_buffers, "List all buffers", ';'
84
+ k.add :list_contacts, "List contacts", 'C'
85
+ k.add :redraw, "Redraw screen", :ctrl_l
86
+ k.add :search, "Search all messages", '\\', 'F'
87
+ k.add :search_unread, "Show all unread messages", 'U'
88
+ k.add :list_labels, "List labels", 'L'
89
+ k.add :poll, "Poll for new messages", 'P'
90
+ k.add :poll_unusual, "Poll for new messages from unusual sources", '{'
91
+ k.add :compose, "Compose new message", 'm', 'c'
92
+ k.add :nothing, "Do nothing", :ctrl_g
93
+ k.add :recall_draft, "Edit most recent draft message", 'R'
94
+ k.add :show_inbox, "Show the Inbox buffer", 'I'
95
+ k.add :clear_hooks, "Clear all hooks", 'H'
96
+ k.add :show_console, "Show the Console buffer", '~'
97
+
98
+ ## Submap for less often used keybindings
99
+ k.add_multi "reload (c)olors, rerun (k)eybindings hook", 'O' do |kk|
100
+ kk.add :reload_colors, "Reload colors", 'c'
101
+ kk.add :run_keybindings_hook, "Rerun keybindings hook", 'k'
102
+ end
103
+ end
104
+
105
+ ## the following magic enables wide characters when used with a ruby
106
+ ## ncurses.so that's been compiled against libncursesw. (note the w.) why
107
+ ## this works, i have no idea. much like pretty much every aspect of
108
+ ## dealing with curses. cargo cult programming at its best.
109
+ require 'dl/import'
110
+ require 'rbconfig'
111
+ module LibC
112
+ extend DL.const_defined?(:Importer) ? DL::Importer : DL::Importable
113
+ setlocale_lib = case RbConfig::CONFIG['arch']
114
+ when /darwin/; "libc.dylib"
115
+ when /cygwin/; "cygwin1.dll"
116
+ when /freebsd/; "libc.so.7"
117
+ else; "libc.so.6"
118
+ end
119
+
120
+ debug "dynamically loading setlocale() from #{setlocale_lib}"
121
+ begin
122
+ dlload setlocale_lib
123
+ extern "void setlocale(int, const char *)"
124
+ debug "setting locale..."
125
+ LibC.setlocale(6, "") # LC_ALL == 6
126
+ rescue RuntimeError => e
127
+ warn "cannot dlload setlocale(); ncurses wide character support probably broken."
128
+ warn "dlload error was #{e.class}: #{e.message}"
129
+ end
130
+ end
131
+
132
+ def start_cursing
133
+ Ncurses.initscr
134
+ Ncurses.noecho
135
+ Ncurses.cbreak
136
+ Ncurses.stdscr.keypad 1
137
+ Ncurses.use_default_colors
138
+ Ncurses.curs_set 0
139
+ Ncurses.start_color
140
+ Ncurses.prepare_form_driver
141
+ $cursing = true
142
+ end
143
+
144
+ def stop_cursing
145
+ return unless $cursing
146
+ Ncurses.curs_set 1
147
+ Ncurses.echo
148
+ Ncurses.endwin
149
+ end
150
+ module_function :start_cursing, :stop_cursing
151
+
152
+ Index.init
153
+ Index.lock_interactively or exit
154
+
155
+ begin
156
+ Redwood::start
157
+ Index.load
158
+ Redwood::check_syncback_settings
159
+ Index.start_sync_worker unless $opts[:no_threads]
160
+
161
+ $die = false
162
+ trap("TERM") { |x| $die = true }
163
+ trap("WINCH") do |x|
164
+ ::Thread.new do
165
+ BufferManager.sigwinch_happened!
166
+ end
167
+ end
168
+
169
+ if(s = Redwood::SourceManager.source_for DraftManager.source_name)
170
+ DraftManager.source = s
171
+ else
172
+ debug "no draft source, auto-adding..."
173
+ Redwood::SourceManager.add_source DraftManager.new_source
174
+ end
175
+
176
+ if(s = Redwood::SourceManager.source_for SentManager.source_uri)
177
+ SentManager.source = s
178
+ else
179
+ Redwood::SourceManager.add_source SentManager.default_source
180
+ end
181
+
182
+ HookManager.run "startup"
183
+ Redwood::Keymap.run_hook global_keymap
184
+
185
+ debug "starting curses"
186
+ Redwood::Logger.remove_sink $stderr
187
+ start_cursing
188
+
189
+ bm = BufferManager.init
190
+ Colormap.new.populate_colormap
191
+
192
+ debug "initializing log buffer"
193
+ lmode = Redwood::LogMode.new "system log"
194
+ lmode.on_kill { Logger.clear! }
195
+ Logger.add_sink lmode
196
+ Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
197
+ if Logger::LEVELS.index(Logger.level) > 0
198
+ Logger.force_message "For more verbose logging, restart with SUP_LOG_LEVEL=#{Logger::LEVELS[Logger::LEVELS.index(Logger.level)-1]}."
199
+ end
200
+
201
+ debug "initializing inbox buffer"
202
+ imode = InboxMode.new
203
+ ibuf = bm.spawn "Inbox", imode
204
+
205
+ debug "ready for interaction!"
206
+
207
+ bm.draw_screen
208
+
209
+ Redwood::SourceManager.usual_sources.each do |s|
210
+ next unless s.respond_to? :connect
211
+ reporting_thread("call #connect on #{s}") do
212
+ begin
213
+ s.connect
214
+ rescue SourceError => e
215
+ error "fatal error loading from #{s}: #{e.message}"
216
+ end
217
+ end
218
+ end unless $opts[:no_initial_poll]
219
+
220
+ imode.load_threads :num => ibuf.content_height, :when_done => lambda { |num| reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
221
+
222
+ if $opts[:compose]
223
+ to = Person.from_address_list $opts[:compose]
224
+ mode = ComposeMode.new :to => to, :subj => $opts[:subject]
225
+ BufferManager.spawn "New Message", mode
226
+ mode.default_edit_message
227
+ end
228
+
229
+ unless $opts[:no_threads]
230
+ PollManager.start
231
+ IdleManager.start
232
+ Index.start_lock_update_thread
233
+ end
234
+
235
+ if $opts[:search]
236
+ SearchResultsMode.spawn_from_query $opts[:search]
237
+ end
238
+
239
+ until Redwood::exceptions.nonempty? || $die
240
+ c = begin
241
+ Ncurses::CharCode.get false
242
+ rescue Interrupt
243
+ raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
244
+ BufferManager.draw_screen
245
+ Ncurses::CharCode.empty
246
+ end
247
+
248
+ if c.empty?
249
+ if BufferManager.sigwinch_happened?
250
+ debug "redrawing screen on sigwinch"
251
+ BufferManager.completely_redraw_screen
252
+ end
253
+ next
254
+ end
255
+
256
+ IdleManager.ping
257
+
258
+ if c.is_keycode? 410
259
+ ## this is ncurses's way of telling us it's detected a refresh.
260
+ ## since we have our own sigwinch handler, we don't do anything.
261
+ next
262
+ end
263
+
264
+ bm.erase_flash
265
+
266
+ action =
267
+ begin
268
+ if bm.handle_input c
269
+ :nothing
270
+ else
271
+ bm.resolve_input_with_keymap c, global_keymap
272
+ end
273
+ rescue InputSequenceAborted
274
+ :nothing
275
+ end
276
+ case action
277
+ when :quit_now
278
+ break if bm.kill_all_buffers_safely
279
+ when :quit_ask
280
+ if bm.ask_yes_or_no "Really quit?"
281
+ break if bm.kill_all_buffers_safely
282
+ end
283
+ when :help
284
+ curmode = bm.focus_buf.mode
285
+ bm.spawn_unless_exists("<help for #{curmode.name}>") { HelpMode.new curmode, global_keymap }
286
+ when :roll_buffers
287
+ bm.roll_buffers
288
+ when :roll_buffers_backwards
289
+ bm.roll_buffers_backwards
290
+ when :kill_buffer
291
+ bm.kill_buffer_safely bm.focus_buf
292
+ when :list_buffers
293
+ bm.spawn_unless_exists("buffer list", :system => true) { BufferListMode.new }
294
+ when :list_contacts
295
+ b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
296
+ b.mode.load_in_background if new
297
+ when :search
298
+ completions = LabelManager.all_labels.map { |l| "label:#{LabelManager.string_for l}" }
299
+ completions = completions.each { |l| l.fix_encoding! }
300
+ completions += Index::COMPL_PREFIXES
301
+ query = BufferManager.ask_many_with_completions :search, "Search all messages (enter for saved searches): ", completions
302
+ unless query.nil?
303
+ if query.empty?
304
+ bm.spawn_unless_exists("Saved searches") { SearchListMode.new }
305
+ else
306
+ SearchResultsMode.spawn_from_query query
307
+ end
308
+ end
309
+ when :search_unread
310
+ SearchResultsMode.spawn_from_query "is:unread"
311
+ when :list_labels
312
+ labels = LabelManager.all_labels.map { |l| LabelManager.string_for l }
313
+ labels = labels.each { |l| l.fix_encoding! }
314
+
315
+ user_label = bm.ask_with_completions :label, "Show threads with label (enter for listing): ", labels
316
+ unless user_label.nil?
317
+ if user_label.empty?
318
+ bm.spawn_unless_exists("Label list") { LabelListMode.new } if user_label && user_label.empty?
319
+ else
320
+ LabelSearchResultsMode.spawn_nicely user_label
321
+ end
322
+ end
323
+ when :compose
324
+ ComposeMode.spawn_nicely
325
+ when :poll
326
+ reporting_thread("user-invoked poll") { PollManager.poll }
327
+ when :poll_unusual
328
+ if BufferManager.ask_yes_or_no "Really poll unusual sources?"
329
+ reporting_thread("user-invoked unusual poll") { PollManager.poll_unusual }
330
+ end
331
+ when :recall_draft
332
+ case Index.num_results_for :label => :draft
333
+ when 0
334
+ bm.flash "No draft messages."
335
+ when 1
336
+ m = nil
337
+ Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
338
+ r = ResumeMode.new(m)
339
+ BufferManager.spawn "Edit message", r
340
+ r.default_edit_message
341
+ else
342
+ b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
343
+ b.mode.load_threads :num => b.content_height if new
344
+ end
345
+ when :show_inbox
346
+ BufferManager.raise_to_front ibuf
347
+ when :clear_hooks
348
+ HookManager.clear
349
+ when :show_console
350
+ b, new = bm.spawn_unless_exists("Console", :system => true) { ConsoleMode.new }
351
+ b.mode.run
352
+ when :reload_colors
353
+ Colormap.reset
354
+ Colormap.populate_colormap
355
+ bm.completely_redraw_screen
356
+ bm.flash "reloaded colors"
357
+ when :run_keybindings_hook
358
+ HookManager.clear_one 'keybindings'
359
+ Keymap.run_hook global_keymap
360
+ bm.flash "keybindings hook run"
361
+ when :nothing, InputSequenceAborted
362
+ when :redraw
363
+ bm.completely_redraw_screen
364
+ else
365
+ bm.flash "Unknown keypress '#{c.to_character}' for #{bm.focus_buf.mode.name}."
366
+ end
367
+
368
+ bm.draw_screen
369
+ end
370
+
371
+ bm.kill_all_buffers if $die
372
+ rescue Exception => e
373
+ Redwood::record_exception e, "main"
374
+ ensure
375
+ unless $opts[:no_threads]
376
+ PollManager.stop if PollManager.instantiated?
377
+ IdleManager.stop if IdleManager.instantiated?
378
+ Index.stop_lock_update_thread
379
+ end
380
+
381
+ HookManager.run "shutdown" if HookManager.instantiated?
382
+
383
+ Index.stop_sync_worker
384
+ Redwood::finish
385
+ stop_cursing
386
+ Redwood::Logger.remove_all_sinks!
387
+ Redwood::Logger.add_sink $stderr, false
388
+ debug "stopped cursing"
389
+
390
+ if $die
391
+ info "I've been ordered to commit seppuku. I obey!"
392
+ end
393
+
394
+ if Redwood::exceptions.empty?
395
+ debug "no fatal errors. good job, william."
396
+ Index.save
397
+ else
398
+ error "oh crap, an exception"
399
+ end
400
+
401
+ Index.unlock
402
+
403
+ if (fn = ENV['SUP_PROFILE'])
404
+ result = RubyProf.stop
405
+ File.open(fn, 'w') { |io| RubyProf::CallTreePrinter.new(result).print(io) }
406
+ end
407
+ end
408
+
409
+ unless Redwood::exceptions.empty?
410
+ File.open(File.join(BASE_DIR, "exception-log.txt"), "w") do |f|
411
+ Redwood::exceptions.each do |e, name|
412
+ f.puts "--- #{e.class.name} from thread: #{name}"
413
+ f.puts e.message, e.backtrace
414
+ end
415
+ end
416
+ $stderr.puts <<EOS
417
+ ----------------------------------------------------------------
418
+ We are very sorry. It seems that an error occurred in Sup. Please
419
+ accept our sincere apologies. Please submit the contents of
420
+ #{BASE_DIR}/exception-log.txt and a brief report of the
421
+ circumstances to https://github.com/sup-heliotrope/sup/issues so that
422
+ we might address this problem. Thank you!
423
+
424
+ Sincerely,
425
+ The Sup Developers
426
+ ----------------------------------------------------------------
427
+ EOS
428
+ Redwood::exceptions.each do |e, name|
429
+ puts "--- #{e.class.name} from thread: #{name}"
430
+ puts e.message, e.backtrace
431
+ end
432
+ end
433
+
434
+ end
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
4
+
5
+ require 'uri'
6
+ require 'rubygems'
7
+ require 'highline/import'
8
+ require 'trollop'
9
+ require "sup"
10
+
11
+ $opts = Trollop::options do
12
+ version "sup-add (sup #{Redwood::VERSION})"
13
+ banner <<EOS
14
+ Adds a source to the Sup source list.
15
+
16
+ Usage:
17
+ sup-add [options] <source uri>+
18
+
19
+ where <source uri>+ is one or more source URIs.
20
+
21
+ For mbox files on local disk, use the form:
22
+ mbox:<path to mbox file>, or
23
+ mbox://<path to mbox file>
24
+
25
+ For Maildir folders, use the form:
26
+ maildir:<path to Maildir directory>; or
27
+ maildir://<path to Maildir directory>
28
+
29
+ Options are:
30
+ EOS
31
+ opt :archive, "Automatically archive all new messages from these sources."
32
+ opt :unusual, "Do not automatically poll these sources for new messages."
33
+ opt :sync_back, "Synchronize status flags back into messages, defaults to true (Maildir sources only).", :default => true
34
+ opt :labels, "A comma-separated set of labels to apply to all messages from this source", :type => String
35
+ opt :force_new, "Create a new account for this source, even if one already exists."
36
+ opt :force_account, "Reuse previously defined account user@hostname.", :type => String
37
+ end
38
+
39
+ Trollop::die "require one or more sources" if ARGV.empty?
40
+
41
+ ## for sources that require login information, prompt the user for
42
+ ## that. also provide a list of previously-defined login info to
43
+ ## choose from, if any.
44
+ def get_login_info uri, sources
45
+ uri = URI(uri)
46
+ accounts = sources.map do |s|
47
+ next unless s.respond_to?(:username)
48
+ suri = URI(s.uri)
49
+ [suri.host, s.username, s.password]
50
+ end.compact.uniq.sort_by { |h, u, p| h == uri.host ? 0 : 1 }
51
+
52
+ username, password = nil, nil
53
+ unless accounts.empty? || $opts[:force_new]
54
+ if $opts[:force_account]
55
+ host, username, password = accounts.find { |h, u, p| $opts[:force_account] == "#{u}@#{h}" }
56
+ unless username && password
57
+ say "No previous account #{$opts[:force_account].inspect} found."
58
+ end
59
+ else
60
+ say "Would you like to use the same account as for a previous source for #{uri}?"
61
+ choose do |menu|
62
+ accounts.each do |host, olduser, oldpw|
63
+ menu.choice("Use the account info for #{olduser}@#{host}") { username, password = olduser, oldpw }
64
+ end
65
+ menu.choice("Use a new account") { }
66
+ menu.prompt = "Account selection? "
67
+ end
68
+ end
69
+ end
70
+
71
+ unless username && password
72
+ username = ask("Username for #{uri.host}: ");
73
+ password = ask("Password for #{uri.host}: ") { |q| q.echo = false }
74
+ puts # why?
75
+ end
76
+
77
+ [username, password]
78
+ end
79
+
80
+ $terminal.wrap_at = :auto
81
+ Redwood::start
82
+ index = Redwood::Index.init
83
+ index.load
84
+
85
+ index.lock_interactively or exit
86
+
87
+ begin
88
+ Redwood::SourceManager.load_sources
89
+
90
+ ARGV.each do |uri|
91
+ labels = $opts[:labels] ? $opts[:labels].split(/\s*,\s*/).uniq : []
92
+
93
+ if !$opts[:force_new] && Redwood::SourceManager.source_for(uri)
94
+ say "Already know about #{uri}; skipping."
95
+ next
96
+ end
97
+
98
+ parsed_uri = URI(uri)
99
+
100
+ source =
101
+ case parsed_uri.scheme
102
+ when "maildir"
103
+ Redwood::Maildir.new uri, !$opts[:unusual], $opts[:archive], $opts[:sync_back], nil, labels
104
+ when "mbox"
105
+ Redwood::MBox.new uri, !$opts[:unusual], $opts[:archive], nil, labels
106
+ when nil
107
+ Trollop::die "Sources must be specified with an URI"
108
+ else
109
+ Trollop::die "Unknown source type #{parsed_uri.scheme.inspect}"
110
+ end
111
+ say "Adding #{source}..."
112
+ Redwood::SourceManager.add_source source
113
+ end
114
+ ensure
115
+ index.save
116
+ index.unlock
117
+ Redwood::finish
118
+ end