sup 0.19.0

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